Repository: akuity/kargo

Issue:
Google Artifact Registry credentials should be more flexible
# Checklist

* [X] I've searched the issue queue to verify this is not a duplicate feature request.
* [X] I've pasted the output of `kargo version`, if applicable.
* [X] I've pasted logs, if applicable.

# Proposed Feature

Improve the flexibility of Google Artifact Registry credentials service account impersonation.

# Motivation

Currently, the hardcoded identity format:
`kargo-project-<kargo project name>@<gcp project name>.iam.gserviceaccount.com` leads to a suboptimal administrator experience.
In scenarios where tens or hundreds of projects are required, administrators must create numerous Service Accounts and IAM permissions, even when all of them ultimately point to the same Artifact Repository. This results in unnecessary complexity and overhead.

# Suggested Implementation

Instead of enforcing a strict identity format, allow experienced users to manage their IAM configurations more flexibly. One possible approach is to enable mapping a Service Account to an Artifact Registry instance rather than binding it directly to a Kargo Project. Other way is to add some field/annotation to the Repository secret with the name of service account which should be impersonated.

Discussion:
@krancour: For AWS/ECR, we have long had a feature where if the project-specific ambient credentials could not be assumed or were assumed and have insufficient permissions, Kargo will fall back on using the ambient credentials of the controller itself.

We don't have this for Google/GAR yet, but I've been meaning to get it caught up to AWS/ECR in this regard.

Without necessarily closing the door to other approaches in the future, would you find this relatively simple improvement helpful?@kacpercesarz98: > For AWS/ECR, we have long had a feature where if the project-specific ambient credentials could not be assumed or were assumed and have insufficient permissions, Kargo will fall back on using the ambient credentials of the controller itself.
> 
> We don't have this for Google/GAR yet, but I've been meaning to get it caught up to AWS/ECR in this regard.
> 
> Without necessarily closing the door to other approaches in the future, would you find this relatively simple improvement helpful?


Yes it will be definitely helpful and more convenient. If it will not close doors for future improvements it is good enough for now.@kacpercesarz98: Another issue which I found is lenght limit of GCP service account name:
`The account ID "kargo-project-*" does not have a length between 6 and 30., badRequest`
It force me to create project with not clear, shortener name.
@remi-gelinas: @krancour We're running into the same service account fatigue - we have a distinct project per logical application (for more granular access control) and it does get extremely cumbersome when the permissions required are the exact same for every SA. Being able to fall back to the controller's service account would be a VERY helpful quick win :D @qasim9872: Hi all - I have been following this issue for the last 2-3 weeks as this feature is blocking us from using it in our infrastructure. Just checking in as there has been no progress on this? Happy to chip in if it will help move things along? @krancour: One of two things needs to happen. @kacpercesarz98 needs to address the unresolved feedback on PR #3597, or @hiddeco or myself need to find the time to get it over the line ourselves, which isn't a current priority.

Solution patch:
diff --git a/Dockerfile b/Dockerfile
index 1f67d139..b9f5458a 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -61,7 +61,7 @@ WORKDIR /kargo/bin
 ####################################################################################################
 # `tools` stage allows us to take the leverage of the parallel build.
 # For example, this stage can be cached and re-used when we have to rebuild code base.
-FROM curlimages/curl:8.12.1 AS tools
+FROM curlimages/curl:8.13.0 AS tools
 
 ARG TARGETOS
 ARG TARGETARCH
diff --git a/api/v1alpha1/expression_types.go b/api/v1alpha1/expression_types.go
new file mode 100644
index 00000000..02e739a9
--- /dev/null
+++ b/api/v1alpha1/expression_types.go
@@ -0,0 +1,19 @@
+package v1alpha1
+
+// ExpressionVariable describes a single variable that may be referenced by
+// expressions in the context of a ClusterPromotionTask, PromotionTask,
+// Promotion, AnalysisRun arguments, or other objects that support expressions.
+//
+// It is used to pass information to the expression evaluation engine, and to
+// allow for dynamic evaluation of expressions based on the variable values.
+type ExpressionVariable struct {
+	// Name is the name of the variable.
+	//
+	// +kubebuilder:validation:MinLength=1
+	// +kubebuilder:validation:Pattern=^[a-zA-Z_]\w*$
+	Name string `json:"name" protobuf:"bytes,1,opt,name=name"`
+	// Value is the value of the variable. It is allowed to utilize expressions
+	// in the value.
+	// See https://docs.kargo.io/user-guide/reference-docs/expressions for details.
+	Value string `json:"value,omitempty" protobuf:"bytes,2,opt,name=value"`
+}
diff --git a/api/v1alpha1/generated.pb.go b/api/v1alpha1/generated.pb.go
index 318a4af1..4a85c659 100644
--- a/api/v1alpha1/generated.pb.go
+++ b/api/v1alpha1/generated.pb.go
@@ -506,10 +506,38 @@ func (m *DiscoveredImageReference) XXX_DiscardUnknown() {
 
 var xxx_messageInfo_DiscoveredImageReference proto.InternalMessageInfo
 
+func (m *ExpressionVariable) Reset()      { *m = ExpressionVariable{} }
+func (*ExpressionVariable) ProtoMessage() {}
+func (*ExpressionVariable) Descriptor() ([]byte, []int) {
+	return fileDescriptor_e26b7f7bbc391025, []int{17}
+}
+func (m *ExpressionVariable) XXX_Unmarshal(b []byte) error {
+	return m.Unmarshal(b)
+}
+func (m *ExpressionVariable) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
+	b = b[:cap(b)]
+	n, err := m.MarshalToSizedBuffer(b)
+	if err != nil {
+		return nil, err
+	}
+	return b[:n], nil
+}
+func (m *ExpressionVariable) XXX_Merge(src proto.Message) {
+	xxx_messageInfo_ExpressionVariable.Merge(m, src)
+}
+func (m *ExpressionVariable) XXX_Size() int {
+	return m.Size()
+}
+func (m *ExpressionVariable) XXX_DiscardUnknown() {
+	xxx_messageInfo_ExpressionVariable.DiscardUnknown(m)
+}
+
+var xxx_messageInfo_ExpressionVariable proto.InternalMessageInfo
+
 func (m *Freight) Reset()      { *m = Freight{} }
 func (*Freight) ProtoMessage() {}
 func (*Freight) Descriptor() ([]byte, []int) {
-	return fileDescriptor_e26b7f7bbc391025, []int{17}
+	return fileDescriptor_e26b7f7bbc391025, []int{18}
 }
 func (m *Freight) XXX_Unmarshal(b []byte) error {
 	return m.Unmarshal(b)
@@ -537,7 +565,7 @@ var xxx_messageInfo_Freight proto.InternalMessageInfo
 func (m *FreightCollection) Reset()      { *m = FreightCollection{} }
 func (*FreightCollection) ProtoMessage() {}
 func (*FreightCollection) Descriptor() ([]byte, []int) {
-	return fileDescriptor_e26b7f7bbc391025, []int{18}
+	return fileDescriptor_e26b7f7bbc391025, []int{19}
 }
 func (m *FreightCollection) XXX_Unmarshal(b []byte) error {
 	return m.Unmarshal(b)
@@ -565,7 +593,7 @@ var xxx_messageInfo_FreightCollection proto.InternalMessageInfo
 func (m *FreightList) Reset()      { *m = FreightList{} }
 func (*FreightList) ProtoMessage() {}
 func (*FreightList) Descriptor() ([]byte, []int) {
-	return fileDescriptor_e26b7f7bbc391025, []int{19}
+	return fileDescriptor_e26b7f7bbc391025, []int{20}
 }
 func (m *FreightList) XXX_Unmarshal(b []byte) error {
 	return m.Unmarshal(b)
@@ -593,7 +621,7 @@ var xxx_messageInfo_FreightList proto.InternalMessageInfo
 func (m *FreightOrigin) Reset()      { *m = FreightOrigin{} }
 func (*FreightOrigin) ProtoMessage() {}
 func (*FreightOrigin) Descriptor() ([]byte, []int) {
-	return fileDescriptor_e26b7f7bbc391025, []int{20}
+	return fileDescriptor_e26b7f7bbc391025, []int{21}
 }
 func (m *FreightOrigin) XXX_Unmarshal(b []byte) error {
 	return m.Unmarshal(b)
@@ -621,7 +649,7 @@ var xxx_messageInfo_FreightOrigin proto.InternalMessageInfo
 func (m *FreightReference) Reset()      { *m = FreightReference{} }
 func (*FreightReference) ProtoMessage() {}
 func (*FreightReference) Descriptor() ([]byte, []int) {
-	return fileDescriptor_e26b7f7bbc391025, []int{21}
+	return fileDescriptor_e26b7f7bbc391025, []int{22}
 }
 func (m *FreightReference) XXX_Unmarshal(b []byte) error {
 	return m.Unmarshal(b)
@@ -649,7 +677,7 @@ var xxx_messageInfo_FreightReference proto.InternalMessageInfo
 func (m *FreightRequest) Reset()      { *m = FreightRequest{} }
 func (*FreightRequest) ProtoMessage() {}
 func (*FreightRequest) Descriptor() ([]byte, []int) {
-	return fileDescriptor_e26b7f7bbc391025, []int{22}
+	return fileDescriptor_e26b7f7bbc391025, []int{23}
 }
 func (m *FreightRequest) XXX_Unmarshal(b []byte) error {
 	return m.Unmarshal(b)
@@ -677,7 +705,7 @@ var xxx_messageInfo_FreightRequest proto.InternalMessageInfo
 func (m *FreightSources) Reset()      { *m = FreightSources{} }
 func (*FreightSources) ProtoMessage() {}
 func (*FreightSources) Descriptor() ([]byte, []int) {
-	return fileDescriptor_e26b7f7bbc391025, []int{23}
+	return fileDescriptor_e26b7f7bbc391025, []int{24}
 }
 func (m *FreightSources) XXX_Unmarshal(b []byte) error {
 	return m.Unmarshal(b)
@@ -705,7 +733,7 @@ var xxx_messageInfo_FreightSources proto.InternalMessageInfo
 func (m *FreightStatus) Reset()      { *m = FreightStatus{} }
 func (*FreightStatus) ProtoMessage() {}
 func (*FreightStatus) Descriptor() ([]byte, []int) {
-	return fileDescriptor_e26b7f7bbc391025, []int{24}
+	return fileDescriptor_e26b7f7bbc391025, []int{25}
 }
 func (m *FreightStatus) XXX_Unmarshal(b []byte) error {
 	return m.Unmarshal(b)
@@ -733,7 +761,7 @@ var xxx_messageInfo_FreightStatus proto.InternalMessageInfo
 func (m *GitCommit) Reset()      { *m = GitCommit{} }
 func (*GitCommit) ProtoMessage() {}
 func (*GitCommit) Descriptor() ([]byte, []int) {
-	return fileDescriptor_e26b7f7bbc391025, []int{25}
+	return fileDescriptor_e26b7f7bbc391025, []int{26}
 }
 func (m *GitCommit) XXX_Unmarshal(b []byte) error {
 	return m.Unmarshal(b)
@@ -761,7 +789,7 @@ var xxx_messageInfo_GitCommit proto.InternalMessageInfo
 func (m *GitDiscoveryResult) Reset()      { *m = GitDiscoveryResult{} }
 func (*GitDiscoveryResult) ProtoMessage() {}
 func (*GitDiscoveryResult) Descriptor() ([]byte, []int) {
-	return fileDescriptor_e26b7f7bbc391025, []int{26}
+	return fileDescriptor_e26b7f7bbc391025, []int{27}
 }
 func (m *GitDiscoveryResult) XXX_Unmarshal(b []byte) error {
 	return m.Unmarshal(b)
@@ -789,7 +817,7 @@ var xxx_messageInfo_GitDiscoveryResult proto.InternalMessageInfo
 func (m *GitSubscription) Reset()      { *m = GitSubscription{} }
 func (*GitSubscription) ProtoMessage() {}
 func (*GitSubscription) Descriptor() ([]byte, []int) {
-	return fileDescriptor_e26b7f7bbc391025, []int{27}
+	return fileDescriptor_e26b7f7bbc391025, []int{28}
 }
 func (m *GitSubscription) XXX_Unmarshal(b []byte) error {
 	return m.Unmarshal(b)
@@ -817,7 +845,7 @@ var xxx_messageInfo_GitSubscription proto.InternalMessageInfo
 func (m *Health) Reset()      { *m = Health{} }
 func (*Health) ProtoMessage() {}
 func (*Health) Descriptor() ([]byte, []int) {
-	return fileDescriptor_e26b7f7bbc391025, []int{28}
+	return fileDescriptor_e26b7f7bbc391025, []int{29}
 }
 func (m *Health) XXX_Unmarshal(b []byte) error {
 	return m.Unmarshal(b)
@@ -845,7 +873,7 @@ var xxx_messageInfo_Health proto.InternalMessageInfo
 func (m *HealthCheckStep) Reset()      { *m = HealthCheckStep{} }
 func (*HealthCheckStep) ProtoMessage() {}
 func (*HealthCheckStep) Descriptor() ([]byte, []int) {
-	return fileDescriptor_e26b7f7bbc391025, []int{29}
+	return fileDescriptor_e26b7f7bbc391025, []int{30}
 }
 func (m *HealthCheckStep) XXX_Unmarshal(b []byte) error {
 	return m.Unmarshal(b)
@@ -873,7 +901,7 @@ var xxx_messageInfo_HealthCheckStep proto.InternalMessageInfo
 func (m *Image) Reset()      { *m = Image{} }
 func (*Image) ProtoMessage() {}
 func (*Image) Descriptor() ([]byte, []int) {
-	return fileDescriptor_e26b7f7bbc391025, []int{30}
+	return fileDescriptor_e26b7f7bbc391025, []int{31}
 }
 func (m *Image) XXX_Unmarshal(b []byte) error {
 	return m.Unmarshal(b)
@@ -901,7 +929,7 @@ var xxx_messageInfo_Image proto.InternalMessageInfo
 func (m *ImageDiscoveryResult) Reset()      { *m = ImageDiscoveryResult{} }
 func (*ImageDiscoveryResult) ProtoMessage() {}
 func (*ImageDiscoveryResult) Descriptor() ([]byte, []int) {
-	return fileDescriptor_e26b7f7bbc391025, []int{31}
+	return fileDescriptor_e26b7f7bbc391025, []int{32}
 }
 func (m *ImageDiscoveryResult) XXX_Unmarshal(b []byte) error {
 	return m.Unmarshal(b)
@@ -929,7 +957,7 @@ var xxx_messageInfo_ImageDiscoveryResult proto.InternalMessageInfo
 func (m *ImageSubscription) Reset()      { *m = ImageSubscription{} }
 func (*ImageSubscription) ProtoMessage() {}
 func (*ImageSubscription) Descriptor() ([]byte, []int) {
-	return fileDescriptor_e26b7f7bbc391025, []int{32}
+	return fileDescriptor_e26b7f7bbc391025, []int{33}
 }
 func (m *ImageSubscription) XXX_Unmarshal(b []byte) error {
 	return m.Unmarshal(b)
@@ -957,7 +985,7 @@ var xxx_messageInfo_ImageSubscription proto.InternalMessageInfo
 func (m *Project) Reset()      { *m = Project{} }
 func (*Project) ProtoMessage() {}
 func (*Project) Descriptor() ([]byte, []int) {
-	return fileDescriptor_e26b7f7bbc391025, []int{33}
+	return fileDescriptor_e26b7f7bbc391025, []int{34}
 }
 func (m *Project) XXX_Unmarshal(b []byte) error {
 	return m.Unmarshal(b)
@@ -985,7 +1013,7 @@ var xxx_messageInfo_Project proto.InternalMessageInfo
 func (m *ProjectList) Reset()      { *m = ProjectList{} }
 func (*ProjectList) ProtoMessage() {}
 func (*ProjectList) Descriptor() ([]byte, []int) {
-	return fileDescriptor_e26b7f7bbc391025, []int{34}
+	return fileDescriptor_e26b7f7bbc391025, []int{35}
 }
 func (m *ProjectList) XXX_Unmarshal(b []byte) error {
 	return m.Unmarshal(b)
@@ -1013,7 +1041,7 @@ var xxx_messageInfo_ProjectList proto.InternalMessageInfo
 func (m *ProjectSpec) Reset()      { *m = ProjectSpec{} }
 func (*ProjectSpec) ProtoMessage() {}
 func (*ProjectSpec) Descriptor() ([]byte, []int) {
-	return fileDescriptor_e26b7f7bbc391025, []int{35}
+	return fileDescriptor_e26b7f7bbc391025, []int{36}
 }
 func (m *ProjectSpec) XXX_Unmarshal(b []byte) error {
 	return m.Unmarshal(b)
@@ -1041,7 +1069,7 @@ var xxx_messageInfo_ProjectSpec proto.InternalMessageInfo
 func (m *ProjectStatus) Reset()      { *m = ProjectStatus{} }
 func (*ProjectStatus) ProtoMessage() {}
 func (*ProjectStatus) Descriptor() ([]byte, []int) {
-	return fileDescriptor_e26b7f7bbc391025, []int{36}
+	return fileDescriptor_e26b7f7bbc391025, []int{37}
 }
 func (m *ProjectStatus) XXX_Unmarshal(b []byte) error {
 	return m.Unmarshal(b)
@@ -1069,7 +1097,7 @@ var xxx_messageInfo_ProjectStatus proto.InternalMessageInfo
 func (m *Promotion) Reset()      { *m = Promotion{} }
 func (*Promotion) ProtoMessage() {}
 func (*Promotion) Descriptor() ([]byte, []int) {
-	return fileDescriptor_e26b7f7bbc391025, []int{37}
+	return fileDescriptor_e26b7f7bbc391025, []int{38}
 }
 func (m *Promotion) XXX_Unmarshal(b []byte) error {
 	return m.Unmarshal(b)
@@ -1097,7 +1125,7 @@ var xxx_messageInfo_Promotion proto.InternalMessageInfo
 func (m *PromotionList) Reset()      { *m = PromotionList{} }
 func (*PromotionList) ProtoMessage() {}
 func (*PromotionList) Descriptor() ([]byte, []int) {
-	return fileDescriptor_e26b7f7bbc391025, []int{38}
+	return fileDescriptor_e26b7f7bbc391025, []int{39}
 }
 func (m *PromotionList) XXX_Unmarshal(b []byte) error {
 	return m.Unmarshal(b)
@@ -1125,7 +1153,7 @@ var xxx_messageInfo_PromotionList proto.InternalMessageInfo
 func (m *PromotionPolicy) Reset()      { *m = PromotionPolicy{} }
 func (*PromotionPolicy) ProtoMessage() {}
 func (*PromotionPolicy) Descriptor() ([]byte, []int) {
-	return fileDescriptor_e26b7f7bbc391025, []int{39}
+	return fileDescriptor_e26b7f7bbc391025, []int{40}
 }
 func (m *PromotionPolicy) XXX_Unmarshal(b []byte) error {
 	return m.Unmarshal(b)
@@ -1153,7 +1181,7 @@ var xxx_messageInfo_PromotionPolicy proto.InternalMessageInfo
 func (m *PromotionReference) Reset()      { *m = PromotionReference{} }
 func (*PromotionReference) ProtoMessage() {}
 func (*PromotionReference) Descriptor() ([]byte, []int) {
-	return fileDescriptor_e26b7f7bbc391025, []int{40}
+	return fileDescriptor_e26b7f7bbc391025, []int{41}
 }
 func (m *PromotionReference) XXX_Unmarshal(b []byte) error {
 	return m.Unmarshal(b)
@@ -1181,7 +1209,7 @@ var xxx_messageInfo_PromotionReference proto.InternalMessageInfo
 func (m *PromotionSpec) Reset()      { *m = PromotionSpec{} }
 func (*PromotionSpec) ProtoMessage() {}
 func (*PromotionSpec) Descriptor() ([]byte, []int) {
-	return fileDescriptor_e26b7f7bbc391025, []int{41}
+	return fileDescriptor_e26b7f7bbc391025, []int{42}
 }
 func (m *PromotionSpec) XXX_Unmarshal(b []byte) error {
 	return m.Unmarshal(b)
@@ -1209,7 +1237,7 @@ var xxx_messageInfo_PromotionSpec proto.InternalMessageInfo
 func (m *PromotionStatus) Reset()      { *m = PromotionStatus{} }
 func (*PromotionStatus) ProtoMessage() {}
 func (*PromotionStatus) Descriptor() ([]byte, []int) {
-	return fileDescriptor_e26b7f7bbc391025, []int{42}
+	return fileDescriptor_e26b7f7bbc391025, []int{43}
 }
 func (m *PromotionStatus) XXX_Unmarshal(b []byte) error {
 	return m.Unmarshal(b)
@@ -1237,7 +1265,7 @@ var xxx_messageInfo_PromotionStatus proto.InternalMessageInfo
 func (m *PromotionStep) Reset()      { *m = PromotionStep{} }
 func (*PromotionStep) ProtoMessage() {}
 func (*PromotionStep) Descriptor() ([]byte, []int) {
-	return fileDescriptor_e26b7f7bbc391025, []int{43}
+	return fileDescriptor_e26b7f7bbc391025, []int{44}
 }
 func (m *PromotionStep) XXX_Unmarshal(b []byte) error {
 	return m.Unmarshal(b)
@@ -1265,7 +1293,7 @@ var xxx_messageInfo_PromotionStep proto.InternalMessageInfo
 func (m *PromotionStepRetry) Reset()      { *m = PromotionStepRetry{} }
 func (*PromotionStepRetry) ProtoMessage() {}
 func (*PromotionStepRetry) Descriptor() ([]byte, []int) {
-	return fileDescriptor_e26b7f7bbc391025, []int{44}
+	return fileDescriptor_e26b7f7bbc391025, []int{45}
 }
 func (m *PromotionStepRetry) XXX_Unmarshal(b []byte) error {
 	return m.Unmarshal(b)
@@ -1293,7 +1321,7 @@ var xxx_messageInfo_PromotionStepRetry proto.InternalMessageInfo
 func (m *PromotionTask) Reset()      { *m = PromotionTask{} }
 func (*PromotionTask) ProtoMessage() {}
 func (*PromotionTask) Descriptor() ([]byte, []int) {
-	return fileDescriptor_e26b7f7bbc391025, []int{45}
+	return fileDescriptor_e26b7f7bbc391025, []int{46}
 }
 func (m *PromotionTask) XXX_Unmarshal(b []byte) error {
 	return m.Unmarshal(b)
@@ -1321,7 +1349,7 @@ var xxx_messageInfo_PromotionTask proto.InternalMessageInfo
 func (m *PromotionTaskList) Reset()      { *m = PromotionTaskList{} }
 func (*PromotionTaskList) ProtoMessage() {}
 func (*PromotionTaskList) Descriptor() ([]byte, []int) {
-	return fileDescriptor_e26b7f7bbc391025, []int{46}
+	return fileDescriptor_e26b7f7bbc391025, []int{47}
 }
 func (m *PromotionTaskList) XXX_Unmarshal(b []byte) error {
 	return m.Unmarshal(b)
@@ -1349,7 +1377,7 @@ var xxx_messageInfo_PromotionTaskList proto.InternalMessageInfo
 func (m *PromotionTaskReference) Reset()      { *m = PromotionTaskReference{} }
 func (*PromotionTaskReference) ProtoMessage() {}
 func (*PromotionTaskReference) Descriptor() ([]byte, []int) {
-	return fileDescriptor_e26b7f7bbc391025, []int{47}
+	return fileDescriptor_e26b7f7bbc391025, []int{48}
 }
 func (m *PromotionTaskReference) XXX_Unmarshal(b []byte) error {
 	return m.Unmarshal(b)
@@ -1377,7 +1405,7 @@ var xxx_messageInfo_PromotionTaskReference proto.InternalMessageInfo
 func (m *PromotionTaskSpec) Reset()      { *m = PromotionTaskSpec{} }
 func (*PromotionTaskSpec) ProtoMessage() {}
 func (*PromotionTaskSpec) Descriptor() ([]byte, []int) {
-	return fileDescriptor_e26b7f7bbc391025, []int{48}
+	return fileDescriptor_e26b7f7bbc391025, []int{49}
 }
 func (m *PromotionTaskSpec) XXX_Unmarshal(b []byte) error {
 	return m.Unmarshal(b)
@@ -1405,7 +1433,7 @@ var xxx_messageInfo_PromotionTaskSpec proto.InternalMessageInfo
 func (m *PromotionTemplate) Reset()      { *m = PromotionTemplate{} }
 func (*PromotionTemplate) ProtoMessage() {}
 func (*PromotionTemplate) Descriptor() ([]byte, []int) {
-	return fileDescriptor_e26b7f7bbc391025, []int{49}
+	return fileDescriptor_e26b7f7bbc391025, []int{50}
 }
 func (m *PromotionTemplate) XXX_Unmarshal(b []byte) error {
 	return m.Unmarshal(b)
@@ -1433,7 +1461,7 @@ var xxx_messageInfo_PromotionTemplate proto.InternalMessageInfo
 func (m *PromotionTemplateSpec) Reset()      { *m = PromotionTemplateSpec{} }
 func (*PromotionTemplateSpec) ProtoMessage() {}
 func (*PromotionTemplateSpec) Descriptor() ([]byte, []int) {
-	return fileDescriptor_e26b7f7bbc391025, []int{50}
+	return fileDescriptor_e26b7f7bbc391025, []int{51}
 }
 func (m *PromotionTemplateSpec) XXX_Unmarshal(b []byte) error {
 	return m.Unmarshal(b)
@@ -1458,34 +1486,6 @@ func (m *PromotionTemplateSpec) XXX_DiscardUnknown() {
 
 var xxx_messageInfo_PromotionTemplateSpec proto.InternalMessageInfo
 
-func (m *PromotionVariable) Reset()      { *m = PromotionVariable{} }
-func (*PromotionVariable) ProtoMessage() {}
-func (*PromotionVariable) Descriptor() ([]byte, []int) {
-	return fileDescriptor_e26b7f7bbc391025, []int{51}
-}
-func (m *PromotionVariable) XXX_Unmarshal(b []byte) error {
-	return m.Unmarshal(b)
-}
-func (m *PromotionVariable) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
-	b = b[:cap(b)]
-	n, err := m.MarshalToSizedBuffer(b)
-	if err != nil {
-		return nil, err
-	}
-	return b[:n], nil
-}
-func (m *PromotionVariable) XXX_Merge(src proto.Message) {
-	xxx_messageInfo_PromotionVariable.Merge(m, src)
-}
-func (m *PromotionVariable) XXX_Size() int {
-	return m.Size()
-}
-func (m *PromotionVariable) XXX_DiscardUnknown() {
-	xxx_messageInfo_PromotionVariable.DiscardUnknown(m)
-}
-
-var xxx_messageInfo_PromotionVariable proto.InternalMessageInfo
-
 func (m *RepoSubscription) Reset()      { *m = RepoSubscription{} }
 func (*RepoSubscription) ProtoMessage() {}
 func (*RepoSubscription) Descriptor() ([]byte, []int) {
@@ -1871,6 +1871,7 @@ func init() {
 	proto.RegisterType((*DiscoveredCommit)(nil), "github.com.akuity.kargo.api.v1alpha1.DiscoveredCommit")
 	proto.RegisterType((*DiscoveredImageReference)(nil), "github.com.akuity.kargo.api.v1alpha1.DiscoveredImageReference")
 	proto.RegisterMapType((map[string]string)(nil), "github.com.akuity.kargo.api.v1alpha1.DiscoveredImageReference.AnnotationsEntry")
+	proto.RegisterType((*ExpressionVariable)(nil), "github.com.akuity.kargo.api.v1alpha1.ExpressionVariable")
 	proto.RegisterType((*Freight)(nil), "github.com.akuity.kargo.api.v1alpha1.Freight")
 	proto.RegisterType((*FreightCollection)(nil), "github.com.akuity.kargo.api.v1alpha1.FreightCollection")
 	proto.RegisterMapType((map[string]FreightReference)(nil), "github.com.akuity.kargo.api.v1alpha1.FreightCollection.ItemsEntry")
@@ -1910,7 +1911,6 @@ func init() {
 	proto.RegisterType((*PromotionTaskSpec)(nil), "github.com.akuity.kargo.api.v1alpha1.PromotionTaskSpec")
 	proto.RegisterType((*PromotionTemplate)(nil), "github.com.akuity.kargo.api.v1alpha1.PromotionTemplate")
 	proto.RegisterType((*PromotionTemplateSpec)(nil), "github.com.akuity.kargo.api.v1alpha1.PromotionTemplateSpec")
-	proto.RegisterType((*PromotionVariable)(nil), "github.com.akuity.kargo.api.v1alpha1.PromotionVariable")
 	proto.RegisterType((*RepoSubscription)(nil), "github.com.akuity.kargo.api.v1alpha1.RepoSubscription")
 	proto.RegisterType((*Stage)(nil), "github.com.akuity.kargo.api.v1alpha1.Stage")
 	proto.RegisterType((*StageList)(nil), "github.com.akuity.kargo.api.v1alpha1.StageList")
@@ -1931,267 +1931,268 @@ func init() {
 }
 
 var fileDescriptor_e26b7f7bbc391025 = []byte{
-	// 4156 bytes of a gzipped FileDescriptorProto
-	0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xdc, 0x5c, 0xdd, 0x6f, 0x23, 0x47,
-	0x72, 0xdf, 0x21, 0x45, 0x4a, 0x2c, 0x4a, 0xbb, 0x52, 0xaf, 0xd6, 0xd6, 0xc9, 0xf1, 0x6a, 0x33,
-	0x36, 0x0c, 0x3b, 0xb6, 0xa9, 0xac, 0xec, 0xb5, 0xd7, 0x6b, 0x63, 0x03, 0x91, 0xda, 0x0f, 0xed,
-	0xe9, 0xbc, 0x4a, 0x53, 0x5e, 0x9f, 0xd7, 0x36, 0x36, 0x2d, 0xb2, 0x45, 0xce, 0x89, 0x9c, 0xa1,
-	0xa7, 0x9b, 0x3a, 0x2b, 0x09, 0x92, 0xcb, 0x27, 0x0e, 0x09, 0x12, 0xdc, 0x83, 0x01, 0x5f, 0x80,
-	0x04, 0x08, 0x92, 0xc7, 0x03, 0xf2, 0x9a, 0x87, 0x3c, 0xf8, 0x21, 0x2f, 0x46, 0x62, 0x04, 0x46,
-	0x12, 0x20, 0x0e, 0x70, 0xd0, 0xc5, 0x3a, 0x20, 0xff, 0x41, 0x5e, 0x16, 0x08, 0x10, 0xf4, 0xc7,
-	0xcc, 0xf4, 0x0c, 0x87, 0x2b, 0x0e, 0x25, 0x2d, 0x9c, 0x7b, 0x93, 0xba, 0xba, 0x7f, 0xd5, 0x5d,
-	0xdd, 0x55, 0x5d, 0x55, 0x5d, 0x43, 0x78, 0xb5, 0xe5, 0xf0, 0x76, 0x7f, 0xbb, 0xd2, 0xf0, 0xba,
-	0xcb, 0x64, 0xb7, 0xef, 0xf0, 0xfd, 0xe5, 0x5d, 0xe2, 0xb7, 0xbc, 0x65, 0xd2, 0x73, 0x96, 0xf7,
-	0x2e, 0x93, 0x4e, 0xaf, 0x4d, 0x2e, 0x2f, 0xb7, 0xa8, 0x4b, 0x7d, 0xc2, 0x69, 0xb3, 0xd2, 0xf3,
-	0x3d, 0xee, 0xa1, 0x67, 0xa3, 0x51, 0x15, 0x35, 0xaa, 0x22, 0x47, 0x55, 0x48, 0xcf, 0xa9, 0x04,
-	0xa3, 0x16, 0x5f, 0x36, 0xb0, 0x5b, 0x5e, 0xcb, 0x5b, 0x96, 0x83, 0xb7, 0xfb, 0x3b, 0xf2, 0x3f,
-	0xf9, 0x8f, 0xfc, 0x4b, 0x81, 0x2e, 0xde, 0xde, 0xbd, 0xca, 0x2a, 0x8e, 0xe4, 0x4c, 0x3f, 0xe6,
-	0xd4, 0x65, 0x8e, 0xe7, 0xb2, 0x97, 0x49, 0xcf, 0x61, 0xd4, 0xdf, 0xa3, 0xfe, 0x72, 0x6f, 0xb7,
-	0x25, 0x68, 0x2c, 0xde, 0x61, 0x79, 0x6f, 0x60, 0x7a, 0x8b, 0xaf, 0x46, 0x48, 0x5d, 0xd2, 0x68,
-	0x3b, 0x2e, 0xf5, 0xf7, 0xa3, 0xe1, 0x5d, 0xca, 0x49, 0xda, 0xa8, 0xe5, 0x61, 0xa3, 0xfc, 0xbe,
-	0xcb, 0x9d, 0x2e, 0x1d, 0x18, 0xf0, 0xda, 0x51, 0x03, 0x58, 0xa3, 0x4d, 0xbb, 0x24, 0x39, 0xce,
-	0xfe, 0x00, 0xce, 0xaf, 0xba, 0xa4, 0xb3, 0xcf, 0x1c, 0x86, 0xfb, 0xee, 0xaa, 0xdf, 0xea, 0x77,
-	0xa9, 0xcb, 0xd1, 0x25, 0x98, 0x70, 0x49, 0x97, 0x2e, 0x58, 0x97, 0xac, 0xe7, 0x4b, 0xd5, 0xe9,
-	0xcf, 0x0f, 0x96, 0xce, 0x1c, 0x1e, 0x2c, 0x4d, 0xbc, 0x4d, 0xba, 0x14, 0x4b, 0x0a, 0x7a, 0x06,
-	0x0a, 0x7b, 0xa4, 0xd3, 0xa7, 0x0b, 0x39, 0xd9, 0x65, 0x46, 0x77, 0x29, 0xdc, 0x13, 0x8d, 0x58,
-	0xd1, 0xec, 0x3f, 0xc8, 0xc7, 0xe0, 0xbf, 0x43, 0x39, 0x69, 0x12, 0x4e, 0x50, 0x17, 0x8a, 0x1d,
-	0xb2, 0x4d, 0x3b, 0x6c, 0xc1, 0xba, 0x94, 0x7f, 0xbe, 0xbc, 0x72, 0xa3, 0x32, 0xca, 0x26, 0x56,
-	0x52, 0xa0, 0x2a, 0x1b, 0x12, 0xe7, 0x86, 0xcb, 0xfd, 0xfd, 0xea, 0x59, 0x3d, 0x89, 0xa2, 0x6a,
-	0xc4, 0x9a, 0x09, 0xfa, 0x3d, 0x0b, 0xca, 0xc4, 0x75, 0x3d, 0x4e, 0xb8, 0xd8, 0xa6, 0x85, 0x9c,
-	0x64, 0x7a, 0x67, 0x7c, 0xa6, 0xab, 0x11, 0x98, 0xe2, 0x7c, 0x5e, 0x73, 0x2e, 0x1b, 0x14, 0x6c,
-	0xf2, 0x5c, 0x7c, 0x03, 0xca, 0xc6, 0x54, 0xd1, 0x2c, 0xe4, 0x77, 0xe9, 0xbe, 0x92, 0x2f, 0x16,
-	0x7f, 0xa2, 0xf9, 0x98, 0x40, 0xb5, 0x04, 0xaf, 0xe5, 0xae, 0x5a, 0x8b, 0xd7, 0x61, 0x36, 0xc9,
-	0x30, 0xcb, 0x78, 0xfb, 0xcf, 0x2d, 0x98, 0x37, 0x56, 0x81, 0xe9, 0x0e, 0xf5, 0xa9, 0xdb, 0xa0,
-	0x68, 0x19, 0x4a, 0x62, 0x2f, 0x59, 0x8f, 0x34, 0x82, 0xad, 0x9e, 0xd3, 0x0b, 0x29, 0xbd, 0x1d,
-	0x10, 0x70, 0xd4, 0x27, 0x3c, 0x16, 0xb9, 0x47, 0x1d, 0x8b, 0x5e, 0x9b, 0x30, 0xba, 0x90, 0x8f,
-	0x1f, 0x8b, 0x4d, 0xd1, 0x88, 0x15, 0xcd, 0x7e, 0x00, 0xdf, 0x0a, 0xe6, 0xb3, 0x45, 0xbb, 0xbd,
-	0x0e, 0xe1, 0x34, 0x9a, 0xd4, 0xd1, 0x47, 0xef, 0x12, 0x4c, 0xec, 0x3a, 0x6e, 0x33, 0x39, 0x8b,
-	0x6f, 0x3b, 0x6e, 0x13, 0x4b, 0x8a, 0xbd, 0x0b, 0x33, 0xab, 0xbd, 0x9e, 0xef, 0xed, 0xd1, 0x66,
-	0x9d, 0x93, 0x16, 0x45, 0xf7, 0x01, 0x88, 0x6e, 0x58, 0xe5, 0x12, 0xba, 0xbc, 0xf2, 0x2b, 0x15,
-	0xa5, 0x33, 0x15, 0x53, 0x67, 0x2a, 0xbd, 0xdd, 0x96, 0x68, 0x60, 0x15, 0xa1, 0x9a, 0x95, 0xbd,
-	0xcb, 0x95, 0x2d, 0xa7, 0x4b, 0xab, 0x67, 0x0f, 0x0f, 0x96, 0x60, 0x35, 0x44, 0xc0, 0x06, 0x9a,
-	0xfd, 0xfb, 0x16, 0x5c, 0x58, 0xf5, 0x5b, 0x5e, 0x6d, 0x6d, 0xb5, 0xd7, 0xbb, 0x4d, 0x49, 0x87,
-	0xb7, 0xeb, 0x9c, 0xf0, 0x3e, 0x43, 0xd7, 0xa1, 0xc8, 0xe4, 0x5f, 0x7a, 0x31, 0xcf, 0x05, 0xe7,
-	0x53, 0xd1, 0x1f, 0x1e, 0x2c, 0xcd, 0xa7, 0x0c, 0xa4, 0x58, 0x8f, 0x42, 0x2f, 0xc0, 0x64, 0x97,
-	0x32, 0x46, 0x5a, 0x81, 0xc4, 0xcf, 0x69, 0x80, 0xc9, 0xef, 0xa8, 0x66, 0x1c, 0xd0, 0xed, 0x7f,
-	0xca, 0xc1, 0xb9, 0x10, 0x4b, 0xb3, 0x3f, 0x85, 0xed, 0xed, 0xc3, 0x74, 0xdb, 0x58, 0xa1, 0xdc,
-	0xe5, 0xf2, 0xca, 0x9b, 0x23, 0x6a, 0x52, 0x9a, 0x90, 0xaa, 0xf3, 0x9a, 0xcd, 0xb4, 0xd9, 0x8a,
-	0x63, 0x6c, 0x50, 0x17, 0x80, 0xed, 0xbb, 0x0d, 0xcd, 0x74, 0x42, 0x32, 0x7d, 0x23, 0x23, 0xd3,
-	0x7a, 0x08, 0x50, 0x45, 0x9a, 0x25, 0x44, 0x6d, 0xd8, 0x60, 0x60, 0xff, 0x9d, 0x05, 0xe7, 0x53,
-	0xc6, 0xa1, 0xb7, 0x12, 0xfb, 0xf9, 0xec, 0xc0, 0x7e, 0xa2, 0x81, 0x61, 0xd1, 0x6e, 0xbe, 0x04,
-	0x53, 0x3e, 0xdd, 0x73, 0xc4, 0x4d, 0xa1, 0x25, 0x3c, 0xab, 0xc7, 0x4f, 0x61, 0xdd, 0x8e, 0xc3,
-	0x1e, 0xe8, 0x45, 0x28, 0x05, 0x7f, 0x0b, 0x31, 0xe7, 0x85, 0x32, 0x89, 0x8d, 0x0b, 0xba, 0x32,
-	0x1c, 0xd1, 0xed, 0xdf, 0x85, 0x42, 0xad, 0x4d, 0x7c, 0x2e, 0x4e, 0x8c, 0x4f, 0x7b, 0xde, 0x3b,
-	0x78, 0x43, 0x4f, 0x31, 0x3c, 0x31, 0x58, 0x35, 0xe3, 0x80, 0x3e, 0xc2, 0x66, 0xbf, 0x00, 0x93,
-	0x7b, 0xd4, 0x97, 0xf3, 0xcd, 0xc7, 0xc1, 0xee, 0xa9, 0x66, 0x1c, 0xd0, 0xed, 0x7f, 0xb3, 0x60,
-	0x5e, 0xce, 0x60, 0xcd, 0x61, 0x0d, 0x6f, 0x8f, 0xfa, 0xfb, 0x98, 0xb2, 0x7e, 0xe7, 0x84, 0x27,
-	0xb4, 0x06, 0xb3, 0x8c, 0x76, 0xf7, 0xa8, 0x5f, 0xf3, 0x5c, 0xc6, 0x7d, 0xe2, 0xb8, 0x5c, 0xcf,
-	0x6c, 0x41, 0xf7, 0x9e, 0xad, 0x27, 0xe8, 0x78, 0x60, 0x04, 0x7a, 0x1e, 0xa6, 0xf4, 0xb4, 0xc5,
-	0x51, 0x12, 0x82, 0x9d, 0x16, 0x7b, 0xa0, 0xd7, 0xc4, 0x70, 0x48, 0xb5, 0xff, 0xdb, 0x82, 0x39,
-	0xb9, 0xaa, 0x7a, 0x7f, 0x9b, 0x35, 0x7c, 0xa7, 0x27, 0x0c, 0xf0, 0x37, 0x71, 0x49, 0xd7, 0xe1,
-	0x6c, 0x33, 0x10, 0xfc, 0x86, 0xd3, 0x75, 0xb8, 0xd4, 0x91, 0x42, 0xf5, 0x09, 0x8d, 0x71, 0x76,
-	0x2d, 0x46, 0xc5, 0x89, 0xde, 0x6a, 0xfb, 0x3a, 0x7d, 0xc6, 0xa9, 0xbf, 0xe9, 0x7b, 0x5d, 0x4f,
-	0xac, 0x73, 0x8b, 0xb0, 0x5d, 0xf4, 0x1b, 0x30, 0xd5, 0xd5, 0x97, 0x9e, 0xb6, 0x9a, 0xbf, 0x3a,
-	0x9a, 0xd5, 0xbc, 0xbb, 0xfd, 0x3d, 0xda, 0xe0, 0xe2, 0xc2, 0x8c, 0xb4, 0x2d, 0x6a, 0xc3, 0x21,
-	0x2a, 0x7a, 0x0f, 0x26, 0x58, 0x8f, 0x36, 0xa4, 0x88, 0xca, 0x2b, 0xaf, 0x8f, 0xa6, 0xd4, 0xb1,
-	0x49, 0xd6, 0x7b, 0xb4, 0x11, 0xc9, 0x56, 0xfc, 0x87, 0x25, 0xa4, 0xfd, 0x9f, 0x16, 0x2c, 0xa4,
-	0xad, 0x6a, 0xc3, 0x61, 0x1c, 0x7d, 0x30, 0xb0, 0xb2, 0xca, 0x68, 0x2b, 0x13, 0xa3, 0xe5, 0xba,
-	0x42, 0xed, 0x0d, 0x5a, 0x8c, 0x55, 0x3d, 0x80, 0x82, 0xc3, 0x69, 0x37, 0x70, 0x35, 0xae, 0x8d,
-	0xb6, 0xac, 0xb4, 0xc9, 0x46, 0x57, 0xe8, 0xba, 0x00, 0xc4, 0x0a, 0xd7, 0x7e, 0x1f, 0xa6, 0x6b,
-	0x7d, 0xdf, 0xa7, 0x2e, 0x57, 0x17, 0xdc, 0xb7, 0xa1, 0xc0, 0x1c, 0x57, 0xdb, 0xf9, 0x6c, 0x77,
-	0x5b, 0x49, 0x80, 0xd7, 0xc5, 0x60, 0xac, 0x30, 0xec, 0xbf, 0xcc, 0xc3, 0xf9, 0xe0, 0xc4, 0xd0,
-	0xe6, 0xaa, 0xcf, 0x9d, 0x1d, 0xd2, 0xe0, 0x0c, 0x35, 0x61, 0xba, 0x19, 0x35, 0x73, 0x6d, 0x88,
-	0xb3, 0xf0, 0x0a, 0x8d, 0xbd, 0x01, 0xcf, 0x71, 0x0c, 0x15, 0xbd, 0x0b, 0xf9, 0x96, 0xc3, 0xb5,
-	0x67, 0x78, 0x75, 0x34, 0xc9, 0xdd, 0x72, 0x92, 0x96, 0xa7, 0x5a, 0xd6, 0xac, 0xf2, 0xb7, 0x1c,
-	0x8e, 0x05, 0x22, 0xda, 0x86, 0xa2, 0xd3, 0x25, 0x2d, 0x9a, 0x71, 0x57, 0xd6, 0xc5, 0x98, 0x24,
-	0x7a, 0xe8, 0x6a, 0x4a, 0x2a, 0xc3, 0x1a, 0x59, 0xf0, 0x68, 0x08, 0x8b, 0xa1, 0x6c, 0xf6, 0xe8,
-	0x3b, 0x9f, 0x62, 0x3b, 0x23, 0x1e, 0x92, 0xca, 0xb0, 0x46, 0xb6, 0xbf, 0xca, 0xc1, 0x6c, 0x24,
-	0xbf, 0x9a, 0xd7, 0xed, 0x3a, 0x1c, 0x2d, 0x42, 0xce, 0x69, 0x6a, 0x83, 0x04, 0x7a, 0x60, 0x6e,
-	0x7d, 0x0d, 0xe7, 0x9c, 0x26, 0x7a, 0x0e, 0x8a, 0xdb, 0x3e, 0x71, 0x1b, 0x6d, 0x6d, 0x88, 0x42,
-	0xe0, 0xaa, 0x6c, 0xc5, 0x9a, 0x8a, 0x9e, 0x86, 0x3c, 0x27, 0x2d, 0x6d, 0x7f, 0x42, 0xf9, 0x6d,
-	0x91, 0x16, 0x16, 0xed, 0xc2, 0xf0, 0xb1, 0xbe, 0xd4, 0x61, 0xb9, 0xf3, 0x86, 0xe1, 0xab, 0xab,
-	0x66, 0x1c, 0xd0, 0x05, 0x47, 0xd2, 0xe7, 0x6d, 0xcf, 0x5f, 0x28, 0xc4, 0x39, 0xae, 0xca, 0x56,
-	0xac, 0xa9, 0xc2, 0x45, 0x69, 0xc8, 0xf9, 0x73, 0xea, 0x2f, 0x14, 0xe3, 0x2e, 0x4a, 0x2d, 0x20,
-	0xe0, 0xa8, 0x0f, 0xfa, 0x10, 0xca, 0x0d, 0x9f, 0x12, 0xee, 0xf9, 0x6b, 0x84, 0xd3, 0x85, 0xc9,
-	0xcc, 0x27, 0xf0, 0x9c, 0xf0, 0xd2, 0x6b, 0x11, 0x04, 0x36, 0xf1, 0xec, 0xbf, 0xcf, 0xc3, 0x42,
-	0x24, 0x5a, 0xb9, 0xb7, 0x91, 0x67, 0xaa, 0xc5, 0x63, 0x0d, 0x11, 0xcf, 0x73, 0x50, 0x6c, 0x3a,
-	0x2d, 0xca, 0x78, 0x52, 0xca, 0x6b, 0xb2, 0x15, 0x6b, 0x2a, 0xfa, 0xe3, 0x44, 0x34, 0x52, 0x90,
-	0x07, 0xe5, 0xee, 0x68, 0x07, 0x65, 0xd8, 0xe4, 0xc6, 0x08, 0x49, 0xd0, 0x0a, 0x40, 0xcb, 0xe1,
-	0xfa, 0xd2, 0xd2, 0xbb, 0x1e, 0x1a, 0xeb, 0x5b, 0x21, 0x05, 0x1b, 0xbd, 0xd0, 0xbb, 0x50, 0x92,
-	0xf2, 0x1a, 0x53, 0xff, 0xa5, 0x0b, 0x53, 0x0b, 0x00, 0x70, 0x84, 0x75, 0xec, 0x20, 0xe7, 0xcb,
-	0x09, 0x98, 0xbc, 0xe9, 0x53, 0xa7, 0xd5, 0xe6, 0x8f, 0xe1, 0xd6, 0x7a, 0x06, 0x0a, 0xa4, 0xe3,
-	0x10, 0x26, 0x0f, 0xa0, 0x11, 0xe6, 0xac, 0x8a, 0x46, 0xac, 0x68, 0xe8, 0x7d, 0x28, 0x7a, 0xbe,
-	0xd3, 0x72, 0xdc, 0x85, 0x92, 0x9c, 0xc4, 0x2b, 0xa3, 0x6d, 0xb1, 0x5e, 0xc5, 0x5d, 0x39, 0x34,
-	0x3a, 0x45, 0xea, 0x7f, 0xac, 0x21, 0xd1, 0x7d, 0x98, 0x54, 0x5a, 0x11, 0x58, 0x9a, 0xe5, 0x91,
-	0x2d, 0xa5, 0x52, 0xac, 0x48, 0x7b, 0xd5, 0xff, 0x0c, 0x07, 0x80, 0xa8, 0x1e, 0x1a, 0xca, 0x09,
-	0x09, 0xfd, 0x62, 0x06, 0x43, 0x39, 0xd4, 0x32, 0xd6, 0x43, 0xcb, 0x58, 0xc8, 0x02, 0x2a, 0x6d,
-	0xdf, 0x30, 0x53, 0x28, 0x44, 0xac, 0x3d, 0xf2, 0xe2, 0x18, 0x22, 0xd6, 0xe1, 0xc0, 0xd9, 0xb8,
-	0x1b, 0x1f, 0x38, 0xec, 0xf6, 0x27, 0x79, 0x98, 0xd3, 0x3d, 0x6b, 0x5e, 0xa7, 0x43, 0x1b, 0xd2,
-	0xfd, 0x53, 0x86, 0x36, 0x9f, 0x6a, 0x68, 0x9d, 0xe0, 0xda, 0x57, 0x97, 0x57, 0x35, 0xd3, 0x6c,
-	0x22, 0x1e, 0x15, 0x79, 0xd5, 0x2b, 0x35, 0x0e, 0x77, 0x49, 0xf7, 0xd2, 0x0e, 0x00, 0xfa, 0x23,
-	0x0b, 0xce, 0xef, 0x51, 0xdf, 0xd9, 0x71, 0x1a, 0x52, 0x65, 0x6e, 0x3b, 0x8c, 0x7b, 0xfe, 0xbe,
-	0xbe, 0xda, 0x5e, 0x1b, 0x8d, 0xf3, 0x3d, 0x03, 0x60, 0xdd, 0xdd, 0xf1, 0xaa, 0x4f, 0x69, 0x6e,
-	0xe7, 0xef, 0x0d, 0x42, 0xe3, 0x34, 0x7e, 0x8b, 0x3d, 0x80, 0x68, 0xb6, 0x29, 0x1a, 0xbb, 0x61,
-	0x6a, 0xec, 0xc8, 0x13, 0x0b, 0x16, 0x1b, 0x98, 0x37, 0x53, 0xd3, 0x3f, 0xb3, 0xa0, 0xac, 0xe9,
-	0x8f, 0xc1, 0x93, 0xc3, 0x71, 0x4f, 0xee, 0xe5, 0x4c, 0xf3, 0x1f, 0xe2, 0xbc, 0xf9, 0x30, 0x13,
-	0x53, 0x72, 0x74, 0x45, 0x67, 0x34, 0xd4, 0xd5, 0xf2, 0xcb, 0x66, 0x46, 0xe3, 0xe1, 0xc1, 0xd2,
-	0x5c, 0xac, 0x73, 0x94, 0xe6, 0x38, 0x3a, 0xbc, 0xb8, 0x36, 0xf5, 0xe3, 0xbf, 0x5e, 0x3a, 0xf3,
-	0x83, 0x9f, 0x5e, 0x3a, 0x63, 0x7f, 0x9a, 0x87, 0xd9, 0xa4, 0x54, 0x47, 0xc8, 0xb5, 0x44, 0x36,
-	0x6c, 0xea, 0x54, 0x6d, 0x58, 0xee, 0xf4, 0x6c, 0x58, 0xfe, 0x34, 0x6c, 0xd8, 0xc4, 0x89, 0xd9,
-	0x30, 0xfb, 0x5f, 0x2c, 0x38, 0x1b, 0xee, 0xcc, 0x47, 0x7d, 0xe1, 0x22, 0x44, 0x52, 0xb7, 0x4e,
-	0x5e, 0xea, 0x0f, 0x60, 0x92, 0x79, 0x7d, 0xbf, 0x21, 0xfd, 0x60, 0x81, 0xfe, 0x6a, 0x36, 0xa3,
-	0xa9, 0xc6, 0x1a, 0xce, 0x9f, 0x6a, 0xc0, 0x01, 0xaa, 0xfd, 0x59, 0x2e, 0x5c, 0x90, 0xa6, 0x29,
-	0xdf, 0xc8, 0x17, 0x9e, 0xa3, 0x58, 0xd0, 0x94, 0xe9, 0x1b, 0x89, 0x56, 0xac, 0xa9, 0xc8, 0x96,
-	0xf6, 0x3c, 0x70, 0xd1, 0x4b, 0x55, 0xd0, 0x66, 0x59, 0x6e, 0x82, 0xa2, 0xa0, 0x1e, 0xcc, 0xfa,
-	0xf4, 0xa3, 0xbe, 0xe3, 0xd3, 0x66, 0xdd, 0x23, 0xbb, 0xc2, 0xaf, 0xd0, 0x79, 0xa8, 0x11, 0xf5,
-	0x7e, 0xad, 0xef, 0x4b, 0x13, 0x56, 0x9d, 0x17, 0xe1, 0x35, 0x4e, 0x60, 0xe1, 0x01, 0x74, 0xe4,
-	0xc1, 0x3c, 0xd9, 0x23, 0x4e, 0x87, 0x6c, 0x3b, 0x1d, 0x87, 0xef, 0xd7, 0xb9, 0x4f, 0x38, 0x6d,
-	0xed, 0x6b, 0x2f, 0xf8, 0x4d, 0xbd, 0x96, 0xf9, 0xd5, 0x94, 0x3e, 0x0f, 0x0f, 0x96, 0x9e, 0xd2,
-	0xb2, 0x48, 0x23, 0xe3, 0x54, 0x60, 0xfb, 0x67, 0x85, 0xd0, 0x42, 0xe8, 0xd4, 0xd3, 0x6f, 0x41,
-	0xb9, 0xa1, 0xe2, 0xbd, 0xce, 0xfe, 0xba, 0xab, 0xcf, 0xf4, 0xda, 0x18, 0xb7, 0x5d, 0xa5, 0x16,
-	0xc1, 0x24, 0x1c, 0x45, 0x83, 0x82, 0x4d, 0x6e, 0xe8, 0xfb, 0x00, 0xca, 0xf4, 0xd3, 0xe6, 0xba,
-	0xab, 0xef, 0xb6, 0xda, 0x38, 0xbc, 0xef, 0x85, 0x28, 0x8a, 0x75, 0xe8, 0x64, 0x45, 0x04, 0x6c,
-	0xb0, 0x12, 0xab, 0x0e, 0x12, 0xad, 0x37, 0x3d, 0x5f, 0x1b, 0x89, 0xb1, 0x56, 0xbd, 0x1a, 0xc1,
-	0x24, 0xdd, 0xe3, 0x88, 0x82, 0x4d, 0x6e, 0x8b, 0x3e, 0xcc, 0x26, 0x65, 0x95, 0x72, 0xbf, 0xdd,
-	0x8e, 0xdf, 0x6f, 0x2b, 0x23, 0x5a, 0x04, 0x23, 0x76, 0x37, 0x53, 0xfd, 0x3e, 0x9c, 0x4b, 0xc8,
-	0x28, 0x85, 0xe5, 0x7a, 0x9c, 0xe5, 0x2b, 0x59, 0xee, 0x7a, 0x9d, 0x10, 0x37, 0x79, 0x32, 0x98,
-	0x4d, 0x4a, 0xe7, 0xc4, 0x98, 0xc6, 0xb2, 0xf0, 0xe6, 0x25, 0xfe, 0x57, 0x39, 0x28, 0x85, 0x66,
-	0x3c, 0x4b, 0x4a, 0x4d, 0xb9, 0x5f, 0xb9, 0x23, 0xe2, 0xdc, 0xfc, 0x28, 0x71, 0xee, 0xc4, 0xf0,
-	0x38, 0x37, 0x48, 0xbb, 0x17, 0x1f, 0x9d, 0x76, 0x37, 0xe2, 0xdc, 0xc9, 0xd1, 0xe3, 0xdc, 0xa9,
-	0xa3, 0xe3, 0x5c, 0xfb, 0x6f, 0x2c, 0x40, 0x83, 0x49, 0x8d, 0x2c, 0x82, 0x22, 0xc9, 0xcb, 0xf5,
-	0xb5, 0xac, 0x11, 0xe6, 0x51, 0x77, 0xac, 0xfd, 0x59, 0x01, 0xce, 0xdd, 0x72, 0xc6, 0xce, 0x8e,
-	0x72, 0x78, 0x52, 0x21, 0xd5, 0xa9, 0x76, 0x7c, 0x43, 0xcb, 0xaa, 0xf6, 0xf7, 0x9a, 0x1e, 0xfa,
-	0x64, 0x2d, 0xbd, 0xdb, 0xc3, 0xe1, 0x24, 0x3c, 0x0c, 0x7a, 0xe4, 0x43, 0xf2, 0x26, 0xcc, 0x30,
-	0xee, 0x3b, 0x0d, 0xae, 0xf2, 0xaf, 0x6c, 0xa1, 0x2c, 0x6f, 0xae, 0x0b, 0xba, 0xfb, 0x4c, 0xdd,
-	0x24, 0xe2, 0x78, 0xdf, 0xd4, 0xb4, 0xee, 0x44, 0xe6, 0xb4, 0xee, 0x32, 0x94, 0x48, 0xa7, 0xe3,
-	0x7d, 0x7f, 0x8b, 0xb4, 0x98, 0x4e, 0xa4, 0x84, 0xa7, 0x66, 0x35, 0x20, 0xe0, 0xa8, 0x0f, 0xaa,
-	0x00, 0x38, 0x2d, 0xd7, 0xf3, 0xa9, 0x1c, 0x51, 0x94, 0x57, 0xa8, 0x7c, 0xba, 0x5a, 0x0f, 0x5b,
-	0xb1, 0xd1, 0x03, 0xd5, 0xe1, 0x82, 0xe3, 0x32, 0xda, 0xe8, 0xfb, 0xb4, 0xbe, 0xeb, 0xf4, 0xb6,
-	0x36, 0xea, 0xd2, 0x4a, 0xec, 0xcb, 0xd3, 0x3c, 0x55, 0x7d, 0x5a, 0x33, 0xbb, 0xb0, 0x9e, 0xd6,
-	0x09, 0xa7, 0x8f, 0x45, 0xaf, 0xc2, 0xb4, 0xe3, 0x36, 0x3a, 0xfd, 0x26, 0xdd, 0x24, 0xbc, 0xcd,
-	0x16, 0xa6, 0xe4, 0x34, 0x66, 0x0f, 0x0f, 0x96, 0xa6, 0xd7, 0x8d, 0x76, 0x1c, 0xeb, 0x25, 0x46,
-	0xd1, 0x8f, 0x8d, 0x51, 0xa5, 0x68, 0xd4, 0x8d, 0x8f, 0xcd, 0x51, 0x66, 0xaf, 0x94, 0xc4, 0x37,
-	0x64, 0x4a, 0x7c, 0xff, 0x24, 0x07, 0x45, 0xf5, 0xee, 0x84, 0xae, 0x24, 0x1e, 0x77, 0x9e, 0x1e,
-	0x78, 0xdc, 0x29, 0xa7, 0xbd, 0xd1, 0xd9, 0x50, 0x74, 0x18, 0xeb, 0xc7, 0x3d, 0x96, 0x75, 0xd9,
-	0x82, 0x35, 0x45, 0x26, 0x05, 0x3d, 0x77, 0xc7, 0x69, 0xe9, 0x8c, 0xc9, 0x75, 0xc3, 0x4f, 0x89,
-	0xaa, 0x07, 0x1e, 0x84, 0xe5, 0x05, 0x91, 0xcb, 0x12, 0xeb, 0x20, 0x7c, 0x97, 0x3b, 0xf5, 0xbb,
-	0x6f, 0x2b, 0x1e, 0x35, 0x89, 0x88, 0x35, 0xb2, 0xe0, 0xe1, 0xf5, 0x79, 0xaf, 0xcf, 0xe5, 0x41,
-	0x39, 0x21, 0x1e, 0x77, 0x25, 0x22, 0xd6, 0xc8, 0xf6, 0xa7, 0x16, 0x9c, 0x53, 0x32, 0xa8, 0xb5,
-	0x69, 0x63, 0xb7, 0xce, 0x69, 0x4f, 0x84, 0x10, 0x7d, 0x46, 0x59, 0x32, 0x84, 0x78, 0x87, 0x51,
-	0x86, 0x25, 0xc5, 0x58, 0x7d, 0xee, 0xb4, 0x56, 0x6f, 0x1f, 0xe6, 0xa0, 0x20, 0x7d, 0xf5, 0x2c,
-	0xf6, 0x27, 0x9e, 0xff, 0xca, 0x8d, 0x94, 0xff, 0x3a, 0x22, 0x45, 0x1a, 0xe5, 0x00, 0x27, 0x1e,
-	0x99, 0x03, 0x64, 0x69, 0x29, 0xc0, 0xb7, 0x32, 0x84, 0x28, 0xe3, 0x94, 0x20, 0x1c, 0x37, 0xc5,
-	0xf6, 0x73, 0x0b, 0xe6, 0xd3, 0x92, 0xe1, 0x59, 0x64, 0xfe, 0x12, 0x4c, 0xf5, 0x3a, 0x84, 0xef,
-	0x78, 0x7e, 0x37, 0xf9, 0x08, 0xba, 0xa9, 0xdb, 0x71, 0xd8, 0x03, 0xf9, 0x00, 0x7e, 0x10, 0xac,
-	0x06, 0x81, 0xdc, 0xf5, 0xe3, 0x25, 0x4a, 0xa3, 0x1d, 0x0e, 0x9b, 0x18, 0x36, 0xb8, 0xd8, 0x7f,
-	0x5a, 0x80, 0x39, 0x39, 0x64, 0xdc, 0x6b, 0x6d, 0x9c, 0x63, 0xd5, 0x83, 0x27, 0x64, 0x88, 0x39,
-	0x78, 0x13, 0xaa, 0x93, 0x76, 0x55, 0x8f, 0x7f, 0x62, 0x3d, 0xb5, 0xd7, 0xc3, 0xa1, 0x14, 0x3c,
-	0x04, 0x77, 0xf0, 0x7a, 0x83, 0x5f, 0xbc, 0xeb, 0xcd, 0x3c, 0x6c, 0x93, 0x47, 0x1e, 0xb6, 0xa1,
-	0x97, 0xe1, 0xd4, 0x31, 0x2e, 0xc3, 0xc1, 0x0b, 0xaa, 0x94, 0xe9, 0x82, 0xfa, 0x8b, 0x1c, 0x4c,
-	0x6e, 0xfa, 0x9e, 0x7c, 0x54, 0x39, 0xfd, 0xb4, 0xf6, 0xdd, 0xd8, 0x63, 0xec, 0xe5, 0x91, 0x1f,
-	0x63, 0x05, 0x94, 0x7c, 0x86, 0x9d, 0x8a, 0x3f, 0xc1, 0x1a, 0xf9, 0xd9, 0x7c, 0x96, 0xb0, 0x21,
-	0x80, 0x7c, 0x74, 0x7e, 0xf6, 0x33, 0x0b, 0xca, 0xba, 0xe7, 0x37, 0x36, 0x11, 0xa8, 0xe7, 0x37,
-	0x24, 0x11, 0xf8, 0x67, 0xd1, 0x0a, 0x84, 0xd0, 0xd0, 0xef, 0xc0, 0x5c, 0x2f, 0x78, 0xfc, 0xdd,
-	0xf4, 0x3a, 0x4e, 0xc3, 0xa1, 0x41, 0x2e, 0xf9, 0x4a, 0xc6, 0x97, 0x71, 0x39, 0x7c, 0xbf, 0xfa,
-	0x2d, 0xcd, 0x77, 0x6e, 0x33, 0x89, 0x8b, 0x07, 0x59, 0xd9, 0xff, 0x6e, 0xc1, 0x4c, 0x4c, 0xf6,
-	0xa8, 0x01, 0xd0, 0xf0, 0xdc, 0xa6, 0xc3, 0xc3, 0x3a, 0x94, 0xf2, 0xca, 0xf2, 0x68, 0x52, 0xad,
-	0x05, 0xe3, 0xa2, 0x43, 0x17, 0x36, 0x31, 0x6c, 0xc0, 0xa2, 0x57, 0x82, 0xa2, 0xb1, 0xb8, 0xe7,
-	0xa5, 0x8a, 0xc6, 0x1e, 0x1e, 0x2c, 0x4d, 0xeb, 0x39, 0x99, 0x45, 0x64, 0x59, 0x8a, 0xa3, 0xfe,
-	0x36, 0x07, 0xa5, 0x70, 0xfd, 0x8f, 0x41, 0x8d, 0xde, 0x89, 0xa9, 0xd1, 0x2b, 0x19, 0x77, 0x6e,
-	0x58, 0x3d, 0x03, 0xfa, 0x30, 0xa1, 0x4c, 0x59, 0x8f, 0xc4, 0x11, 0xea, 0xf4, 0x8f, 0x6a, 0xf3,
-	0x55, 0xdf, 0xc7, 0xa0, 0x50, 0x5b, 0x71, 0x85, 0x5a, 0xce, 0xb8, 0x9a, 0x21, 0x2a, 0xf5, 0x43,
-	0x0b, 0xce, 0x25, 0x94, 0x00, 0x3d, 0x03, 0x05, 0x99, 0x3b, 0xd4, 0xe7, 0x2b, 0x1c, 0xa8, 0xb3,
-	0x12, 0x92, 0x86, 0x36, 0x61, 0x9e, 0xf4, 0xb9, 0x17, 0x8e, 0xbd, 0xe1, 0x92, 0xed, 0x0e, 0x55,
-	0xa9, 0x86, 0xa9, 0xea, 0x2f, 0x85, 0x49, 0xbe, 0x94, 0x3e, 0x38, 0x75, 0xa4, 0xfd, 0x45, 0x0e,
-	0x50, 0xd8, 0x98, 0x25, 0xe9, 0xfe, 0x21, 0x4c, 0xee, 0xa8, 0xec, 0xd5, 0xf1, 0x5e, 0x4d, 0xaa,
-	0x65, 0xf3, 0xe1, 0x28, 0xc0, 0x44, 0xef, 0x9d, 0xcc, 0x39, 0x82, 0xc1, 0x33, 0x84, 0xee, 0x03,
-	0xec, 0x38, 0xae, 0xc3, 0xda, 0x63, 0xbe, 0x0f, 0xcb, 0xdb, 0xfc, 0x66, 0x88, 0x80, 0x0d, 0x34,
-	0xfb, 0x93, 0x9c, 0x71, 0x3e, 0xa5, 0xb9, 0x1c, 0x69, 0x5f, 0x5f, 0x88, 0x0b, 0xb3, 0x34, 0xf8,
-	0xa2, 0x66, 0x08, 0x66, 0x62, 0x8f, 0xf8, 0x41, 0x72, 0x3f, 0x6b, 0x2d, 0xd2, 0x3d, 0xe2, 0x3b,
-	0x62, 0xe3, 0xa3, 0x2d, 0xbd, 0x47, 0x7c, 0x86, 0x25, 0x24, 0xfa, 0xae, 0x98, 0x2a, 0xed, 0x05,
-	0x26, 0x34, 0xb3, 0x4d, 0xe0, 0xb4, 0x67, 0xae, 0x8f, 0xf6, 0x18, 0x56, 0x80, 0xf6, 0x27, 0x93,
-	0xc6, 0x81, 0xd7, 0x56, 0xfb, 0x0e, 0xa0, 0x0e, 0x61, 0xfc, 0x36, 0x71, 0x9b, 0xe2, 0x78, 0xd2,
-	0x1d, 0x9f, 0xb2, 0xb6, 0xf6, 0xd0, 0x16, 0x35, 0x0a, 0xda, 0x18, 0xe8, 0x81, 0x53, 0x46, 0xa1,
-	0x2b, 0x71, 0xe3, 0xbc, 0x94, 0x34, 0xce, 0x67, 0x23, 0x6d, 0x1b, 0xcf, 0x3c, 0x9b, 0xc7, 0xbd,
-	0x70, 0x0a, 0xc7, 0xfd, 0xb7, 0x61, 0x6e, 0x27, 0xf9, 0xc2, 0xaa, 0x0b, 0x47, 0x5e, 0x1f, 0xf3,
-	0x81, 0xb6, 0x7a, 0xe1, 0x30, 0x7a, 0x96, 0x8b, 0x9a, 0xf1, 0x20, 0x23, 0xe4, 0x05, 0x15, 0xb3,
-	0x32, 0x64, 0x56, 0xd9, 0x90, 0x91, 0x55, 0x2e, 0x11, 0x6c, 0x27, 0x6b, 0x65, 0x15, 0x24, 0x8e,
-	0x31, 0x48, 0xa8, 0x60, 0xf1, 0x24, 0x55, 0x10, 0x5d, 0x09, 0x5f, 0x21, 0xc4, 0x74, 0xa4, 0x2b,
-	0x9b, 0x1f, 0x78, 0x3f, 0x10, 0x24, 0x6c, 0xf6, 0x43, 0x3f, 0xb2, 0xe0, 0x82, 0x38, 0xac, 0x37,
-	0x3e, 0xa6, 0x8d, 0xbe, 0x90, 0x4a, 0x50, 0x48, 0xbf, 0x50, 0x96, 0xd2, 0x18, 0xb1, 0x7e, 0xb8,
-	0x9e, 0x06, 0x11, 0xf9, 0xe5, 0xa9, 0x64, 0x9c, 0xce, 0x18, 0x3d, 0x90, 0xa6, 0x83, 0x53, 0x19,
-	0xf6, 0x1c, 0x3f, 0x27, 0x51, 0xd2, 0x66, 0x87, 0x2b, 0xb3, 0xc3, 0xa9, 0xfd, 0xb3, 0xbc, 0x69,
-	0xad, 0x46, 0xcb, 0x94, 0xdc, 0x87, 0x09, 0x4e, 0xd8, 0xae, 0xd6, 0x82, 0xb7, 0xc6, 0xa8, 0x85,
-	0x8c, 0x74, 0x41, 0x7a, 0xe2, 0xb2, 0x49, 0x62, 0xa2, 0x45, 0xc8, 0x11, 0x96, 0xcc, 0x9b, 0xaf,
-	0x32, 0x9c, 0x23, 0x4c, 0xe6, 0xd4, 0x77, 0x74, 0x84, 0x14, 0xe5, 0xd4, 0x77, 0x70, 0xce, 0xd9,
-	0x41, 0xef, 0x41, 0xc1, 0xa7, 0xdc, 0xdf, 0xd7, 0xc6, 0xfc, 0xea, 0x18, 0x86, 0x0b, 0x8b, 0xf1,
-	0x4a, 0x44, 0xf2, 0x4f, 0xac, 0x10, 0x43, 0x73, 0x5b, 0x3c, 0x79, 0x73, 0x1b, 0xe5, 0x9c, 0xf2,
-	0xa7, 0x96, 0x73, 0xfa, 0x89, 0x65, 0x5c, 0xef, 0xe1, 0x3a, 0xd1, 0x3b, 0x30, 0xc9, 0x9d, 0x2e,
-	0xf5, 0xfa, 0x3c, 0x9b, 0xcf, 0x14, 0xbe, 0x4a, 0x4a, 0x2b, 0xb6, 0xa5, 0x20, 0x70, 0x80, 0x25,
-	0x02, 0x49, 0xea, 0xfb, 0x9e, 0xbf, 0xd5, 0x16, 0x56, 0xd9, 0xeb, 0x28, 0xc7, 0x64, 0x26, 0x0a,
-	0x24, 0x6f, 0xc4, 0xa8, 0x38, 0xd1, 0xdb, 0xfe, 0xc2, 0xf4, 0xee, 0xfe, 0xff, 0xd7, 0xf6, 0xfe,
-	0xb3, 0x05, 0x73, 0x8f, 0xbb, 0xa8, 0xf7, 0xbb, 0x71, 0x87, 0xf5, 0x95, 0x31, 0xd6, 0x33, 0xc4,
-	0x69, 0xfd, 0x00, 0x9e, 0x48, 0x57, 0xe3, 0x13, 0xf9, 0x1a, 0xe6, 0xf3, 0xa4, 0xac, 0xa4, 0xf3,
-	0x14, 0x68, 0x9f, 0x75, 0x8a, 0xce, 0x4e, 0xee, 0xa4, 0x9d, 0x1d, 0xdf, 0x5c, 0x89, 0xfe, 0x74,
-	0x08, 0x7d, 0xa8, 0x8f, 0x99, 0x95, 0xe5, 0x63, 0x94, 0x01, 0x98, 0xa1, 0x47, 0xed, 0x0b, 0x0b,
-	0x2e, 0xa4, 0xf6, 0x0e, 0x45, 0x98, 0x3b, 0x45, 0x11, 0x5a, 0x27, 0x2d, 0xc2, 0xfb, 0x86, 0x08,
-	0x83, 0x29, 0x9c, 0xd4, 0xf7, 0x7e, 0x3f, 0xce, 0xc1, 0x2c, 0xa6, 0x3d, 0x2f, 0x96, 0x3a, 0xdd,
-	0x0c, 0xea, 0xb9, 0x33, 0xc4, 0x1a, 0x89, 0x57, 0xc5, 0xea, 0x64, 0xac, 0x90, 0x5b, 0x28, 0x62,
-	0x37, 0xf0, 0x2c, 0x47, 0x16, 0xfc, 0x40, 0x52, 0x57, 0x5d, 0x49, 0x2a, 0x3d, 0xac, 0x00, 0x05,
-	0xb2, 0xac, 0xca, 0xd1, 0xd7, 0xc6, 0xeb, 0x19, 0xea, 0x7b, 0x06, 0x91, 0x65, 0x33, 0x56, 0x80,
-	0xf6, 0xa7, 0x39, 0x50, 0x71, 0xc9, 0x63, 0xb0, 0xbb, 0xbf, 0x1e, 0xb3, 0xbb, 0xcb, 0xa3, 0x7a,
-	0x57, 0x42, 0x3c, 0xc3, 0x72, 0x0f, 0xc9, 0x98, 0xf1, 0x72, 0x16, 0xd0, 0x47, 0xe7, 0x1d, 0xfe,
-	0xc1, 0x82, 0x92, 0xec, 0xf7, 0x18, 0x4c, 0xf8, 0x66, 0xdc, 0x84, 0xbf, 0x98, 0x61, 0x15, 0x43,
-	0x4c, 0xf7, 0x27, 0x79, 0x3d, 0xfb, 0x30, 0x22, 0x6d, 0x13, 0xbf, 0xa9, 0x63, 0xad, 0x48, 0x03,
-	0x45, 0x23, 0x56, 0x34, 0xf4, 0x9b, 0xaa, 0x80, 0x89, 0x32, 0x4e, 0x9b, 0x37, 0xc3, 0xc0, 0x27,
-	0x9f, 0xb9, 0x12, 0x4b, 0x57, 0x8b, 0x45, 0x39, 0x77, 0x9c, 0x40, 0xc5, 0x03, 0x7c, 0x44, 0x30,
-	0xd4, 0x4b, 0xda, 0x32, 0x1d, 0x24, 0xbc, 0x3e, 0xa6, 0xe1, 0x54, 0xc1, 0xd0, 0x40, 0x33, 0x1e,
-	0x64, 0x84, 0xda, 0x30, 0x6d, 0xd6, 0x90, 0xea, 0xb3, 0xb4, 0x92, 0xbd, 0x58, 0x55, 0x3d, 0x0c,
-	0x9b, 0x2d, 0x38, 0x86, 0x6c, 0x1f, 0x14, 0xa1, 0x6c, 0x1c, 0xbe, 0x44, 0x1e, 0x73, 0xe6, 0x74,
-	0xf2, 0x98, 0xe9, 0x61, 0x77, 0x79, 0xac, 0xb0, 0xfb, 0x72, 0x3c, 0xec, 0x7e, 0x2a, 0x19, 0x76,
-	0x83, 0x5c, 0x5d, 0x2c, 0xe4, 0x66, 0x70, 0x56, 0xc7, 0x9f, 0x41, 0x31, 0x70, 0xa6, 0x44, 0xc6,
-	0x60, 0x94, 0x8b, 0x84, 0x5f, 0x79, 0x33, 0x06, 0x89, 0x13, 0x2c, 0x84, 0x5f, 0xaa, 0x5b, 0xea,
-	0xfd, 0x6e, 0x97, 0xf8, 0xfb, 0x0b, 0xd3, 0x72, 0xc2, 0xa1, 0x5f, 0x7a, 0x33, 0x46, 0xc5, 0x89,
-	0xde, 0x68, 0x13, 0x8a, 0x2a, 0x7c, 0xd5, 0x05, 0xa6, 0x2f, 0x65, 0x89, 0x8c, 0x95, 0x5f, 0xae,
-	0xfe, 0xc6, 0x1a, 0xc7, 0xcc, 0x3c, 0x94, 0x8e, 0xc8, 0x3c, 0xdc, 0x01, 0xe4, 0x6d, 0xcb, 0x08,
-	0xa0, 0x79, 0x4b, 0x7d, 0x18, 0x2f, 0x4e, 0x65, 0x51, 0x86, 0xb5, 0xe1, 0x86, 0xdd, 0x1d, 0xe8,
-	0x81, 0x53, 0x46, 0x09, 0xad, 0xd6, 0x31, 0x6f, 0xa8, 0x0a, 0x3a, 0xcb, 0x90, 0x35, 0x66, 0x8a,
-	0x82, 0x38, 0x59, 0xa0, 0x58, 0x4b, 0xa0, 0xe2, 0x01, 0x3e, 0xe8, 0x23, 0x98, 0x11, 0x47, 0x28,
-	0x62, 0x0c, 0xc7, 0x64, 0x3c, 0x77, 0x78, 0xb0, 0x34, 0xb3, 0x61, 0x42, 0xe2, 0x38, 0x07, 0xfb,
-	0x4f, 0xf2, 0x90, 0x1e, 0x71, 0x47, 0xdf, 0x46, 0x58, 0x8f, 0xf8, 0x36, 0xe2, 0x5d, 0x28, 0x31,
-	0x4e, 0x7c, 0xf5, 0x1d, 0x49, 0x6e, 0xbc, 0xef, 0x48, 0xea, 0x01, 0x00, 0x8e, 0xb0, 0x12, 0xe9,
-	0x8f, 0xfc, 0x89, 0xa6, 0x3f, 0x56, 0x00, 0x64, 0x54, 0x55, 0xf3, 0xfa, 0xfa, 0xc1, 0x73, 0x26,
-	0xb2, 0x09, 0x37, 0x42, 0x0a, 0x36, 0x7a, 0xa1, 0xab, 0xe1, 0xc5, 0xa9, 0x5e, 0x38, 0x2f, 0x0d,
-	0x94, 0x95, 0x24, 0x13, 0x68, 0x29, 0x5f, 0x7f, 0x1f, 0x51, 0x86, 0x66, 0xff, 0x6f, 0x0e, 0x62,
-	0xc6, 0x10, 0xfd, 0xd0, 0x82, 0x39, 0x92, 0xf8, 0xc4, 0x3e, 0xf0, 0x25, 0x7f, 0x2d, 0xdb, 0xef,
-	0x1e, 0x0c, 0x7c, 0xa1, 0x1f, 0xbd, 0x29, 0x25, 0xbb, 0x30, 0x3c, 0xc8, 0x14, 0xfd, 0xa1, 0x05,
-	0xe7, 0xc9, 0xe0, 0x6f, 0x28, 0xe8, 0x4d, 0x7f, 0x63, 0xec, 0x1f, 0x61, 0xa8, 0x3e, 0x79, 0x78,
-	0xb0, 0x94, 0xf6, 0xeb, 0x12, 0x38, 0x8d, 0x1d, 0x7a, 0x1f, 0x26, 0x88, 0xdf, 0x0a, 0xf2, 0xaf,
-	0xd9, 0xd9, 0x06, 0x3f, 0x8d, 0x11, 0x79, 0x47, 0xab, 0x7e, 0x8b, 0x61, 0x09, 0x6a, 0xff, 0x34,
-	0x0f, 0xb3, 0xc9, 0x6f, 0x29, 0x74, 0xa5, 0xe2, 0x44, 0x6a, 0xa5, 0xa2, 0xd0, 0x91, 0x06, 0x0f,
-	0xcb, 0x06, 0x23, 0x1d, 0x11, 0x8d, 0x58, 0xd1, 0x42, 0x1d, 0x91, 0x15, 0xce, 0x85, 0x63, 0xe8,
-	0x88, 0x2c, 0x6b, 0x8e, 0xb0, 0xd0, 0xd5, 0xf8, 0xdd, 0x62, 0x27, 0xef, 0x96, 0x39, 0x73, 0x2d,
-	0xe3, 0x66, 0x75, 0xbb, 0x50, 0x36, 0xf6, 0x41, 0x6b, 0xe2, 0xb5, 0xcc, 0x72, 0x8f, 0x8e, 0xdd,
-	0x39, 0x55, 0xdc, 0x12, 0x51, 0x4c, 0xfc, 0x48, 0xef, 0xa5, 0xb4, 0x8e, 0x95, 0xf6, 0x94, 0xe2,
-	0x32, 0xd0, 0xec, 0xff, 0xb0, 0x60, 0x26, 0x56, 0x3e, 0x2b, 0xb8, 0x05, 0x65, 0xca, 0xe3, 0xff,
-	0x9e, 0xc4, 0xbd, 0x10, 0x01, 0x1b, 0x68, 0xe8, 0x7b, 0x50, 0xee, 0x78, 0x6e, 0x8b, 0x32, 0x5e,
-	0xf7, 0xc8, 0xae, 0xd6, 0x93, 0xac, 0x49, 0xa4, 0x85, 0xc3, 0x83, 0xa5, 0xf9, 0x0d, 0x05, 0x53,
-	0xf3, 0xba, 0xbd, 0x0e, 0xe5, 0xaa, 0xa0, 0x1d, 0x9b, 0xe0, 0xf2, 0x65, 0xf4, 0x5d, 0xe2, 0xd3,
-	0xb6, 0xd7, 0x67, 0xf4, 0x9b, 0xfa, 0x32, 0x1a, 0x4e, 0xf0, 0xa4, 0x5f, 0x46, 0x23, 0xe0, 0xa3,
-	0x5f, 0x46, 0xc3, 0xbe, 0xdf, 0xd8, 0x97, 0xd1, 0x70, 0x86, 0x43, 0x22, 0x95, 0xff, 0xc9, 0x19,
-	0xab, 0x88, 0x47, 0x2b, 0xb9, 0x47, 0x44, 0x2b, 0x1f, 0xc0, 0x94, 0xe3, 0x72, 0xea, 0xef, 0x91,
-	0x8e, 0xce, 0x01, 0x67, 0x3d, 0x8b, 0xe1, 0x52, 0xd7, 0x35, 0x0e, 0x0e, 0x11, 0x51, 0x07, 0x2e,
-	0x04, 0x6f, 0x26, 0x3e, 0x25, 0xd1, 0x9b, 0xad, 0xae, 0x7b, 0x7a, 0x2d, 0x48, 0xee, 0xdf, 0x4c,
-	0xeb, 0xf4, 0x70, 0x18, 0x01, 0xa7, 0x83, 0x22, 0x06, 0x33, 0xcc, 0x08, 0xd3, 0x83, 0x1b, 0x71,
-	0xc4, 0xf7, 0xa6, 0x64, 0x66, 0xc3, 0x28, 0x96, 0x32, 0x41, 0x71, 0x9c, 0x87, 0xfd, 0xaf, 0x79,
-	0x38, 0x97, 0x38, 0x69, 0x89, 0x70, 0xa4, 0xf4, 0x38, 0xc3, 0x91, 0xe2, 0x58, 0xe1, 0x48, 0xba,
-	0xa7, 0x3c, 0x31, 0x96, 0xa7, 0xfc, 0xa6, 0xf2, 0x56, 0xf5, 0xce, 0xad, 0xaf, 0xe9, 0x82, 0xf8,
-	0x50, 0x9a, 0x1b, 0x26, 0x11, 0xc7, 0xfb, 0x4a, 0x77, 0xa2, 0x39, 0xf8, 0xdb, 0x04, 0xda, 0xd5,
-	0x7e, 0x23, 0x6b, 0x71, 0x60, 0x08, 0xa0, 0xdc, 0x89, 0x14, 0x02, 0x4e, 0x63, 0x57, 0xbd, 0xf3,
-	0xf9, 0xd7, 0x17, 0xcf, 0x7c, 0xf9, 0xf5, 0xc5, 0x33, 0x5f, 0x7d, 0x7d, 0xf1, 0xcc, 0x0f, 0x0e,
-	0x2f, 0x5a, 0x9f, 0x1f, 0x5e, 0xb4, 0xbe, 0x3c, 0xbc, 0x68, 0x7d, 0x75, 0x78, 0xd1, 0xfa, 0xaf,
-	0xc3, 0x8b, 0xd6, 0x8f, 0x7e, 0x7e, 0xf1, 0xcc, 0xfd, 0x67, 0x47, 0xf9, 0x45, 0xb3, 0xff, 0x0b,
-	0x00, 0x00, 0xff, 0xff, 0xb3, 0x95, 0x0d, 0x4d, 0xf8, 0x4c, 0x00, 0x00,
+	// 4174 bytes of a gzipped FileDescriptorProto
+	0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xdc, 0x5c, 0x4d, 0x6c, 0x1c, 0x47,
+	0x76, 0x56, 0xcf, 0x70, 0x86, 0x9c, 0x37, 0xa4, 0x44, 0x96, 0x28, 0x9b, 0x4b, 0xc7, 0xa2, 0xd2,
+	0x36, 0x0c, 0x3b, 0xb6, 0x87, 0x91, 0x64, 0xd9, 0xb2, 0x6c, 0x28, 0xe0, 0x0c, 0xf5, 0x43, 0x2d,
+	0xd7, 0x62, 0x6a, 0x68, 0x79, 0x2d, 0xdb, 0x50, 0x8a, 0x33, 0xc5, 0x99, 0x5e, 0xce, 0x74, 0x8f,
+	0xbb, 0x6a, 0xb8, 0x62, 0x12, 0x24, 0x9b, 0x5f, 0x2c, 0x12, 0x24, 0xd8, 0xc3, 0x06, 0xde, 0x00,
+	0x09, 0x10, 0x24, 0xc7, 0x05, 0x72, 0xcd, 0x21, 0x07, 0x1f, 0x72, 0x71, 0x92, 0x4d, 0x60, 0x24,
+	0x01, 0xe2, 0x00, 0x0b, 0x66, 0xcd, 0x05, 0x72, 0xce, 0x25, 0x17, 0x01, 0x01, 0x82, 0xfa, 0xe9,
+	0xee, 0xea, 0x9e, 0x1e, 0x71, 0x7a, 0x48, 0x0a, 0xce, 0xde, 0xc8, 0x7a, 0x55, 0xdf, 0xab, 0x7a,
+	0x55, 0xef, 0xd5, 0x7b, 0xaf, 0x5e, 0x0f, 0xbc, 0xd6, 0x72, 0x78, 0xbb, 0xbf, 0x55, 0x69, 0x78,
+	0xdd, 0x65, 0xb2, 0xd3, 0x77, 0xf8, 0xde, 0xf2, 0x0e, 0xf1, 0x5b, 0xde, 0x32, 0xe9, 0x39, 0xcb,
+	0xbb, 0x17, 0x49, 0xa7, 0xd7, 0x26, 0x17, 0x97, 0x5b, 0xd4, 0xa5, 0x3e, 0xe1, 0xb4, 0x59, 0xe9,
+	0xf9, 0x1e, 0xf7, 0xd0, 0xf3, 0xd1, 0xa8, 0x8a, 0x1a, 0x55, 0x91, 0xa3, 0x2a, 0xa4, 0xe7, 0x54,
+	0x82, 0x51, 0x8b, 0xaf, 0x1a, 0xd8, 0x2d, 0xaf, 0xe5, 0x2d, 0xcb, 0xc1, 0x5b, 0xfd, 0x6d, 0xf9,
+	0x9f, 0xfc, 0x47, 0xfe, 0xa5, 0x40, 0x17, 0x6f, 0xef, 0x5c, 0x65, 0x15, 0x47, 0x72, 0xa6, 0x0f,
+	0x39, 0x75, 0x99, 0xe3, 0xb9, 0xec, 0x55, 0xd2, 0x73, 0x18, 0xf5, 0x77, 0xa9, 0xbf, 0xdc, 0xdb,
+	0x69, 0x09, 0x1a, 0x8b, 0x77, 0x58, 0xde, 0x1d, 0x98, 0xde, 0xe2, 0x6b, 0x11, 0x52, 0x97, 0x34,
+	0xda, 0x8e, 0x4b, 0xfd, 0xbd, 0x68, 0x78, 0x97, 0x72, 0x92, 0x36, 0x6a, 0x79, 0xd8, 0x28, 0xbf,
+	0xef, 0x72, 0xa7, 0x4b, 0x07, 0x06, 0xbc, 0x7e, 0xd8, 0x00, 0xd6, 0x68, 0xd3, 0x2e, 0x49, 0x8e,
+	0xb3, 0x3f, 0x84, 0xb3, 0x2b, 0x2e, 0xe9, 0xec, 0x31, 0x87, 0xe1, 0xbe, 0xbb, 0xe2, 0xb7, 0xfa,
+	0x5d, 0xea, 0x72, 0x74, 0x01, 0x26, 0x5c, 0xd2, 0xa5, 0x0b, 0xd6, 0x05, 0xeb, 0xc5, 0x52, 0x75,
+	0xfa, 0xb3, 0xfd, 0xa5, 0x53, 0x07, 0xfb, 0x4b, 0x13, 0xef, 0x90, 0x2e, 0xc5, 0x92, 0x82, 0x9e,
+	0x83, 0xc2, 0x2e, 0xe9, 0xf4, 0xe9, 0x42, 0x4e, 0x76, 0x99, 0xd1, 0x5d, 0x0a, 0xf7, 0x44, 0x23,
+	0x56, 0x34, 0xfb, 0x77, 0xf2, 0x31, 0xf8, 0x6f, 0x50, 0x4e, 0x9a, 0x84, 0x13, 0xd4, 0x85, 0x62,
+	0x87, 0x6c, 0xd1, 0x0e, 0x5b, 0xb0, 0x2e, 0xe4, 0x5f, 0x2c, 0x5f, 0xba, 0x51, 0x19, 0x65, 0x13,
+	0x2b, 0x29, 0x50, 0x95, 0x75, 0x89, 0x73, 0xc3, 0xe5, 0xfe, 0x5e, 0xf5, 0xb4, 0x9e, 0x44, 0x51,
+	0x35, 0x62, 0xcd, 0x04, 0xfd, 0x96, 0x05, 0x65, 0xe2, 0xba, 0x1e, 0x27, 0x5c, 0x6c, 0xd3, 0x42,
+	0x4e, 0x32, 0xbd, 0x33, 0x3e, 0xd3, 0x95, 0x08, 0x4c, 0x71, 0x3e, 0xab, 0x39, 0x97, 0x0d, 0x0a,
+	0x36, 0x79, 0x2e, 0xbe, 0x09, 0x65, 0x63, 0xaa, 0x68, 0x16, 0xf2, 0x3b, 0x74, 0x4f, 0xc9, 0x17,
+	0x8b, 0x3f, 0xd1, 0x7c, 0x4c, 0xa0, 0x5a, 0x82, 0xd7, 0x72, 0x57, 0xad, 0xc5, 0xeb, 0x30, 0x9b,
+	0x64, 0x98, 0x65, 0xbc, 0xfd, 0xc7, 0x16, 0xcc, 0x1b, 0xab, 0xc0, 0x74, 0x9b, 0xfa, 0xd4, 0x6d,
+	0x50, 0xb4, 0x0c, 0x25, 0xb1, 0x97, 0xac, 0x47, 0x1a, 0xc1, 0x56, 0xcf, 0xe9, 0x85, 0x94, 0xde,
+	0x09, 0x08, 0x38, 0xea, 0x13, 0x1e, 0x8b, 0xdc, 0xe3, 0x8e, 0x45, 0xaf, 0x4d, 0x18, 0x5d, 0xc8,
+	0xc7, 0x8f, 0xc5, 0x86, 0x68, 0xc4, 0x8a, 0x66, 0x3f, 0x80, 0xaf, 0x05, 0xf3, 0xd9, 0xa4, 0xdd,
+	0x5e, 0x87, 0x70, 0x1a, 0x4d, 0xea, 0xf0, 0xa3, 0x77, 0x01, 0x26, 0x76, 0x1c, 0xb7, 0x99, 0x9c,
+	0xc5, 0xd7, 0x1d, 0xb7, 0x89, 0x25, 0xc5, 0xde, 0x81, 0x99, 0x95, 0x5e, 0xcf, 0xf7, 0x76, 0x69,
+	0xb3, 0xce, 0x49, 0x8b, 0xa2, 0xfb, 0x00, 0x44, 0x37, 0xac, 0x70, 0x09, 0x5d, 0xbe, 0xf4, 0x0b,
+	0x15, 0xa5, 0x33, 0x15, 0x53, 0x67, 0x2a, 0xbd, 0x9d, 0x96, 0x68, 0x60, 0x15, 0xa1, 0x9a, 0x95,
+	0xdd, 0x8b, 0x95, 0x4d, 0xa7, 0x4b, 0xab, 0xa7, 0x0f, 0xf6, 0x97, 0x60, 0x25, 0x44, 0xc0, 0x06,
+	0x9a, 0xfd, 0xdb, 0x16, 0x9c, 0x5b, 0xf1, 0x5b, 0x5e, 0x6d, 0x75, 0xa5, 0xd7, 0xbb, 0x4d, 0x49,
+	0x87, 0xb7, 0xeb, 0x9c, 0xf0, 0x3e, 0x43, 0xd7, 0xa1, 0xc8, 0xe4, 0x5f, 0x7a, 0x31, 0x2f, 0x04,
+	0xe7, 0x53, 0xd1, 0x1f, 0xed, 0x2f, 0xcd, 0xa7, 0x0c, 0xa4, 0x58, 0x8f, 0x42, 0x2f, 0xc1, 0x64,
+	0x97, 0x32, 0x46, 0x5a, 0x81, 0xc4, 0xcf, 0x68, 0x80, 0xc9, 0x6f, 0xa8, 0x66, 0x1c, 0xd0, 0xed,
+	0x7f, 0xc8, 0xc1, 0x99, 0x10, 0x4b, 0xb3, 0x3f, 0x81, 0xed, 0xed, 0xc3, 0x74, 0xdb, 0x58, 0xa1,
+	0xdc, 0xe5, 0xf2, 0xa5, 0xb7, 0x46, 0xd4, 0xa4, 0x34, 0x21, 0x55, 0xe7, 0x35, 0x9b, 0x69, 0xb3,
+	0x15, 0xc7, 0xd8, 0xa0, 0x2e, 0x00, 0xdb, 0x73, 0x1b, 0x9a, 0xe9, 0x84, 0x64, 0xfa, 0x66, 0x46,
+	0xa6, 0xf5, 0x10, 0xa0, 0x8a, 0x34, 0x4b, 0x88, 0xda, 0xb0, 0xc1, 0xc0, 0xfe, 0x6b, 0x0b, 0xce,
+	0xa6, 0x8c, 0x43, 0x6f, 0x27, 0xf6, 0xf3, 0xf9, 0x81, 0xfd, 0x44, 0x03, 0xc3, 0xa2, 0xdd, 0x7c,
+	0x05, 0xa6, 0x7c, 0xba, 0xeb, 0x88, 0x9b, 0x42, 0x4b, 0x78, 0x56, 0x8f, 0x9f, 0xc2, 0xba, 0x1d,
+	0x87, 0x3d, 0xd0, 0xcb, 0x50, 0x0a, 0xfe, 0x16, 0x62, 0xce, 0x0b, 0x65, 0x12, 0x1b, 0x17, 0x74,
+	0x65, 0x38, 0xa2, 0xdb, 0xbf, 0x09, 0x85, 0x5a, 0x9b, 0xf8, 0x5c, 0x9c, 0x18, 0x9f, 0xf6, 0xbc,
+	0x77, 0xf1, 0xba, 0x9e, 0x62, 0x78, 0x62, 0xb0, 0x6a, 0xc6, 0x01, 0x7d, 0x84, 0xcd, 0x7e, 0x09,
+	0x26, 0x77, 0xa9, 0x2f, 0xe7, 0x9b, 0x8f, 0x83, 0xdd, 0x53, 0xcd, 0x38, 0xa0, 0xdb, 0xff, 0x6a,
+	0xc1, 0xbc, 0x9c, 0xc1, 0xaa, 0xc3, 0x1a, 0xde, 0x2e, 0xf5, 0xf7, 0x30, 0x65, 0xfd, 0xce, 0x31,
+	0x4f, 0x68, 0x15, 0x66, 0x19, 0xed, 0xee, 0x52, 0xbf, 0xe6, 0xb9, 0x8c, 0xfb, 0xc4, 0x71, 0xb9,
+	0x9e, 0xd9, 0x82, 0xee, 0x3d, 0x5b, 0x4f, 0xd0, 0xf1, 0xc0, 0x08, 0xf4, 0x22, 0x4c, 0xe9, 0x69,
+	0x8b, 0xa3, 0x24, 0x04, 0x3b, 0x2d, 0xf6, 0x40, 0xaf, 0x89, 0xe1, 0x90, 0x6a, 0xff, 0x97, 0x05,
+	0x73, 0x72, 0x55, 0xf5, 0xfe, 0x16, 0x6b, 0xf8, 0x4e, 0x4f, 0x18, 0xe0, 0xaf, 0xe2, 0x92, 0xae,
+	0xc3, 0xe9, 0x66, 0x20, 0xf8, 0x75, 0xa7, 0xeb, 0x70, 0xa9, 0x23, 0x85, 0xea, 0x53, 0x1a, 0xe3,
+	0xf4, 0x6a, 0x8c, 0x8a, 0x13, 0xbd, 0xd5, 0xf6, 0x75, 0xfa, 0x8c, 0x53, 0x7f, 0xc3, 0xf7, 0xba,
+	0x9e, 0x58, 0xe7, 0x26, 0x61, 0x3b, 0xe8, 0x57, 0x60, 0xaa, 0xab, 0x2f, 0x3d, 0x6d, 0x35, 0x7f,
+	0x71, 0x34, 0xab, 0x79, 0x77, 0xeb, 0x5b, 0xb4, 0xc1, 0xc5, 0x85, 0x19, 0x69, 0x5b, 0xd4, 0x86,
+	0x43, 0x54, 0xf4, 0x3e, 0x4c, 0xb0, 0x1e, 0x6d, 0x48, 0x11, 0x95, 0x2f, 0xbd, 0x31, 0x9a, 0x52,
+	0xc7, 0x26, 0x59, 0xef, 0xd1, 0x46, 0x24, 0x5b, 0xf1, 0x1f, 0x96, 0x90, 0xf6, 0x7f, 0x58, 0xb0,
+	0x90, 0xb6, 0xaa, 0x75, 0x87, 0x71, 0xf4, 0xe1, 0xc0, 0xca, 0x2a, 0xa3, 0xad, 0x4c, 0x8c, 0x96,
+	0xeb, 0x0a, 0xb5, 0x37, 0x68, 0x31, 0x56, 0xf5, 0x00, 0x0a, 0x0e, 0xa7, 0xdd, 0xc0, 0xd5, 0xb8,
+	0x36, 0xda, 0xb2, 0xd2, 0x26, 0x1b, 0x5d, 0xa1, 0x6b, 0x02, 0x10, 0x2b, 0x5c, 0xfb, 0x03, 0x98,
+	0xae, 0xf5, 0x7d, 0x9f, 0xba, 0x5c, 0x5d, 0x70, 0x5f, 0x87, 0x02, 0x73, 0x5c, 0x6d, 0xe7, 0xb3,
+	0xdd, 0x6d, 0x25, 0x01, 0x5e, 0x17, 0x83, 0xb1, 0xc2, 0xb0, 0xff, 0x2c, 0x0f, 0x67, 0x83, 0x13,
+	0x43, 0x9b, 0x2b, 0x3e, 0x77, 0xb6, 0x49, 0x83, 0x33, 0xd4, 0x84, 0xe9, 0x66, 0xd4, 0xcc, 0xb5,
+	0x21, 0xce, 0xc2, 0x2b, 0x34, 0xf6, 0x06, 0x3c, 0xc7, 0x31, 0x54, 0xf4, 0x1e, 0xe4, 0x5b, 0x0e,
+	0xd7, 0x9e, 0xe1, 0xd5, 0xd1, 0x24, 0x77, 0xcb, 0x49, 0x5a, 0x9e, 0x6a, 0x59, 0xb3, 0xca, 0xdf,
+	0x72, 0x38, 0x16, 0x88, 0x68, 0x0b, 0x8a, 0x4e, 0x97, 0xb4, 0x68, 0xc6, 0x5d, 0x59, 0x13, 0x63,
+	0x92, 0xe8, 0xa1, 0xab, 0x29, 0xa9, 0x0c, 0x6b, 0x64, 0xc1, 0xa3, 0x21, 0x2c, 0x86, 0xb2, 0xd9,
+	0xa3, 0xef, 0x7c, 0x8a, 0xed, 0x8c, 0x78, 0x48, 0x2a, 0xc3, 0x1a, 0xd9, 0xfe, 0x22, 0x07, 0xb3,
+	0x91, 0xfc, 0x6a, 0x5e, 0xb7, 0xeb, 0x70, 0xb4, 0x08, 0x39, 0xa7, 0xa9, 0x0d, 0x12, 0xe8, 0x81,
+	0xb9, 0xb5, 0x55, 0x9c, 0x73, 0x9a, 0xe8, 0x05, 0x28, 0x6e, 0xf9, 0xc4, 0x6d, 0xb4, 0xb5, 0x21,
+	0x0a, 0x81, 0xab, 0xb2, 0x15, 0x6b, 0x2a, 0x7a, 0x16, 0xf2, 0x9c, 0xb4, 0xb4, 0xfd, 0x09, 0xe5,
+	0xb7, 0x49, 0x5a, 0x58, 0xb4, 0x0b, 0xc3, 0xc7, 0xfa, 0x52, 0x87, 0xe5, 0xce, 0x1b, 0x86, 0xaf,
+	0xae, 0x9a, 0x71, 0x40, 0x17, 0x1c, 0x49, 0x9f, 0xb7, 0x3d, 0x7f, 0xa1, 0x10, 0xe7, 0xb8, 0x22,
+	0x5b, 0xb1, 0xa6, 0x0a, 0x17, 0xa5, 0x21, 0xe7, 0xcf, 0xa9, 0xbf, 0x50, 0x8c, 0xbb, 0x28, 0xb5,
+	0x80, 0x80, 0xa3, 0x3e, 0xe8, 0x23, 0x28, 0x37, 0x7c, 0x4a, 0xb8, 0xe7, 0xaf, 0x12, 0x4e, 0x17,
+	0x26, 0x33, 0x9f, 0xc0, 0x33, 0xc2, 0x4b, 0xaf, 0x45, 0x10, 0xd8, 0xc4, 0xb3, 0xff, 0x26, 0x0f,
+	0x0b, 0x91, 0x68, 0xe5, 0xde, 0x46, 0x9e, 0xa9, 0x16, 0x8f, 0x35, 0x44, 0x3c, 0x2f, 0x40, 0xb1,
+	0xe9, 0xb4, 0x28, 0xe3, 0x49, 0x29, 0xaf, 0xca, 0x56, 0xac, 0xa9, 0xe8, 0xf7, 0x13, 0xd1, 0x48,
+	0x41, 0x1e, 0x94, 0xbb, 0xa3, 0x1d, 0x94, 0x61, 0x93, 0x1b, 0x23, 0x24, 0x41, 0x97, 0x00, 0x5a,
+	0x0e, 0xd7, 0x97, 0x96, 0xde, 0xf5, 0xd0, 0x58, 0xdf, 0x0a, 0x29, 0xd8, 0xe8, 0x85, 0xde, 0x83,
+	0x92, 0x94, 0xd7, 0x98, 0xfa, 0x2f, 0x5d, 0x98, 0x5a, 0x00, 0x80, 0x23, 0xac, 0x23, 0x07, 0x39,
+	0x1f, 0x00, 0xba, 0xf1, 0xb0, 0xe7, 0x53, 0x26, 0xae, 0xee, 0x7b, 0xc4, 0x77, 0xc8, 0x56, 0x87,
+	0x1e, 0x57, 0x1c, 0xfb, 0xf9, 0x04, 0x4c, 0xde, 0xf4, 0xa9, 0xd3, 0x6a, 0xf3, 0x27, 0x70, 0x25,
+	0x3e, 0x07, 0x05, 0xd2, 0x71, 0x08, 0x93, 0xa7, 0xdb, 0x98, 0xd2, 0x8a, 0x68, 0xc4, 0x8a, 0x86,
+	0x3e, 0x80, 0xa2, 0xe7, 0x3b, 0x2d, 0xc7, 0x5d, 0x28, 0xc9, 0x49, 0x5c, 0x1e, 0xed, 0xfc, 0xe8,
+	0x55, 0xdc, 0x95, 0x43, 0xa3, 0x23, 0xaa, 0xfe, 0xc7, 0x1a, 0x12, 0xdd, 0x87, 0x49, 0xa5, 0x72,
+	0x81, 0x19, 0x5b, 0x1e, 0xd9, 0x0c, 0x2b, 0xad, 0x8d, 0x4c, 0x83, 0xfa, 0x9f, 0xe1, 0x00, 0x10,
+	0xd5, 0x43, 0x2b, 0x3c, 0x21, 0xa1, 0x5f, 0xce, 0x60, 0x85, 0x87, 0x9a, 0xdd, 0x7a, 0x68, 0x76,
+	0x0b, 0x59, 0x40, 0xa5, 0x61, 0x1d, 0x66, 0x67, 0x85, 0x88, 0xb5, 0xbb, 0x5f, 0x1c, 0x43, 0xc4,
+	0x3a, 0xd6, 0x38, 0x1d, 0x8f, 0x11, 0x82, 0x68, 0xc0, 0xfe, 0x7e, 0x1e, 0xe6, 0x74, 0xcf, 0x9a,
+	0xd7, 0xe9, 0xd0, 0x86, 0xf4, 0x2d, 0x95, 0x15, 0xcf, 0xa7, 0x5a, 0x71, 0x27, 0xf0, 0x29, 0xd4,
+	0xcd, 0x58, 0xcd, 0x34, 0x9b, 0x88, 0x47, 0x45, 0xfa, 0x11, 0xca, 0x46, 0x84, 0xbb, 0xa4, 0x7b,
+	0x69, 0xef, 0x02, 0xfd, 0x9e, 0x05, 0x67, 0x77, 0xa9, 0xef, 0x6c, 0x3b, 0x0d, 0xa9, 0x8f, 0xb7,
+	0x1d, 0xc6, 0x3d, 0x7f, 0x4f, 0xdf, 0x9b, 0xaf, 0x8f, 0xc6, 0xf9, 0x9e, 0x01, 0xb0, 0xe6, 0x6e,
+	0x7b, 0xd5, 0x67, 0x34, 0xb7, 0xb3, 0xf7, 0x06, 0xa1, 0x71, 0x1a, 0xbf, 0xc5, 0x1e, 0x40, 0x34,
+	0xdb, 0x14, 0x73, 0xb0, 0x6e, 0x2a, 0xef, 0xc8, 0x13, 0x0b, 0x16, 0x1b, 0xd8, 0x4e, 0xd3, 0x8c,
+	0x7c, 0x6a, 0x41, 0x59, 0xd3, 0x9f, 0x80, 0x9b, 0x88, 0xe3, 0x6e, 0xe2, 0xab, 0x99, 0xe6, 0x3f,
+	0xc4, 0x33, 0xf4, 0x61, 0x26, 0xa6, 0xe4, 0xe8, 0x8a, 0x4e, 0x97, 0x28, 0x1b, 0xf8, 0xf3, 0x66,
+	0xba, 0xe4, 0xd1, 0xfe, 0xd2, 0x5c, 0xac, 0x73, 0x94, 0x43, 0x39, 0x3c, 0x76, 0xb9, 0x36, 0xf5,
+	0x83, 0xbf, 0x58, 0x3a, 0xf5, 0x9d, 0x1f, 0x5f, 0x38, 0x65, 0x7f, 0x92, 0x87, 0xd9, 0xa4, 0x54,
+	0x47, 0xb0, 0xbd, 0x91, 0x0d, 0x9b, 0x3a, 0x51, 0x1b, 0x96, 0x3b, 0x39, 0x1b, 0x96, 0x3f, 0x09,
+	0x1b, 0x36, 0x71, 0x6c, 0x36, 0xcc, 0xfe, 0x67, 0x0b, 0x4e, 0x87, 0x3b, 0xf3, 0x71, 0x5f, 0xf8,
+	0x1f, 0x91, 0xd4, 0xad, 0xe3, 0x97, 0xfa, 0x03, 0x98, 0x64, 0x5e, 0xdf, 0x6f, 0x48, 0x27, 0x5b,
+	0xa0, 0xbf, 0x96, 0xcd, 0x68, 0xaa, 0xb1, 0x86, 0x67, 0xa9, 0x1a, 0x70, 0x80, 0x6a, 0x7f, 0x9a,
+	0x0b, 0x17, 0xa4, 0x69, 0xca, 0xf1, 0xf2, 0x85, 0x5b, 0x2a, 0x16, 0x34, 0x65, 0x3a, 0x5e, 0xa2,
+	0x15, 0x6b, 0x2a, 0xb2, 0xa5, 0x3d, 0x0f, 0xfc, 0xff, 0x52, 0x15, 0xb4, 0x59, 0x96, 0x9b, 0xa0,
+	0x28, 0xa8, 0x07, 0xb3, 0x3e, 0xfd, 0xb8, 0xef, 0xf8, 0xb4, 0x59, 0xf7, 0xc8, 0x8e, 0x70, 0x5a,
+	0x74, 0x92, 0x6b, 0x44, 0xbd, 0x5f, 0xed, 0xfb, 0xd2, 0x84, 0x55, 0xe7, 0x45, 0xec, 0x8e, 0x13,
+	0x58, 0x78, 0x00, 0x1d, 0x79, 0x30, 0x4f, 0x76, 0x89, 0xd3, 0x21, 0x5b, 0x4e, 0xc7, 0xe1, 0x7b,
+	0x75, 0xee, 0x13, 0x4e, 0x5b, 0x7b, 0xda, 0xc5, 0x7e, 0x4b, 0xaf, 0x65, 0x7e, 0x25, 0xa5, 0xcf,
+	0xa3, 0xfd, 0xa5, 0x67, 0xb4, 0x2c, 0xd2, 0xc8, 0x38, 0x15, 0xd8, 0xfe, 0xcf, 0x42, 0x68, 0x21,
+	0x74, 0x5e, 0xeb, 0xd7, 0xa0, 0xdc, 0x50, 0xc1, 0x64, 0x67, 0x6f, 0xcd, 0xd5, 0x67, 0x7a, 0x75,
+	0x8c, 0xdb, 0xae, 0x52, 0x8b, 0x60, 0x12, 0x5e, 0xa8, 0x41, 0xc1, 0x26, 0x37, 0xf4, 0x6d, 0x00,
+	0x65, 0xfa, 0x69, 0x73, 0xcd, 0xd5, 0x77, 0x5b, 0x6d, 0x1c, 0xde, 0xf7, 0x42, 0x14, 0xc5, 0x3a,
+	0x74, 0xb2, 0x22, 0x02, 0x36, 0x58, 0x89, 0x55, 0x07, 0x59, 0xdc, 0x9b, 0x9e, 0xaf, 0x8d, 0xc4,
+	0x58, 0xab, 0x5e, 0x89, 0x60, 0x92, 0xbe, 0x77, 0x44, 0xc1, 0x26, 0xb7, 0x45, 0x1f, 0x66, 0x93,
+	0xb2, 0x4a, 0xb9, 0xdf, 0x6e, 0xc7, 0xef, 0xb7, 0x4b, 0x23, 0x5a, 0x04, 0x23, 0x31, 0x60, 0xbe,
+	0x23, 0xf8, 0x70, 0x26, 0x21, 0xa3, 0x14, 0x96, 0x6b, 0x71, 0x96, 0x97, 0xb3, 0xdc, 0xf5, 0x3a,
+	0xdb, 0x6e, 0xf2, 0x64, 0x30, 0x9b, 0x94, 0xce, 0xb1, 0x31, 0x8d, 0xa5, 0xf8, 0xcd, 0x4b, 0xfc,
+	0xcf, 0x73, 0x50, 0x0a, 0xcd, 0x78, 0x96, 0x7c, 0x9d, 0x72, 0xbf, 0x72, 0x87, 0x04, 0xd1, 0xf9,
+	0x51, 0x82, 0xe8, 0x89, 0xe1, 0x41, 0x74, 0x90, 0xd3, 0x2f, 0x3e, 0x3e, 0xa7, 0x6f, 0x04, 0xd1,
+	0x93, 0xa3, 0x07, 0xd1, 0x53, 0x87, 0x07, 0xd1, 0xf6, 0x5f, 0x5a, 0x80, 0x06, 0x33, 0x26, 0x59,
+	0x04, 0x45, 0x92, 0x97, 0xeb, 0xeb, 0x59, 0xc3, 0xd7, 0xc3, 0xee, 0x58, 0xfb, 0xd3, 0x02, 0x9c,
+	0xb9, 0xe5, 0x8c, 0x9d, 0x7a, 0xe5, 0xf0, 0xb4, 0x42, 0xaa, 0x53, 0xed, 0xf8, 0x86, 0x96, 0x55,
+	0xed, 0xef, 0x35, 0x3d, 0xf4, 0xe9, 0x5a, 0x7a, 0xb7, 0x47, 0xc3, 0x49, 0x78, 0x18, 0xf4, 0xc8,
+	0x87, 0xe4, 0x2d, 0x98, 0x61, 0xdc, 0x77, 0x1a, 0x5c, 0x25, 0x77, 0xd9, 0x42, 0x59, 0xde, 0x5c,
+	0xe7, 0x74, 0xf7, 0x99, 0xba, 0x49, 0xc4, 0xf1, 0xbe, 0xa9, 0x39, 0xe3, 0x89, 0xcc, 0x39, 0xe3,
+	0x65, 0x28, 0x91, 0x4e, 0xc7, 0xfb, 0xf6, 0x26, 0x69, 0x31, 0x9d, 0xa5, 0x09, 0x4f, 0xcd, 0x4a,
+	0x40, 0xc0, 0x51, 0x1f, 0x54, 0x01, 0x70, 0x5a, 0xae, 0xe7, 0x53, 0x39, 0xa2, 0x28, 0xaf, 0x50,
+	0xf9, 0x2e, 0xb6, 0x16, 0xb6, 0x62, 0xa3, 0x07, 0xaa, 0xc3, 0x39, 0xc7, 0x65, 0xb4, 0xd1, 0xf7,
+	0x69, 0x7d, 0xc7, 0xe9, 0x6d, 0xae, 0xd7, 0xa5, 0x95, 0xd8, 0x93, 0xa7, 0x79, 0xaa, 0xfa, 0xac,
+	0x66, 0x76, 0x6e, 0x2d, 0xad, 0x13, 0x4e, 0x1f, 0x8b, 0x5e, 0x83, 0x69, 0xc7, 0x6d, 0x74, 0xfa,
+	0x4d, 0xba, 0x41, 0x78, 0x9b, 0x2d, 0x4c, 0xc9, 0x69, 0xcc, 0x1e, 0xec, 0x2f, 0x4d, 0xaf, 0x19,
+	0xed, 0x38, 0xd6, 0x4b, 0x8c, 0xa2, 0x0f, 0x8d, 0x51, 0xa5, 0x68, 0xd4, 0x8d, 0x87, 0xe6, 0x28,
+	0xb3, 0x57, 0x4a, 0x56, 0x1d, 0x32, 0x65, 0xd5, 0x7f, 0x98, 0x83, 0xa2, 0x7a, 0xd4, 0x42, 0x57,
+	0x12, 0x2f, 0x47, 0xcf, 0x0e, 0xbc, 0x1c, 0x95, 0xd3, 0x1e, 0x00, 0x6d, 0x28, 0x3a, 0x8c, 0xf5,
+	0xe3, 0x1e, 0xcb, 0x9a, 0x6c, 0xc1, 0x9a, 0x22, 0x33, 0x8e, 0x9e, 0xbb, 0xed, 0xb4, 0x74, 0x3a,
+	0xe6, 0xba, 0xe1, 0xa7, 0x44, 0xa5, 0x09, 0x0f, 0xc2, 0xda, 0x85, 0xc8, 0x65, 0x89, 0x75, 0x10,
+	0xbe, 0xcb, 0x9d, 0xfa, 0xdd, 0x77, 0x14, 0x8f, 0x9a, 0x44, 0xc4, 0x1a, 0x59, 0xf0, 0xf0, 0xfa,
+	0xbc, 0xd7, 0xe7, 0xf2, 0xa0, 0x1c, 0x13, 0x8f, 0xbb, 0x12, 0x11, 0x6b, 0x64, 0xfb, 0x13, 0x0b,
+	0xce, 0x28, 0x19, 0xd4, 0xda, 0xb4, 0xb1, 0x53, 0xe7, 0xb4, 0x27, 0x42, 0x88, 0x3e, 0xa3, 0x2c,
+	0x19, 0x42, 0xbc, 0xcb, 0x28, 0xc3, 0x92, 0x62, 0xac, 0x3e, 0x77, 0x52, 0xab, 0xb7, 0x0f, 0x72,
+	0x50, 0x90, 0xbe, 0x7a, 0x16, 0xfb, 0x13, 0x4f, 0xae, 0xe5, 0x46, 0x4a, 0xae, 0x1d, 0x92, 0x7f,
+	0x8d, 0x12, 0x8c, 0x13, 0x8f, 0x4d, 0x30, 0xb2, 0xb4, 0xfc, 0xe2, 0xdb, 0x19, 0x42, 0x94, 0x71,
+	0xea, 0x1b, 0x8e, 0x9a, 0xbf, 0xfb, 0xa9, 0x05, 0xf3, 0x69, 0x99, 0xf6, 0x2c, 0x32, 0x7f, 0x05,
+	0xa6, 0x7a, 0x1d, 0xc2, 0xb7, 0x3d, 0xbf, 0x9b, 0x7c, 0x61, 0xdd, 0xd0, 0xed, 0x38, 0xec, 0x81,
+	0x7c, 0x00, 0x3f, 0x08, 0x56, 0x83, 0x40, 0xee, 0xfa, 0xd1, 0xb2, 0xb0, 0xd1, 0x0e, 0x87, 0x4d,
+	0x0c, 0x1b, 0x5c, 0xec, 0x3f, 0x2c, 0xc0, 0x9c, 0x1c, 0x32, 0xee, 0xb5, 0x36, 0xce, 0xb1, 0xea,
+	0xc1, 0x53, 0x32, 0xc4, 0x1c, 0xbc, 0x09, 0xd5, 0x49, 0xbb, 0xaa, 0xc7, 0x3f, 0xb5, 0x96, 0xda,
+	0xeb, 0xd1, 0x50, 0x0a, 0x1e, 0x82, 0x3b, 0x78, 0xbd, 0xc1, 0xcf, 0xde, 0xf5, 0x66, 0x1e, 0xb6,
+	0xc9, 0x43, 0x0f, 0xdb, 0xd0, 0xcb, 0x70, 0xea, 0x08, 0x97, 0xe1, 0xe0, 0x05, 0x55, 0xca, 0x74,
+	0x41, 0xfd, 0x69, 0x0e, 0x26, 0x37, 0x7c, 0x4f, 0xbe, 0xd8, 0x9c, 0x7c, 0x5a, 0xfb, 0x6e, 0xec,
+	0xa5, 0xf7, 0xe2, 0xc8, 0x2f, 0xbd, 0x02, 0x4a, 0xbe, 0xf1, 0x4e, 0xc5, 0xdf, 0x77, 0x8d, 0xfc,
+	0x6c, 0x3e, 0x4b, 0xd8, 0x10, 0x40, 0x3e, 0x3e, 0x3f, 0xfb, 0xa9, 0x05, 0x65, 0xdd, 0xf3, 0x2b,
+	0x9b, 0x08, 0xd4, 0xf3, 0x1b, 0x92, 0x08, 0xfc, 0xa3, 0x68, 0x05, 0x42, 0x68, 0xe8, 0x37, 0x60,
+	0xae, 0x17, 0xbc, 0x2c, 0x6f, 0x78, 0x1d, 0xa7, 0xe1, 0xd0, 0x20, 0x97, 0x7c, 0x25, 0xe3, 0xb3,
+	0xbb, 0x1c, 0xbe, 0x57, 0xfd, 0x9a, 0xe6, 0x3b, 0xb7, 0x91, 0xc4, 0xc5, 0x83, 0xac, 0xec, 0x7f,
+	0xb3, 0x60, 0x26, 0x26, 0x7b, 0xd4, 0x00, 0x68, 0x78, 0x6e, 0xd3, 0xe1, 0x61, 0x91, 0x4b, 0xf9,
+	0xd2, 0xf2, 0x68, 0x52, 0xad, 0x05, 0xe3, 0xa2, 0x43, 0x17, 0x36, 0x31, 0x6c, 0xc0, 0xa2, 0xcb,
+	0x41, 0x45, 0x5a, 0xdc, 0xf3, 0x52, 0x15, 0x69, 0x8f, 0xf6, 0x97, 0xa6, 0xf5, 0x9c, 0xcc, 0x0a,
+	0xb5, 0x2c, 0x95, 0x57, 0x7f, 0x95, 0x83, 0x52, 0xb8, 0xfe, 0x27, 0xa0, 0x46, 0xef, 0xc6, 0xd4,
+	0xe8, 0x72, 0xc6, 0x9d, 0x1b, 0x56, 0x2c, 0x81, 0x3e, 0x4a, 0x28, 0x53, 0xd6, 0x23, 0x71, 0x88,
+	0x3a, 0xfd, 0x9d, 0xda, 0x7c, 0xd5, 0xf7, 0x09, 0x28, 0xd4, 0x66, 0x5c, 0xa1, 0x96, 0x33, 0xae,
+	0x66, 0x88, 0x4a, 0x7d, 0xd7, 0x82, 0x33, 0x09, 0x25, 0x40, 0xcf, 0x41, 0x41, 0xe6, 0x0e, 0xf5,
+	0xf9, 0x0a, 0x07, 0xea, 0xac, 0x84, 0xa4, 0xa1, 0x0d, 0x98, 0x27, 0x7d, 0xee, 0x85, 0x63, 0x6f,
+	0xb8, 0x64, 0xab, 0x43, 0x55, 0xaa, 0x61, 0xaa, 0xfa, 0x73, 0x61, 0x92, 0x2f, 0xa5, 0x0f, 0x4e,
+	0x1d, 0x69, 0xff, 0x28, 0x07, 0x28, 0x6c, 0xcc, 0x92, 0x74, 0xff, 0x08, 0x26, 0xb7, 0x55, 0xf6,
+	0xea, 0x68, 0xaf, 0x26, 0xd5, 0xb2, 0xf9, 0x70, 0x14, 0x60, 0xa2, 0xf7, 0x8f, 0xe7, 0x1c, 0xc1,
+	0xe0, 0x19, 0x42, 0xf7, 0x01, 0xb6, 0x1d, 0xd7, 0x61, 0xed, 0x31, 0x1f, 0x9f, 0xe5, 0x6d, 0x7e,
+	0x33, 0x44, 0xc0, 0x06, 0x9a, 0xfd, 0x27, 0x39, 0xe3, 0x7c, 0x4a, 0x73, 0x39, 0xd2, 0xbe, 0xbe,
+	0x14, 0x17, 0x66, 0x69, 0xf0, 0x45, 0x2d, 0x14, 0xcc, 0x7d, 0x98, 0xd8, 0x25, 0x7e, 0x90, 0xdc,
+	0x1f, 0xb1, 0xae, 0x65, 0xf0, 0x49, 0x3b, 0xda, 0xd3, 0x7b, 0xc4, 0x67, 0x58, 0x62, 0xa2, 0x6f,
+	0x8a, 0xb9, 0xd2, 0x5e, 0x60, 0x43, 0x33, 0x1b, 0x05, 0x4e, 0x7b, 0xe6, 0x02, 0x69, 0x8f, 0x61,
+	0x05, 0x68, 0x7f, 0x7f, 0xd2, 0x38, 0xf1, 0xda, 0x6c, 0xdf, 0x01, 0xd4, 0x21, 0x8c, 0xdf, 0x26,
+	0x6e, 0x53, 0x9c, 0x4f, 0xba, 0xed, 0x53, 0xd6, 0xd6, 0x2e, 0xda, 0xa2, 0x46, 0x41, 0xeb, 0x03,
+	0x3d, 0x70, 0xca, 0x28, 0x74, 0x25, 0x6e, 0x9d, 0x97, 0x92, 0xd6, 0xf9, 0x74, 0xa4, 0x6e, 0xe3,
+	0xd9, 0x67, 0xf3, 0xbc, 0x17, 0x4e, 0xe0, 0xbc, 0xff, 0x3a, 0xcc, 0x6d, 0x27, 0x9f, 0x58, 0x75,
+	0x59, 0xca, 0x1b, 0x63, 0xbe, 0xd0, 0x56, 0xcf, 0x1d, 0x44, 0xef, 0x72, 0x51, 0x33, 0x1e, 0x64,
+	0x84, 0xbc, 0xa0, 0x1e, 0x57, 0xc6, 0xcc, 0x2a, 0x1d, 0x32, 0xb2, 0xce, 0x25, 0xa2, 0xed, 0x64,
+	0x25, 0xae, 0x82, 0xc4, 0x31, 0x06, 0x09, 0x1d, 0x2c, 0x1e, 0xa7, 0x0e, 0xa2, 0x2b, 0xe1, 0x33,
+	0x84, 0x98, 0x8e, 0xf4, 0x65, 0xf3, 0x03, 0x0f, 0x08, 0x82, 0x84, 0xcd, 0x7e, 0xe8, 0x7b, 0x16,
+	0x9c, 0x13, 0x87, 0xf5, 0xc6, 0x43, 0xda, 0xe8, 0x0b, 0xa9, 0x04, 0x65, 0xfa, 0x0b, 0x65, 0x29,
+	0x8d, 0x11, 0xab, 0x93, 0xeb, 0x69, 0x10, 0x91, 0x63, 0x9e, 0x4a, 0xc6, 0xe9, 0x8c, 0xd1, 0x03,
+	0x69, 0x3b, 0x38, 0x95, 0x71, 0xcf, 0xd1, 0x93, 0x12, 0x25, 0x6d, 0x77, 0xb8, 0xb2, 0x3b, 0x9c,
+	0xda, 0x3f, 0xc9, 0x9b, 0xe6, 0x6a, 0xb4, 0x54, 0xc9, 0x7d, 0x98, 0xe0, 0x84, 0xed, 0x68, 0x2d,
+	0x78, 0x7b, 0x8c, 0x4a, 0xcb, 0x48, 0x17, 0xa4, 0x2b, 0x2e, 0x9b, 0x24, 0x26, 0x5a, 0x84, 0x1c,
+	0x61, 0xc9, 0xc4, 0xf9, 0x0a, 0xc3, 0x39, 0xc2, 0x64, 0x52, 0x7d, 0x5b, 0x87, 0x48, 0x51, 0x52,
+	0x7d, 0x1b, 0xe7, 0x9c, 0x6d, 0xf4, 0x3e, 0x14, 0x7c, 0xca, 0xfd, 0x3d, 0x6d, 0xcd, 0xaf, 0x8e,
+	0x61, 0xb8, 0xb0, 0x18, 0xaf, 0x44, 0x24, 0xff, 0xc4, 0x0a, 0x31, 0xb4, 0xb7, 0xc5, 0x13, 0xb0,
+	0xb7, 0x51, 0xd6, 0x29, 0x7f, 0x62, 0x59, 0xa7, 0x1f, 0x5a, 0xc6, 0x05, 0x1f, 0x2e, 0x14, 0xbd,
+	0x0b, 0x93, 0xdc, 0xe9, 0x52, 0xaf, 0xcf, 0xb3, 0x79, 0x4d, 0xe1, 0xbb, 0xa4, 0x34, 0x63, 0x9b,
+	0x0a, 0x02, 0x07, 0x58, 0x22, 0x94, 0xa4, 0xbe, 0xef, 0xf9, 0x9b, 0x6d, 0x61, 0x96, 0xbd, 0x8e,
+	0x72, 0x4d, 0x66, 0xa2, 0x50, 0xf2, 0x46, 0x8c, 0x8a, 0x13, 0xbd, 0xed, 0x1f, 0x99, 0xfe, 0xdd,
+	0xff, 0xff, 0xd2, 0xe1, 0x7f, 0xb4, 0x60, 0xee, 0x49, 0xd7, 0x0c, 0x7f, 0x33, 0xee, 0xb2, 0x5e,
+	0x1e, 0x63, 0x3d, 0x43, 0xdc, 0xd6, 0x0f, 0xe1, 0xa9, 0x74, 0x3d, 0x3e, 0x96, 0x8f, 0x6d, 0xfe,
+	0x3e, 0x29, 0x2b, 0xe9, 0x3e, 0x05, 0xea, 0x67, 0x9d, 0xa4, 0xbb, 0x93, 0x3b, 0x6e, 0x77, 0xc7,
+	0x37, 0x97, 0xa2, 0x3f, 0x4d, 0x42, 0x1f, 0xe9, 0x73, 0x66, 0x65, 0xf9, 0xd8, 0x65, 0x00, 0x66,
+	0xe8, 0x59, 0xfb, 0x27, 0x0b, 0xce, 0xa5, 0xf6, 0x0e, 0x65, 0x98, 0x3b, 0x49, 0x19, 0x5a, 0xc7,
+	0x2d, 0xc3, 0x1f, 0xe4, 0x60, 0x16, 0xd3, 0x9e, 0x17, 0x4b, 0x71, 0x6e, 0x04, 0x45, 0xdd, 0x19,
+	0x62, 0x82, 0xc4, 0xeb, 0x5f, 0x75, 0x32, 0x56, 0xcd, 0x2d, 0xd4, 0xa5, 0x1b, 0x38, 0x80, 0x23,
+	0xab, 0xff, 0x40, 0xf2, 0x55, 0xdd, 0x1c, 0x2a, 0x8d, 0xab, 0x00, 0x05, 0xb2, 0xac, 0x9e, 0xd1,
+	0xc6, 0xfd, 0x8d, 0x0c, 0x75, 0x38, 0x83, 0xc8, 0xb2, 0x19, 0x2b, 0x40, 0xfb, 0x93, 0x1c, 0xa8,
+	0xf8, 0xe1, 0x09, 0x58, 0xc7, 0x5f, 0x8e, 0x59, 0xc7, 0xe5, 0x51, 0x9d, 0x20, 0x21, 0x9e, 0x61,
+	0x39, 0x82, 0x64, 0x6c, 0x77, 0x31, 0x0b, 0xe8, 0xe3, 0xf3, 0x03, 0x7f, 0x6b, 0x41, 0x49, 0xf6,
+	0x7b, 0x02, 0x86, 0x76, 0x23, 0x6e, 0x68, 0x5f, 0xce, 0xb0, 0x8a, 0x21, 0x06, 0xf6, 0xbf, 0xf3,
+	0x7a, 0xf6, 0x61, 0xe4, 0xd8, 0x26, 0x7e, 0x53, 0x87, 0x44, 0x91, 0x96, 0x88, 0x46, 0xac, 0x68,
+	0xa1, 0x6e, 0x4f, 0x9e, 0x80, 0x6e, 0xff, 0xaa, 0x2a, 0x62, 0xa2, 0x8c, 0xd3, 0xe6, 0xcd, 0x30,
+	0xf6, 0xc9, 0x67, 0xae, 0xc6, 0xd2, 0x15, 0x63, 0x51, 0xde, 0x1d, 0x27, 0x50, 0xf1, 0x00, 0x1f,
+	0x11, 0x0f, 0xf5, 0x92, 0xc6, 0x4c, 0xc7, 0x09, 0x6f, 0x8c, 0x69, 0x39, 0x55, 0x3c, 0x34, 0xd0,
+	0x8c, 0x07, 0x19, 0xa1, 0x36, 0x4c, 0x9b, 0x75, 0xa4, 0xfa, 0x9c, 0x5e, 0xca, 0x5e, 0xb0, 0xaa,
+	0x1e, 0x87, 0xcd, 0x16, 0x1c, 0x43, 0xb6, 0xf7, 0x8b, 0x50, 0x36, 0x0e, 0x76, 0x22, 0x97, 0x39,
+	0x73, 0x32, 0xb9, 0xcc, 0xf4, 0xc8, 0xbb, 0x3c, 0x56, 0xe4, 0x7d, 0x31, 0x1e, 0x79, 0x3f, 0x93,
+	0x8c, 0xbc, 0x41, 0xae, 0x2e, 0x16, 0x75, 0x33, 0x38, 0xad, 0x43, 0xd0, 0xa0, 0x20, 0x58, 0x25,
+	0x33, 0xc6, 0x0e, 0x74, 0x91, 0xf0, 0x2c, 0x6f, 0xc6, 0x20, 0x71, 0x82, 0x85, 0xf0, 0x4c, 0x75,
+	0x4b, 0xbd, 0xdf, 0xed, 0x12, 0x7f, 0x6f, 0x61, 0x5a, 0x4e, 0x38, 0xf4, 0x4c, 0x6f, 0xc6, 0xa8,
+	0x38, 0xd1, 0x1b, 0x6d, 0x40, 0x51, 0x45, 0xb0, 0xba, 0xc8, 0xf4, 0x95, 0x2c, 0xc1, 0xb1, 0xf2,
+	0xcc, 0xd5, 0xdf, 0x58, 0xe3, 0x98, 0xc9, 0x87, 0xd2, 0x21, 0xc9, 0x87, 0x3b, 0x80, 0xbc, 0x2d,
+	0x19, 0x03, 0x34, 0x6f, 0xa9, 0x2f, 0xef, 0xc5, 0xa9, 0x2c, 0xca, 0xc8, 0x36, 0xdc, 0xb0, 0xbb,
+	0x03, 0x3d, 0x70, 0xca, 0x28, 0xa1, 0xd5, 0x3a, 0xec, 0x0d, 0x55, 0x41, 0x27, 0x1a, 0xb2, 0x86,
+	0x4d, 0x51, 0x1c, 0x27, 0x8b, 0x14, 0x6b, 0x09, 0x54, 0x3c, 0xc0, 0x07, 0x7d, 0x0c, 0x33, 0xe2,
+	0x08, 0x45, 0x8c, 0xe1, 0x88, 0x8c, 0xe7, 0x0e, 0xf6, 0x97, 0x66, 0xd6, 0x4d, 0x48, 0x1c, 0xe7,
+	0x60, 0xff, 0x41, 0x1e, 0xd2, 0x83, 0xee, 0xe8, 0xfb, 0x08, 0xeb, 0x31, 0xdf, 0x47, 0xbc, 0x07,
+	0x25, 0xc6, 0x89, 0xaf, 0x3e, 0x54, 0xc9, 0x8d, 0xf7, 0xa1, 0x4a, 0x3d, 0x00, 0xc0, 0x11, 0x56,
+	0x22, 0x03, 0x92, 0x3f, 0xd6, 0x0c, 0xc8, 0x25, 0x00, 0x19, 0x57, 0xd5, 0xbc, 0xbe, 0x7e, 0xf4,
+	0x9c, 0x89, 0x6c, 0xc2, 0x8d, 0x90, 0x82, 0x8d, 0x5e, 0xe8, 0x6a, 0x78, 0x29, 0xab, 0x57, 0xce,
+	0x0b, 0x03, 0xa5, 0x25, 0xc9, 0x1c, 0x5a, 0xca, 0xe7, 0xe5, 0x87, 0x94, 0xa2, 0xd9, 0xff, 0x9b,
+	0x83, 0x98, 0x31, 0x44, 0xdf, 0xb5, 0x60, 0x8e, 0x24, 0xbe, 0xe1, 0x0f, 0x7c, 0xc9, 0x5f, 0xca,
+	0xf6, 0xc3, 0x0a, 0x03, 0x3f, 0x01, 0x10, 0xbd, 0x2b, 0x25, 0xbb, 0x30, 0x3c, 0xc8, 0x14, 0xfd,
+	0xae, 0x05, 0x67, 0xc9, 0xe0, 0x8f, 0x34, 0xe8, 0x4d, 0x7f, 0x73, 0xec, 0x5f, 0x79, 0xa8, 0x3e,
+	0x7d, 0xb0, 0xbf, 0x94, 0xf6, 0xf3, 0x15, 0x38, 0x8d, 0x1d, 0xfa, 0x00, 0x26, 0x88, 0xdf, 0x0a,
+	0x52, 0xb0, 0xd9, 0xd9, 0x06, 0xbf, 0xbd, 0x11, 0xdd, 0xe8, 0x2b, 0x7e, 0x8b, 0x61, 0x09, 0x6a,
+	0xff, 0x38, 0x0f, 0xb3, 0xc9, 0xef, 0x29, 0x74, 0xb5, 0xe2, 0x44, 0x6a, 0xb5, 0xa2, 0xd0, 0x91,
+	0x06, 0x0f, 0x4b, 0x07, 0x23, 0x1d, 0x11, 0x8d, 0x58, 0xd1, 0x42, 0x1d, 0x91, 0x55, 0xce, 0x85,
+	0x23, 0xe8, 0x88, 0x2c, 0x6d, 0x8e, 0xb0, 0xd0, 0xd5, 0xf8, 0xdd, 0x62, 0x27, 0xef, 0x96, 0x39,
+	0x73, 0x2d, 0xe3, 0x26, 0x76, 0xbb, 0x50, 0x36, 0xf6, 0x41, 0x6b, 0xe2, 0xb5, 0xcc, 0x72, 0x8f,
+	0x8e, 0xdd, 0x19, 0x55, 0xe0, 0x12, 0x51, 0x4c, 0xfc, 0x48, 0xef, 0xa5, 0xb4, 0x8e, 0x94, 0xf9,
+	0x94, 0xe2, 0x32, 0xd0, 0xec, 0x7f, 0xb7, 0x60, 0x26, 0x56, 0x42, 0x2b, 0xb8, 0x05, 0xa5, 0xca,
+	0xe3, 0xff, 0x60, 0xc5, 0xbd, 0x10, 0x01, 0x1b, 0x68, 0xe8, 0x5b, 0x50, 0xee, 0x78, 0x6e, 0x8b,
+	0x32, 0x5e, 0xf7, 0xc8, 0x8e, 0xd6, 0x93, 0xac, 0x69, 0xa4, 0x85, 0x83, 0xfd, 0xa5, 0xf9, 0x75,
+	0x05, 0x53, 0xf3, 0xba, 0xbd, 0x0e, 0xe5, 0xaa, 0xa8, 0x1d, 0x9b, 0xe0, 0xf2, 0x75, 0xf4, 0x3d,
+	0xe2, 0xd3, 0xb6, 0xd7, 0x67, 0xf4, 0xab, 0xfa, 0x3a, 0x1a, 0x4e, 0xf0, 0xb8, 0x5f, 0x47, 0x23,
+	0xe0, 0xc3, 0x5f, 0x47, 0xc3, 0xbe, 0x5f, 0xd9, 0xd7, 0xd1, 0x70, 0x86, 0x43, 0xa2, 0xa0, 0xff,
+	0xc9, 0x19, 0xab, 0x88, 0x47, 0x42, 0xb9, 0xc7, 0x44, 0x42, 0x1f, 0xc2, 0x94, 0xe3, 0x72, 0xea,
+	0xef, 0x92, 0x8e, 0x4e, 0x03, 0x67, 0x3d, 0x8b, 0xe1, 0x52, 0xd7, 0x34, 0x0e, 0x0e, 0x11, 0x51,
+	0x07, 0xce, 0x05, 0xcf, 0x26, 0x3e, 0x25, 0xd1, 0xbb, 0xad, 0xae, 0x7d, 0x7a, 0x3d, 0xc8, 0xef,
+	0xdf, 0x4c, 0xeb, 0xf4, 0x68, 0x18, 0x01, 0xa7, 0x83, 0x22, 0x06, 0x33, 0xcc, 0x48, 0x01, 0x04,
+	0x37, 0xe2, 0x88, 0x4f, 0x4e, 0xc9, 0xac, 0x89, 0x51, 0x30, 0x65, 0x82, 0xe2, 0x38, 0x0f, 0xfb,
+	0x5f, 0xf2, 0x70, 0x26, 0x71, 0xd2, 0x12, 0xe1, 0x48, 0xe9, 0x49, 0x86, 0x23, 0xc5, 0xb1, 0xc2,
+	0x91, 0x74, 0x4f, 0x79, 0x62, 0x2c, 0x4f, 0xf9, 0x2d, 0xe5, 0xad, 0xea, 0x9d, 0x5b, 0x5b, 0xd5,
+	0x45, 0xf1, 0xa1, 0x34, 0xd7, 0x4d, 0x22, 0x8e, 0xf7, 0x95, 0xee, 0x44, 0x73, 0xf0, 0xc7, 0x0f,
+	0xb4, 0xab, 0xfd, 0x66, 0xd6, 0x02, 0xc1, 0x10, 0x40, 0xb9, 0x13, 0x29, 0x04, 0x9c, 0xc6, 0xae,
+	0x7a, 0xe7, 0xb3, 0x2f, 0xcf, 0x9f, 0xfa, 0xfc, 0xcb, 0xf3, 0xa7, 0xbe, 0xf8, 0xf2, 0xfc, 0xa9,
+	0xef, 0x1c, 0x9c, 0xb7, 0x3e, 0x3b, 0x38, 0x6f, 0x7d, 0x7e, 0x70, 0xde, 0xfa, 0xe2, 0xe0, 0xbc,
+	0xf5, 0x93, 0x83, 0xf3, 0xd6, 0xf7, 0x7e, 0x7a, 0xfe, 0xd4, 0xfd, 0xe7, 0x47, 0xf9, 0xc9, 0xb4,
+	0xff, 0x0b, 0x00, 0x00, 0xff, 0xff, 0x28, 0x9c, 0x2e, 0x6d, 0x59, 0x4d, 0x00, 0x00,
 }
 
 func (m *AnalysisRunArgument) Marshal() (dAtA []byte, err error) {
@@ -2997,6 +2998,39 @@ func (m *DiscoveredImageReference) MarshalToSizedBuffer(dAtA []byte) (int, error
 	return len(dAtA) - i, nil
 }
 
+func (m *ExpressionVariable) Marshal() (dAtA []byte, err error) {
+	size := m.Size()
+	dAtA = make([]byte, size)
+	n, err := m.MarshalToSizedBuffer(dAtA[:size])
+	if err != nil {
+		return nil, err
+	}
+	return dAtA[:n], nil
+}
+
+func (m *ExpressionVariable) MarshalTo(dAtA []byte) (int, error) {
+	size := m.Size()
+	return m.MarshalToSizedBuffer(dAtA[:size])
+}
+
+func (m *ExpressionVariable) MarshalToSizedBuffer(dAtA []byte) (int, error) {
+	i := len(dAtA)
+	_ = i
+	var l int
+	_ = l
+	i -= len(m.Value)
+	copy(dAtA[i:], m.Value)
+	i = encodeVarintGenerated(dAtA, i, uint64(len(m.Value)))
+	i--
+	dAtA[i] = 0x12
+	i -= len(m.Name)
+	copy(dAtA[i:], m.Name)
+	i = encodeVarintGenerated(dAtA, i, uint64(len(m.Name)))
+	i--
+	dAtA[i] = 0xa
+	return len(dAtA) - i, nil
+}
+
 func (m *Freight) Marshal() (dAtA []byte, err error) {
 	size := m.Size()
 	dAtA = make([]byte, size)
@@ -4976,39 +5010,6 @@ func (m *PromotionTemplateSpec) MarshalToSizedBuffer(dAtA []byte) (int, error) {
 	return len(dAtA) - i, nil
 }
 
-func (m *PromotionVariable) Marshal() (dAtA []byte, err error) {
-	size := m.Size()
-	dAtA = make([]byte, size)
-	n, err := m.MarshalToSizedBuffer(dAtA[:size])
-	if err != nil {
-		return nil, err
-	}
-	return dAtA[:n], nil
-}
-
-func (m *PromotionVariable) MarshalTo(dAtA []byte) (int, error) {
-	size := m.Size()
-	return m.MarshalToSizedBuffer(dAtA[:size])
-}
-
-func (m *PromotionVariable) MarshalToSizedBuffer(dAtA []byte) (int, error) {
-	i := len(dAtA)
-	_ = i
-	var l int
-	_ = l
-	i -= len(m.Value)
-	copy(dAtA[i:], m.Value)
-	i = encodeVarintGenerated(dAtA, i, uint64(len(m.Value)))
-	i--
-	dAtA[i] = 0x12
-	i -= len(m.Name)
-	copy(dAtA[i:], m.Name)
-	i = encodeVarintGenerated(dAtA, i, uint64(len(m.Name)))
-	i--
-	dAtA[i] = 0xa
-	return len(dAtA) - i, nil
-}
-
 func (m *RepoSubscription) Marshal() (dAtA []byte, err error) {
 	size := m.Size()
 	dAtA = make([]byte, size)
@@ -5188,6 +5189,20 @@ func (m *StageSpec) MarshalToSizedBuffer(dAtA []byte) (int, error) {
 	_ = i
 	var l int
 	_ = l
+	if len(m.Vars) > 0 {
+		for iNdEx := len(m.Vars) - 1; iNdEx >= 0; iNdEx-- {
+			{
+				size, err := m.Vars[iNdEx].MarshalToSizedBuffer(dAtA[:i])
+				if err != nil {
+					return 0, err
+				}
+				i -= size
+				i = encodeVarintGenerated(dAtA, i, uint64(size))
+			}
+			i--
+			dAtA[i] = 0x3a
+		}
+	}
 	if m.PromotionTemplate != nil {
 		{
 			size, err := m.PromotionTemplate.MarshalToSizedBuffer(dAtA[:i])
@@ -6132,6 +6147,19 @@ func (m *DiscoveredImageReference) Size() (n int) {
 	return n
 }
 
+func (m *ExpressionVariable) Size() (n int) {
+	if m == nil {
+		return 0
+	}
+	var l int
+	_ = l
+	l = len(m.Name)
+	n += 1 + l + sovGenerated(uint64(l))
+	l = len(m.Value)
+	n += 1 + l + sovGenerated(uint64(l))
+	return n
+}
+
 func (m *Freight) Size() (n int) {
 	if m == nil {
 		return 0
@@ -6866,19 +6894,6 @@ func (m *PromotionTemplateSpec) Size() (n int) {
 	return n
 }
 
-func (m *PromotionVariable) Size() (n int) {
-	if m == nil {
-		return 0
-	}
-	var l int
-	_ = l
-	l = len(m.Name)
-	n += 1 + l + sovGenerated(uint64(l))
-	l = len(m.Value)
-	n += 1 + l + sovGenerated(uint64(l))
-	return n
-}
-
 func (m *RepoSubscription) Size() (n int) {
 	if m == nil {
 		return 0
@@ -6954,6 +6969,12 @@ func (m *StageSpec) Size() (n int) {
 		l = m.PromotionTemplate.Size()
 		n += 1 + l + sovGenerated(uint64(l))
 	}
+	if len(m.Vars) > 0 {
+		for _, e := range m.Vars {
+			l = e.Size()
+			n += 1 + l + sovGenerated(uint64(l))
+		}
+	}
 	return n
 }
 
@@ -7431,6 +7452,17 @@ func (this *DiscoveredImageReference) String() string {
 	}, "")
 	return s
 }
+func (this *ExpressionVariable) String() string {
+	if this == nil {
+		return "nil"
+	}
+	s := strings.Join([]string{`&ExpressionVariable{`,
+		`Name:` + fmt.Sprintf("%v", this.Name) + `,`,
+		`Value:` + fmt.Sprintf("%v", this.Value) + `,`,
+		`}`,
+	}, "")
+	return s
+}
 func (this *Freight) String() string {
 	if this == nil {
 		return "nil"
@@ -7857,9 +7889,9 @@ func (this *PromotionSpec) String() string {
 		repeatedStringForSteps += strings.Replace(strings.Replace(f.String(), "PromotionStep", "PromotionStep", 1), `&`, ``, 1) + ","
 	}
 	repeatedStringForSteps += "}"
-	repeatedStringForVars := "[]PromotionVariable{"
+	repeatedStringForVars := "[]ExpressionVariable{"
 	for _, f := range this.Vars {
-		repeatedStringForVars += strings.Replace(strings.Replace(f.String(), "PromotionVariable", "PromotionVariable", 1), `&`, ``, 1) + ","
+		repeatedStringForVars += strings.Replace(strings.Replace(f.String(), "ExpressionVariable", "ExpressionVariable", 1), `&`, ``, 1) + ","
 	}
 	repeatedStringForVars += "}"
 	s := strings.Join([]string{`&PromotionSpec{`,
@@ -7904,9 +7936,9 @@ func (this *PromotionStep) String() string {
 	if this == nil {
 		return "nil"
 	}
-	repeatedStringForVars := "[]PromotionVariable{"
+	repeatedStringForVars := "[]ExpressionVariable{"
 	for _, f := range this.Vars {
-		repeatedStringForVars += strings.Replace(strings.Replace(f.String(), "PromotionVariable", "PromotionVariable", 1), `&`, ``, 1) + ","
+		repeatedStringForVars += strings.Replace(strings.Replace(f.String(), "ExpressionVariable", "ExpressionVariable", 1), `&`, ``, 1) + ","
 	}
 	repeatedStringForVars += "}"
 	s := strings.Join([]string{`&PromotionStep{`,
@@ -7974,9 +8006,9 @@ func (this *PromotionTaskSpec) String() string {
 	if this == nil {
 		return "nil"
 	}
-	repeatedStringForVars := "[]PromotionVariable{"
+	repeatedStringForVars := "[]ExpressionVariable{"
 	for _, f := range this.Vars {
-		repeatedStringForVars += strings.Replace(strings.Replace(f.String(), "PromotionVariable", "PromotionVariable", 1), `&`, ``, 1) + ","
+		repeatedStringForVars += strings.Replace(strings.Replace(f.String(), "ExpressionVariable", "ExpressionVariable", 1), `&`, ``, 1) + ","
 	}
 	repeatedStringForVars += "}"
 	repeatedStringForSteps := "[]PromotionStep{"
@@ -8010,9 +8042,9 @@ func (this *PromotionTemplateSpec) String() string {
 		repeatedStringForSteps += strings.Replace(strings.Replace(f.String(), "PromotionStep", "PromotionStep", 1), `&`, ``, 1) + ","
 	}
 	repeatedStringForSteps += "}"
-	repeatedStringForVars := "[]PromotionVariable{"
+	repeatedStringForVars := "[]ExpressionVariable{"
 	for _, f := range this.Vars {
-		repeatedStringForVars += strings.Replace(strings.Replace(f.String(), "PromotionVariable", "PromotionVariable", 1), `&`, ``, 1) + ","
+		repeatedStringForVars += strings.Replace(strings.Replace(f.String(), "ExpressionVariable", "ExpressionVariable", 1), `&`, ``, 1) + ","
 	}
 	repeatedStringForVars += "}"
 	s := strings.Join([]string{`&PromotionTemplateSpec{`,
@@ -8022,17 +8054,6 @@ func (this *PromotionTemplateSpec) String() string {
 	}, "")
 	return s
 }
-func (this *PromotionVariable) String() string {
-	if this == nil {
-		return "nil"
-	}
-	s := strings.Join([]string{`&PromotionVariable{`,
-		`Name:` + fmt.Sprintf("%v", this.Name) + `,`,
-		`Value:` + fmt.Sprintf("%v", this.Value) + `,`,
-		`}`,
-	}, "")
-	return s
-}
 func (this *RepoSubscription) String() string {
 	if this == nil {
 		return "nil"
@@ -8082,11 +8103,17 @@ func (this *StageSpec) String() string {
 		repeatedStringForRequestedFreight += strings.Replace(strings.Replace(f.String(), "FreightRequest", "FreightRequest", 1), `&`, ``, 1) + ","
 	}
 	repeatedStringForRequestedFreight += "}"
+	repeatedStringForVars := "[]ExpressionVariable{"
+	for _, f := range this.Vars {
+		repeatedStringForVars += strings.Replace(strings.Replace(f.String(), "ExpressionVariable", "ExpressionVariable", 1), `&`, ``, 1) + ","
+	}
+	repeatedStringForVars += "}"
 	s := strings.Join([]string{`&StageSpec{`,
 		`Verification:` + strings.Replace(this.Verification.String(), "Verification", "Verification", 1) + `,`,
 		`Shard:` + fmt.Sprintf("%v", this.Shard) + `,`,
 		`RequestedFreight:` + repeatedStringForRequestedFreight + `,`,
 		`PromotionTemplate:` + strings.Replace(this.PromotionTemplate.String(), "PromotionTemplate", "PromotionTemplate", 1) + `,`,
+		`Vars:` + repeatedStringForVars + `,`,
 		`}`,
 	}, "")
 	return s
@@ -11041,6 +11068,120 @@ func (m *DiscoveredImageReference) Unmarshal(dAtA []byte) error {
 	}
 	return nil
 }
+func (m *ExpressionVariable) Unmarshal(dAtA []byte) error {
+	l := len(dAtA)
+	iNdEx := 0
+	for iNdEx < l {
+		preIndex := iNdEx
+		var wire uint64
+		for shift := uint(0); ; shift += 7 {
+			if shift >= 64 {
+				return ErrIntOverflowGenerated
+			}
+			if iNdEx >= l {
+				return io.ErrUnexpectedEOF
+			}
+			b := dAtA[iNdEx]
+			iNdEx++
+			wire |= uint64(b&0x7F) << shift
+			if b < 0x80 {
+				break
+			}
+		}
+		fieldNum := int32(wire >> 3)
+		wireType := int(wire & 0x7)
+		if wireType == 4 {
+			return fmt.Errorf("proto: ExpressionVariable: wiretype end group for non-group")
+		}
+		if fieldNum <= 0 {
+			return fmt.Errorf("proto: ExpressionVariable: illegal tag %d (wire type %d)", fieldNum, wire)
+		}
+		switch fieldNum {
+		case 1:
+			if wireType != 2 {
+				return fmt.Errorf("proto: wrong wireType = %d for field Name", wireType)
+			}
+			var stringLen uint64
+			for shift := uint(0); ; shift += 7 {
+				if shift >= 64 {
+					return ErrIntOverflowGenerated
+				}
+				if iNdEx >= l {
+					return io.ErrUnexpectedEOF
+				}
+				b := dAtA[iNdEx]
+				iNdEx++
+				stringLen |= uint64(b&0x7F) << shift
+				if b < 0x80 {
+					break
+				}
+			}
+			intStringLen := int(stringLen)
+			if intStringLen < 0 {
+				return ErrInvalidLengthGenerated
+			}
+			postIndex := iNdEx + intStringLen
+			if postIndex < 0 {
+				return ErrInvalidLengthGenerated
+			}
+			if postIndex > l {
+				return io.ErrUnexpectedEOF
+			}
+			m.Name = string(dAtA[iNdEx:postIndex])
+			iNdEx = postIndex
+		case 2:
+			if wireType != 2 {
+				return fmt.Errorf("proto: wrong wireType = %d for field Value", wireType)
+			}
+			var stringLen uint64
+			for shift := uint(0); ; shift += 7 {
+				if shift >= 64 {
+					return ErrIntOverflowGenerated
+				}
+				if iNdEx >= l {
+					return io.ErrUnexpectedEOF
+				}
+				b := dAtA[iNdEx]
+				iNdEx++
+				stringLen |= uint64(b&0x7F) << shift
+				if b < 0x80 {
+					break
+				}
+			}
+			intStringLen := int(stringLen)
+			if intStringLen < 0 {
+				return ErrInvalidLengthGenerated
+			}
+			postIndex := iNdEx + intStringLen
+			if postIndex < 0 {
+				return ErrInvalidLengthGenerated
+			}
+			if postIndex > l {
+				return io.ErrUnexpectedEOF
+			}
+			m.Value = string(dAtA[iNdEx:postIndex])
+			iNdEx = postIndex
+		default:
+			iNdEx = preIndex
+			skippy, err := skipGenerated(dAtA[iNdEx:])
+			if err != nil {
+				return err
+			}
+			if (skippy < 0) || (iNdEx+skippy) < 0 {
+				return ErrInvalidLengthGenerated
+			}
+			if (iNdEx + skippy) > l {
+				return io.ErrUnexpectedEOF
+			}
+			iNdEx += skippy
+		}
+	}
+
+	if iNdEx > l {
+		return io.ErrUnexpectedEOF
+	}
+	return nil
+}
 func (m *Freight) Unmarshal(dAtA []byte) error {
 	l := len(dAtA)
 	iNdEx := 0
@@ -15800,7 +15941,7 @@ func (m *PromotionSpec) Unmarshal(dAtA []byte) error {
 			if postIndex > l {
 				return io.ErrUnexpectedEOF
 			}
-			m.Vars = append(m.Vars, PromotionVariable{})
+			m.Vars = append(m.Vars, ExpressionVariable{})
 			if err := m.Vars[len(m.Vars)-1].Unmarshal(dAtA[iNdEx:postIndex]); err != nil {
 				return err
 			}
@@ -16433,7 +16574,7 @@ func (m *PromotionStep) Unmarshal(dAtA []byte) error {
 			if postIndex > l {
 				return io.ErrUnexpectedEOF
 			}
-			m.Vars = append(m.Vars, PromotionVariable{})
+			m.Vars = append(m.Vars, ExpressionVariable{})
 			if err := m.Vars[len(m.Vars)-1].Unmarshal(dAtA[iNdEx:postIndex]); err != nil {
 				return err
 			}
@@ -17001,7 +17142,7 @@ func (m *PromotionTaskSpec) Unmarshal(dAtA []byte) error {
 			if postIndex > l {
 				return io.ErrUnexpectedEOF
 			}
-			m.Vars = append(m.Vars, PromotionVariable{})
+			m.Vars = append(m.Vars, ExpressionVariable{})
 			if err := m.Vars[len(m.Vars)-1].Unmarshal(dAtA[iNdEx:postIndex]); err != nil {
 				return err
 			}
@@ -17236,7 +17377,7 @@ func (m *PromotionTemplateSpec) Unmarshal(dAtA []byte) error {
 			if postIndex > l {
 				return io.ErrUnexpectedEOF
 			}
-			m.Vars = append(m.Vars, PromotionVariable{})
+			m.Vars = append(m.Vars, ExpressionVariable{})
 			if err := m.Vars[len(m.Vars)-1].Unmarshal(dAtA[iNdEx:postIndex]); err != nil {
 				return err
 			}
@@ -17262,120 +17403,6 @@ func (m *PromotionTemplateSpec) Unmarshal(dAtA []byte) error {
 	}
 	return nil
 }
-func (m *PromotionVariable) Unmarshal(dAtA []byte) error {
-	l := len(dAtA)
-	iNdEx := 0
-	for iNdEx < l {
-		preIndex := iNdEx
-		var wire uint64
-		for shift := uint(0); ; shift += 7 {
-			if shift >= 64 {
-				return ErrIntOverflowGenerated
-			}
-			if iNdEx >= l {
-				return io.ErrUnexpectedEOF
-			}
-			b := dAtA[iNdEx]
-			iNdEx++
-			wire |= uint64(b&0x7F) << shift
-			if b < 0x80 {
-				break
-			}
-		}
-		fieldNum := int32(wire >> 3)
-		wireType := int(wire & 0x7)
-		if wireType == 4 {
-			return fmt.Errorf("proto: PromotionVariable: wiretype end group for non-group")
-		}
-		if fieldNum <= 0 {
-			return fmt.Errorf("proto: PromotionVariable: illegal tag %d (wire type %d)", fieldNum, wire)
-		}
-		switch fieldNum {
-		case 1:
-			if wireType != 2 {
-				return fmt.Errorf("proto: wrong wireType = %d for field Name", wireType)
-			}
-			var stringLen uint64
-			for shift := uint(0); ; shift += 7 {
-				if shift >= 64 {
-					return ErrIntOverflowGenerated
-				}
-				if iNdEx >= l {
-					return io.ErrUnexpectedEOF
-				}
-				b := dAtA[iNdEx]
-				iNdEx++
-				stringLen |= uint64(b&0x7F) << shift
-				if b < 0x80 {
-					break
-				}
-			}
-			intStringLen := int(stringLen)
-			if intStringLen < 0 {
-				return ErrInvalidLengthGenerated
-			}
-			postIndex := iNdEx + intStringLen
-			if postIndex < 0 {
-				return ErrInvalidLengthGenerated
-			}
-			if postIndex > l {
-				return io.ErrUnexpectedEOF
-			}
-			m.Name = string(dAtA[iNdEx:postIndex])
-			iNdEx = postIndex
-		case 2:
-			if wireType != 2 {
-				return fmt.Errorf("proto: wrong wireType = %d for field Value", wireType)
-			}
-			var stringLen uint64
-			for shift := uint(0); ; shift += 7 {
-				if shift >= 64 {
-					return ErrIntOverflowGenerated
-				}
-				if iNdEx >= l {
-					return io.ErrUnexpectedEOF
-				}
-				b := dAtA[iNdEx]
-				iNdEx++
-				stringLen |= uint64(b&0x7F) << shift
-				if b < 0x80 {
-					break
-				}
-			}
-			intStringLen := int(stringLen)
-			if intStringLen < 0 {
-				return ErrInvalidLengthGenerated
-			}
-			postIndex := iNdEx + intStringLen
-			if postIndex < 0 {
-				return ErrInvalidLengthGenerated
-			}
-			if postIndex > l {
-				return io.ErrUnexpectedEOF
-			}
-			m.Value = string(dAtA[iNdEx:postIndex])
-			iNdEx = postIndex
-		default:
-			iNdEx = preIndex
-			skippy, err := skipGenerated(dAtA[iNdEx:])
-			if err != nil {
-				return err
-			}
-			if (skippy < 0) || (iNdEx+skippy) < 0 {
-				return ErrInvalidLengthGenerated
-			}
-			if (iNdEx + skippy) > l {
-				return io.ErrUnexpectedEOF
-			}
-			iNdEx += skippy
-		}
-	}
-
-	if iNdEx > l {
-		return io.ErrUnexpectedEOF
-	}
-	return nil
-}
 func (m *RepoSubscription) Unmarshal(dAtA []byte) error {
 	l := len(dAtA)
 	iNdEx := 0
@@ -17967,6 +17994,40 @@ func (m *StageSpec) Unmarshal(dAtA []byte) error {
 				return err
 			}
 			iNdEx = postIndex
+		case 7:
+			if wireType != 2 {
+				return fmt.Errorf("proto: wrong wireType = %d for field Vars", wireType)
+			}
+			var msglen int
+			for shift := uint(0); ; shift += 7 {
+				if shift >= 64 {
+					return ErrIntOverflowGenerated
+				}
+				if iNdEx >= l {
+					return io.ErrUnexpectedEOF
+				}
+				b := dAtA[iNdEx]
+				iNdEx++
+				msglen |= int(b&0x7F) << shift
+				if b < 0x80 {
+					break
+				}
+			}
+			if msglen < 0 {
+				return ErrInvalidLengthGenerated
+			}
+			postIndex := iNdEx + msglen
+			if postIndex < 0 {
+				return ErrInvalidLengthGenerated
+			}
+			if postIndex > l {
+				return io.ErrUnexpectedEOF
+			}
+			m.Vars = append(m.Vars, ExpressionVariable{})
+			if err := m.Vars[len(m.Vars)-1].Unmarshal(dAtA[iNdEx:postIndex]); err != nil {
+				return err
+			}
+			iNdEx = postIndex
 		default:
 			iNdEx = preIndex
 			skippy, err := skipGenerated(dAtA[iNdEx:])
diff --git a/api/v1alpha1/generated.proto b/api/v1alpha1/generated.proto
index 6ee061a7..b72d98f9 100644
--- a/api/v1alpha1/generated.proto
+++ b/api/v1alpha1/generated.proto
@@ -302,6 +302,25 @@ message DiscoveredImageReference {
   optional .k8s.io.apimachinery.pkg.apis.meta.v1.Time createdAt = 4;
 }
 
+// ExpressionVariable describes a single variable that may be referenced by
+// expressions in the context of a ClusterPromotionTask, PromotionTask,
+// Promotion, AnalysisRun arguments, or other objects that support expressions.
+//
+// It is used to pass information to the expression evaluation engine, and to
+// allow for dynamic evaluation of expressions based on the variable values.
+message ExpressionVariable {
+  // Name is the name of the variable.
+  //
+  // +kubebuilder:validation:MinLength=1
+  // +kubebuilder:validation:Pattern=^[a-zA-Z_]\w*$
+  optional string name = 1;
+
+  // Value is the value of the variable. It is allowed to utilize expressions
+  // in the value.
+  // See https://docs.kargo.io/user-guide/reference-docs/expressions for details.
+  optional string value = 2;
+}
+
 // Freight represents a collection of versioned artifacts.
 message Freight {
   optional .k8s.io.apimachinery.pkg.apis.meta.v1.ObjectMeta metadata = 1;
@@ -921,7 +940,7 @@ message PromotionSpec {
 
   // Vars is a list of variables that can be referenced by expressions in
   // promotion steps.
-  repeated PromotionVariable vars = 4;
+  repeated ExpressionVariable vars = 4;
 
   // Steps specifies the directives to be executed as part of this Promotion.
   // The order in which the directives are executed is the order in which they
@@ -1007,7 +1026,7 @@ message PromotionStep {
   // Vars is a list of variables that can be referenced by expressions in
   // the step's Config. The values override the values specified in the
   // PromotionSpec.
-  repeated PromotionVariable vars = 6;
+  repeated ExpressionVariable vars = 6;
 
   // Config is opaque configuration for the PromotionStep that is understood
   // only by each PromotionStep's implementation. It is legal to utilize
@@ -1095,7 +1114,7 @@ message PromotionTaskSpec {
   // Vars specifies the variables available to the PromotionTask. The
   // values of these variables are the default values that can be
   // overridden by the step referencing the task.
-  repeated PromotionVariable vars = 1;
+  repeated ExpressionVariable vars = 1;
 
   // Steps specifies the directives to be executed as part of this
   // PromotionTask. The steps as defined here are inflated into a
@@ -1119,7 +1138,7 @@ message PromotionTemplate {
 message PromotionTemplateSpec {
   // Vars is a list of variables that can be referenced by expressions in
   // promotion steps.
-  repeated PromotionVariable vars = 2;
+  repeated ExpressionVariable vars = 2;
 
   // Steps specifies the directives to be executed as part of a Promotion.
   // The order in which the directives are executed is the order in which they
@@ -1130,21 +1149,6 @@ message PromotionTemplateSpec {
   repeated PromotionStep steps = 1;
 }
 
-// PromotionVariable describes a single variable that may be referenced by
-// expressions in promotion steps.
-message PromotionVariable {
-  // Name is the name of the variable.
-  //
-  // +kubebuilder:validation:MinLength=1
-  // +kubebuilder:validation:Pattern=^[a-zA-Z_]\w*$
-  optional string name = 1;
-
-  // Value is the value of the variable. It is allowed to utilize expressions
-  // in the value.
-  // See https://docs.kargo.io/user-guide/reference-docs/expressions for details.
-  optional string value = 2;
-}
-
 // RepoSubscription describes a subscription to ONE OF a Git repository, a
 // container image repository, or a Helm chart repository.
 message RepoSubscription {
@@ -1189,6 +1193,11 @@ message StageSpec {
   // is empty, the webhook will ensure that label is absent.
   optional string shard = 4;
 
+  // Vars is a list of variables that can be referenced anywhere in the
+  // StageSpec that supports expressions. For example, the PromotionTemplate
+  // and arguments of the Verification.
+  repeated ExpressionVariable vars = 7;
+
   // RequestedFreight expresses the Stage's need for certain pieces of Freight,
   // each having originated from a particular Warehouse. This list must be
   // non-empty. In the common case, a Stage will request Freight having
diff --git a/api/v1alpha1/promotion_task_types.go b/api/v1alpha1/promotion_task_types.go
index 5c0fe35e..20270d10 100644
--- a/api/v1alpha1/promotion_task_types.go
+++ b/api/v1alpha1/promotion_task_types.go
@@ -23,7 +23,7 @@ type PromotionTaskSpec struct {
 	// Vars specifies the variables available to the PromotionTask. The
 	// values of these variables are the default values that can be
 	// overridden by the step referencing the task.
-	Vars []PromotionVariable `json:"vars,omitempty" protobuf:"bytes,1,rep,name=vars"`
+	Vars []ExpressionVariable `json:"vars,omitempty" protobuf:"bytes,1,rep,name=vars"`
 	// Steps specifies the directives to be executed as part of this
 	// PromotionTask. The steps as defined here are inflated into a
 	// Promotion when it is built from a PromotionTemplate.
diff --git a/api/v1alpha1/promotion_types.go b/api/v1alpha1/promotion_types.go
index 8e0cac73..1f8ba97e 100644
--- a/api/v1alpha1/promotion_types.go
+++ b/api/v1alpha1/promotion_types.go
@@ -96,7 +96,7 @@ type PromotionSpec struct {
 	Freight string `json:"freight" protobuf:"bytes,2,opt,name=freight"`
 	// Vars is a list of variables that can be referenced by expressions in
 	// promotion steps.
-	Vars []PromotionVariable `json:"vars,omitempty" protobuf:"bytes,4,rep,name=vars"`
+	Vars []ExpressionVariable `json:"vars,omitempty" protobuf:"bytes,4,rep,name=vars"`
 	// Steps specifies the directives to be executed as part of this Promotion.
 	// The order in which the directives are executed is the order in which they
 	// are listed in this field.
@@ -107,20 +107,6 @@ type PromotionSpec struct {
 	Steps []PromotionStep `json:"steps" protobuf:"bytes,3,rep,name=steps"`
 }
 
-// PromotionVariable describes a single variable that may be referenced by
-// expressions in promotion steps.
-type PromotionVariable struct {
-	// Name is the name of the variable.
-	//
-	// +kubebuilder:validation:MinLength=1
-	// +kubebuilder:validation:Pattern=^[a-zA-Z_]\w*$
-	Name string `json:"name" protobuf:"bytes,1,opt,name=name"`
-	// Value is the value of the variable. It is allowed to utilize expressions
-	// in the value.
-	// See https://docs.kargo.io/user-guide/reference-docs/expressions for details.
-	Value string `json:"value,omitempty" protobuf:"bytes,2,opt,name=value"`
-}
-
 // PromotionTaskReference describes a reference to a PromotionTask.
 type PromotionTaskReference struct {
 	// Name is the name of the (Cluster)PromotionTask.
@@ -217,7 +203,7 @@ type PromotionStep struct {
 	// Vars is a list of variables that can be referenced by expressions in
 	// the step's Config. The values override the values specified in the
 	// PromotionSpec.
-	Vars []PromotionVariable `json:"vars,omitempty" protobuf:"bytes,6,rep,name=vars"`
+	Vars []ExpressionVariable `json:"vars,omitempty" protobuf:"bytes,6,rep,name=vars"`
 	// Config is opaque configuration for the PromotionStep that is understood
 	// only by each PromotionStep's implementation. It is legal to utilize
 	// expressions in defining values at any level of this block.
diff --git a/api/v1alpha1/stage_types.go b/api/v1alpha1/stage_types.go
index 0f7045b2..7fd9f9e5 100644
--- a/api/v1alpha1/stage_types.go
+++ b/api/v1alpha1/stage_types.go
@@ -220,6 +220,10 @@ type StageSpec struct {
 	// kargo.akuity.io/shard label with the value of this field. When this field
 	// is empty, the webhook will ensure that label is absent.
 	Shard string `json:"shard,omitempty" protobuf:"bytes,4,opt,name=shard"`
+	// Vars is a list of variables that can be referenced anywhere in the
+	// StageSpec that supports expressions. For example, the PromotionTemplate
+	// and arguments of the Verification.
+	Vars []ExpressionVariable `json:"vars,omitempty" protobuf:"bytes,7,rep,name=vars"`
 	// RequestedFreight expresses the Stage's need for certain pieces of Freight,
 	// each having originated from a particular Warehouse. This list must be
 	// non-empty. In the common case, a Stage will request Freight having
@@ -329,7 +333,7 @@ type PromotionTemplate struct {
 type PromotionTemplateSpec struct {
 	// Vars is a list of variables that can be referenced by expressions in
 	// promotion steps.
-	Vars []PromotionVariable `json:"vars,omitempty" protobuf:"bytes,2,rep,name=vars"`
+	Vars []ExpressionVariable `json:"vars,omitempty" protobuf:"bytes,2,rep,name=vars"`
 	// Steps specifies the directives to be executed as part of a Promotion.
 	// The order in which the directives are executed is the order in which they
 	// are listed in this field.
diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go
index f3cab7fc..10f4fc31 100644
--- a/api/v1alpha1/zz_generated.deepcopy.go
+++ b/api/v1alpha1/zz_generated.deepcopy.go
@@ -364,6 +364,21 @@ func (in *DiscoveredImageReference) DeepCopy() *DiscoveredImageReference {
 	return out
 }
 
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *ExpressionVariable) DeepCopyInto(out *ExpressionVariable) {
+	*out = *in
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ExpressionVariable.
+func (in *ExpressionVariable) DeepCopy() *ExpressionVariable {
+	if in == nil {
+		return nil
+	}
+	out := new(ExpressionVariable)
+	in.DeepCopyInto(out)
+	return out
+}
+
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *Freight) DeepCopyInto(out *Freight) {
 	*out = *in
@@ -1014,7 +1029,7 @@ func (in *PromotionSpec) DeepCopyInto(out *PromotionSpec) {
 	*out = *in
 	if in.Vars != nil {
 		in, out := &in.Vars, &out.Vars
-		*out = make([]PromotionVariable, len(*in))
+		*out = make([]ExpressionVariable, len(*in))
 		copy(*out, *in)
 	}
 	if in.Steps != nil {
@@ -1099,7 +1114,7 @@ func (in *PromotionStep) DeepCopyInto(out *PromotionStep) {
 	}
 	if in.Vars != nil {
 		in, out := &in.Vars, &out.Vars
-		*out = make([]PromotionVariable, len(*in))
+		*out = make([]ExpressionVariable, len(*in))
 		copy(*out, *in)
 	}
 	if in.Config != nil {
@@ -1217,7 +1232,7 @@ func (in *PromotionTaskSpec) DeepCopyInto(out *PromotionTaskSpec) {
 	*out = *in
 	if in.Vars != nil {
 		in, out := &in.Vars, &out.Vars
-		*out = make([]PromotionVariable, len(*in))
+		*out = make([]ExpressionVariable, len(*in))
 		copy(*out, *in)
 	}
 	if in.Steps != nil {
@@ -1260,7 +1275,7 @@ func (in *PromotionTemplateSpec) DeepCopyInto(out *PromotionTemplateSpec) {
 	*out = *in
 	if in.Vars != nil {
 		in, out := &in.Vars, &out.Vars
-		*out = make([]PromotionVariable, len(*in))
+		*out = make([]ExpressionVariable, len(*in))
 		copy(*out, *in)
 	}
 	if in.Steps != nil {
@@ -1282,21 +1297,6 @@ func (in *PromotionTemplateSpec) DeepCopy() *PromotionTemplateSpec {
 	return out
 }
 
-// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
-func (in *PromotionVariable) DeepCopyInto(out *PromotionVariable) {
-	*out = *in
-}
-
-// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PromotionVariable.
-func (in *PromotionVariable) DeepCopy() *PromotionVariable {
-	if in == nil {
-		return nil
-	}
-	out := new(PromotionVariable)
-	in.DeepCopyInto(out)
-	return out
-}
-
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *RepoSubscription) DeepCopyInto(out *RepoSubscription) {
 	*out = *in
@@ -1389,6 +1389,11 @@ func (in *StageList) DeepCopyObject() runtime.Object {
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *StageSpec) DeepCopyInto(out *StageSpec) {
 	*out = *in
+	if in.Vars != nil {
+		in, out := &in.Vars, &out.Vars
+		*out = make([]ExpressionVariable, len(*in))
+		copy(*out, *in)
+	}
 	if in.RequestedFreight != nil {
 		in, out := &in.RequestedFreight, &out.RequestedFreight
 		*out = make([]FreightRequest, len(*in))
diff --git a/charts/kargo/README.md b/charts/kargo/README.md
index 3b3b8532..2bdd5502 100644
--- a/charts/kargo/README.md
+++ b/charts/kargo/README.md
@@ -67,79 +67,80 @@ the Kargo controller is running.
 
 ### API
 
-| Name                                        | Description                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                        | Value                    |
-| ------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------ |
-| `api.enabled`                               | Whether the API server is enabled.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 | `true`                   |
-| `api.replicas`                              | The number of API server pods.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                     | `1`                      |
-| `api.host`                                  | The domain name where Kargo's API server will be accessible. When applicable, this is used for generation of an Ingress resource, certificates, and the OpenID Connect issuer and callback URLs. Note: The value in this field MAY include a port number and MUST NOT specify the protocol (http vs https), which is automatically inferred from other configuration options.                                                                                                                                                                                                                                                                      | `localhost`              |
-| `api.logLevel`                              | The log level for the API server.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  | `INFO`                   |
-| `api.labels`                                | Labels to add to the api resources. Merges with `global.labels`, allowing you to override or add to the global labels.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                             | `{}`                     |
-| `api.annotations`                           | Annotations to add to the api resources. Merges with `global.annotations`, allowing you to override or add to the global annotations.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                              | `{}`                     |
-| `api.podLabels`                             | Optional labels to add to pods. Merges with `global.podLabels`, allowing you to override or add to the global labels.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                              | `{}`                     |
-| `api.podAnnotations`                        | Optional annotations to add to pods. Merges with `global.podAnnotations`, allowing you to override or add to the global annotations.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                               | `{}`                     |
-| `api.secretManagementEnabled`               | Specifies whether Secret management is enabled. This affects the API server's ability to manage repository credentials and other Project-level Secrets, such as those used by AnalysisRuns for verification purposes. If using GitOps to manage Kargo Projects declaratively, the API's Secret management capabilities are not needed and can be disabled to effectively reduce the API server's attackable surface.                                                                                                                                                                                                                               | `true`                   |
-| `api.resources`                             | Resources limits and requests for the api containers.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                              | `{}`                     |
-| `api.nodeSelector`                          | Node selector for api pods. Defaults to `global.nodeSelector`.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                     | `{}`                     |
-| `api.tolerations`                           | Tolerations for api pods. Defaults to `global.tolerations`.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                        | `[]`                     |
-| `api.affinity`                              | Specifies pod affinity for api pods. Defaults to `global.affinity`.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                | `{}`                     |
-| `api.securityContext`                       | Security context for api pods. Defaults to `global.securityContext`.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                               | `{}`                     |
-| `api.cabundle.configMapName`                | Specifies the name of an optional ConfigMap containing CA certs that is managed "out of band." Values in the ConfigMap named here should each contain a single PEM-encoded CA cert. If secretName is also defined, it will take precedence over this field.                                                                                                                                                                                                                                                                                                                                                                                        | `""`                     |
-| `api.cabundle.secretName`                   | Specifies the name of an optional Secret containing CA certs that is managed "out of band." Values in the Secret named here should each contain a single PEM-encoded CA cert. If defined, the value of this field takes precedence over any in configMapName.                                                                                                                                                                                                                                                                                                                                                                                      | `""`                     |
-| `api.env`                                   | Environment variables to add to API server pods.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                   | `[]`                     |
-| `api.envFrom`                               | Environment variables to add to API server pods from ConfigMaps or Secrets.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                        | `[]`                     |
-| `api.probes.enabled`                        | Whether liveness and readiness probes should be included in the API server deployment. It is sometimes advantageous to disable these during local development.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                     | `true`                   |
-| `api.tls.enabled`                           | Whether to enable TLS directly on the API server. This is helpful if you do not intend to use an ingress controller or if you require TLS end-to-end. All other settings in this section EXCEPT `terminatedUpstream` will be ignored when this is set to `false`.                                                                                                                                                                                                                                                                                                                                                                                  | `true`                   |
-| `api.tls.selfSignedCert`                    | Whether to generate a self-signed certificate for use by the API server. If `true`, `cert-manager` CRDs **must** be present in the cluster. Kargo will create and use its own namespaced issuer. If `false`, a cert secret named `kargo-api-cert` **must** be provided in the same namespace as Kargo.                                                                                                                                                                                                                                                                                                                                             | `true`                   |
-| `api.tls.terminatedUpstream`                | Whether TLS is terminated upstream, i.e. a load balancer, reverse-proxy, or an Ingress controller using a single wildcard cert is terminating it. Setting this to `true` forces all API server URLs to use HTTPS even if the Ingress (if applicable) or API server itself are listening for plain HTTP requests.                                                                                                                                                                                                                                                                                                                                   | `false`                  |
-| `api.permissiveCORSPolicyEnabled`           | Whether to enable a permissive CORS (Cross Origin Resource Sharing) policy. This is sometimes advantageous during local development, but otherwise, should generally be left disabled.                                                                                                                                                                                                                                                                                                                                                                                                                                                             | `false`                  |
-| `api.ingress.enabled`                       | Whether to enable ingress by creating an Ingress resource. By default, this is disabled. Enabling ingress is advanced usage.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                       | `false`                  |
-| `api.ingress.annotations`                   | Annotations specified by your ingress controller to customize the behavior of the Ingress resource.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                | `{}`                     |
-| `api.ingress.ingressClassName`              | If implemented by your ingress controller, specifies the ingress class. If your ingress controller does not support this, use the `kubernetes.io/ingress.class` annotation instead.                                                                                                                                                                                                                                                                                                                                                                                                                                                                | `nil`                    |
-| `api.ingress.tls.enabled`                   | Whether to associate a certificate with the Ingress resource.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                      | `true`                   |
-| `api.ingress.tls.selfSignedCert`            | Whether to generate a self-signed certificate for use with the API server's Ingress resource. If `true`, `cert-manager` CRDs **must** be present in the cluster. Kargo will create and use its own namespaced issuer. If `false`, a cert secret named `kargo-api-ingress-cert` **must** be provided in the same namespace as Kargo. The value in this field has no effect if `api.ingress.tls.enabled` is `false`.                                                                                                                                                                                                                                 | `true`                   |
-| `api.ingress.pathType`                      | You may want to use `Prefix` for some controllers (like AWS LoadBalancer Ingress controller), which don't support `/` as wildcard path when pathType is set to `ImplementationSpecific`                                                                                                                                                                                                                                                                                                                                                                                                                                                            | `ImplementationSpecific` |
-| `api.service.type`                          | If you're not going to use an ingress controller, you may want to change this value to `LoadBalancer` for production deployments. If running locally, you may want to change it to `NodePort` OR leave it as `ClusterIP` and use `kubectl port-forward` to map a port on the local network interface to the service.                                                                                                                                                                                                                                                                                                                               | `ClusterIP`              |
-| `api.service.nodePort`                      | Host port the `Service` will be mapped to when `type` is either `NodePort` or `LoadBalancer`. If not specified, Kubernetes chooses.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                | `undefined`              |
-| `api.service.annotations`                   | Annotations to add to the API server's service. Merges with `global.annotations`, allowing you to override or add to the global annotations.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                       | `{}`                     |
-| `api.secret.name`                           | Specifies the name of an existing Secret which contains the `ADMIN_ACCOUNT_PASSWORD_HASH` and `ADMIN_ACCOUNT_TOKEN_SIGNING_KEY` values. By setting this, the Secret will **not** be generated by Helm.                                                                                                                                                                                                                                                                                                                                                                                                                                             | `""`                     |
-| `api.adminAccount.enabled`                  | Whether to enable the admin account.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                               | `true`                   |
-| `api.adminAccount.passwordHash`             | Bcrypt password hash for the admin account. A value **must** be provided for this field unless `api.secret.name` is specified.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                     | `""`                     |
-| `api.adminAccount.tokenSigningKey`          | Key used to sign ID tokens (JWTs) for the admin account. It is suggested that you generate this using a password manager or a command like: `openssl rand -base64 29 \| tr -d "=+/" \| cut`. A value **must** be provided for this field, unless `api.secret.name` is specified.                                                                                                                                                                                                                                                                                                                                                                   | `""`                     |
-| `api.adminAccount.tokenTTL`                 | Specifies how long ID tokens for the admin account are valid. (i.e. The expiry will be the time of issue plus this duration.)                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                      | `24h`                    |
-| `api.oidc.enabled`                          | Whether to enable authentication using Open ID Connect.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                            | `false`                  |
-| `api.oidc.issuerURL`                        | The issuer URL for the identity provider. If Dex is enabled, this value will be ignored and the issuer URL will be automatically configured. If Dex is not enabled, this should be set to the issuer URL provided to you by your identity provider.                                                                                                                                                                                                                                                                                                                                                                                                | `nil`                    |
-| `api.oidc.clientID`                         | The client ID for the OIDC client. If Dex is enabled, this value will be ignored and the client ID will be automatically configured. If Dex is not enabled, this should be set to the client ID provided to you by your identity provider.                                                                                                                                                                                                                                                                                                                                                                                                         | `nil`                    |
-| `api.oidc.cliClientID`                      | The client ID for the OIDC client used by CLI (optional). Needed by some OIDC providers (such as Dex) that require a separate Client ID for web app login vs. CLI login (`http://localhost`). If Dex is enabled, this value will be ignored and cli client ID will be automatically configured. If Dex is not enabled, and a different client app is configured for localhost CLI login, this should be the client ID configured in the IdP.                                                                                                                                                                                                       | `nil`                    |
-| `api.oidc.additionalScopes`                 | The additional scopes to send to the OIDC provider. This should be set to the scopes you wish to be provided to your identity provider from clients of Kargo, the scopes openid, profile and email are always requested and don't need to be added, this value is intended for any additional ones you require.                                                                                                                                                                                                                                                                                                                                    | `["groups"]`             |
-| `api.oidc.admins.claims`                    | Subjects having any of these claims will automatically be Kargo admins.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                            | `{}`                     |
-| `api.oidc.viewers.claims`                   | Subjects having any of these claims will automatically receive read-only access to all Kargo resources.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                            | `{}`                     |
-| `api.oidc.globalServiceAccounts.namespaces` | List of namespaces to look for shared service accounts.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                            | `[]`                     |
-| `api.oidc.dex.enabled`                      | Whether to enable Dex as the identity provider. When set to true, the Kargo installation will include a Dex server and the Kargo API server will be configured to make the /dex endpoint a reverse proxy for the Dex server.                                                                                                                                                                                                                                                                                                                                                                                                                       | `false`                  |
-| `api.oidc.dex.image.repository`             | Image repository of Dex                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                            | `ghcr.io/dexidp/dex`     |
-| `api.oidc.dex.image.tag`                    | Image tag for Dex.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 | `v2.37.0`                |
-| `api.oidc.dex.image.pullPolicy`             | Image pull policy for Dex.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                         | `IfNotPresent`           |
-| `api.oidc.dex.image.pullSecrets`            | List of imagePullSecrets.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                          | `[]`                     |
-| `api.oidc.dex.env`                          | Environment variables to add to Dex server pods. This is convenient for cases where api.oidc.dex.connectors needs to reference environment variables from a Secret that is managed "out of band" with a secret management solution such as Sealed Secrets.                                                                                                                                                                                                                                                                                                                                                                                         | `[]`                     |
-| `api.oidc.dex.envFrom`                      | Environment variables to add to Dex server pods from ConfigMaps or Secrets. This is especially convenient for cases where api.oidc.dex.connectors needs to reference environment variables from a Secret that is managed "out of band" with a secret management solution such as Sealed Secrets.                                                                                                                                                                                                                                                                                                                                                   | `[]`                     |
-| `api.oidc.dex.volumes`                      | Add additional volumes to Dex pods. This is convenient for cases where api.oidc.dex.connectors needs to reference mounted data from a Secret that is managed "out of band" with a secret management solution such as Sealed Secrets.                                                                                                                                                                                                                                                                                                                                                                                                               | `[]`                     |
-| `api.oidc.dex.volumeMounts`                 | Add additional volume mounts to Dex pods. This is convenient for cases where api.oidc.dex.connectors needs to reference mounted data from a Secret that is managed "out of band" with a secret management solution such as Sealed Secrets.                                                                                                                                                                                                                                                                                                                                                                                                         | `nil`                    |
-| `api.oidc.dex.probes.enabled`               | Whether liveness and readiness probes should be included in the Dex server deployment. It is sometimes advantageous to disable these during local development.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                     | `true`                   |
-| `api.oidc.dex.tls.selfSignedCert`           | Whether to generate a self-signed certificate for use with Dex. If `true`, `cert-manager` CRDs **must** be present in the cluster. Kargo will create and use its own namespaced issuer. If `false`, a cert secret named `kargo-dex-server-cert` **must** be provided in the same namespace as Kargo. There is no provision for running Dex without TLS.                                                                                                                                                                                                                                                                                            | `true`                   |
-| `api.oidc.dex.skipApprovalScreen`           | Whether to skip Dex's own approval screen. Since upstream identity providers will already request user consent, this second approval screen from Dex can be both superfluous and confusing.                                                                                                                                                                                                                                                                                                                                                                                                                                                        | `true`                   |
-| `api.oidc.dex.connectors`                   | Configure [Dex connectors](https://dexidp.io/docs/connectors/) to one or more upstream identity providers.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                         | `[]`                     |
-| `api.oidc.dex.resources`                    | Resources limits and requests for the Dex server containers.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                       | `{}`                     |
-| `api.oidc.dex.nodeSelector`                 | Node selector for Dex server pods. Defaults to `global.nodeSelector`.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                              | `{}`                     |
-| `api.oidc.dex.tolerations`                  | Tolerations for Dex server pods. Defaults to `global.tolerations`.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 | `[]`                     |
-| `api.oidc.dex.affinity`                     | Specifies pod affinity for the Dex server pods. Defaults to `global.affinity`.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                     | `{}`                     |
-| `api.oidc.dex.annotations`                  | Annotations to add to the Dex server pods. Merges with `global.annotations`, allowing you to override or add to the global annotations.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                            | `{}`                     |
-| `api.oidc.dex.securityContext`              | Security context for Dex server pods. Defaults to `global.securityContext`.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                        | `{}`                     |
-| `api.argocd.urls`                           | Mapping of Argo CD shards names to URLs to support deep links to Argo CD URLs. If sharding is not used, map the empty string to the single Argo CD URL.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                            | `nil`                    |
-| `api.rollouts.integrationEnabled`           | Specifies whether Argo Rollouts integration is enabled. When not enabled, the API server will not be capable of creating/updating/applying AnalysesTemplate resources in the Kargo control plane. When enabled, the API server will perform a sanity check at startup. If Argo Rollouts CRDs are not found, the API server will proceed as if this integration had been explicitly disabled. Explicitly disabling is still preferable if this integration is not desired, as it will grant fewer permissions to the API server.                                                                                                                    | `true`                   |
-| `api.rollouts.logs.enabled`                 | Specifies whether support for streaming logs from AnalysisRuns using a JobMetric provider is enabled. This feature requires you to have forwarded and stored the logs yourself in a place where they can be retrieved with an HTTP GET.                                                                                                                                                                                                                                                                                                                                                                                                            | `false`                  |
-| `api.rollouts.logs.urlTemplate`             | Instructs Kargo on how to construct a URL for the retrieval of relevant logs via HTTP GET. Expressions offset by ${{ }} are supported with the following variables pre-defined and injected with values: project, namespace (the Project namespace), stage, analysisRun (its name), metricName (name of the JobMetric), jobNamespace (namespace of the Job; may be different that the Project namespace as the Job may actually execute in a different cluster), jobName, container (since a Pod associated with a Job could have more than one). Example: "https://logs.kargo.example.com/${{project}}/${{analysisRun}}/${{job}}/${{container}}". | `""`                     |
-| `api.rollouts.logs.tokenSecret.name`        | specifies the name of a Kubernetes Secret managed "out of band" that contains a token usable for accessing job metric logs.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                        | `nil`                    |
-| `api.rollouts.logs.tokenSecret.key`         | specifies the key in a Kubernetes Secret (named by name) that is managed "out of band" and contains a token usable for accessing job metric logs.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  | `nil`                    |
-| `api.rollouts.logs.httpHeaders`             | Specifies HTTP headers to include in the HTTP GET request for log retrieval. These are typically used for authentication. The header values support expressions offset by ${{ }}, with the same variables documented for urlTemplate pre-defined and injected with values.                                                                                                                                                                                                                                                                                                                                                                         | `{}`                     |
+| Name                                        | Description                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                         | Value                    |
+| ------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------ |
+| `api.enabled`                               | Whether the API server is enabled.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  | `true`                   |
+| `api.replicas`                              | The number of API server pods.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                      | `1`                      |
+| `api.host`                                  | The domain name where Kargo's API server will be accessible. When applicable, this is used for generation of an Ingress resource, certificates, and the OpenID Connect issuer and callback URLs. Note: The value in this field MAY include a port number and MUST NOT specify the protocol (http vs https), which is automatically inferred from other configuration options.                                                                                                                                                                                                                                                                                                       | `localhost`              |
+| `api.logLevel`                              | The log level for the API server.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                   | `INFO`                   |
+| `api.labels`                                | Labels to add to the api resources. Merges with `global.labels`, allowing you to override or add to the global labels.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                              | `{}`                     |
+| `api.annotations`                           | Annotations to add to the api resources. Merges with `global.annotations`, allowing you to override or add to the global annotations.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                               | `{}`                     |
+| `api.podLabels`                             | Optional labels to add to pods. Merges with `global.podLabels`, allowing you to override or add to the global labels.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                               | `{}`                     |
+| `api.podAnnotations`                        | Optional annotations to add to pods. Merges with `global.podAnnotations`, allowing you to override or add to the global annotations.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                | `{}`                     |
+| `api.secretManagementEnabled`               | Specifies whether Secret management is enabled. This affects the API server's ability to manage repository credentials and other Project-level Secrets, such as those used by AnalysisRuns for verification purposes. If using GitOps to manage Kargo Projects declaratively, the API's Secret management capabilities are not needed and can be disabled to effectively reduce the API server's attackable surface.                                                                                                                                                                                                                                                                | `true`                   |
+| `api.resources`                             | Resources limits and requests for the api containers.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                               | `{}`                     |
+| `api.nodeSelector`                          | Node selector for api pods. Defaults to `global.nodeSelector`.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                      | `{}`                     |
+| `api.tolerations`                           | Tolerations for api pods. Defaults to `global.tolerations`.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                         | `[]`                     |
+| `api.affinity`                              | Specifies pod affinity for api pods. Defaults to `global.affinity`.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 | `{}`                     |
+| `api.securityContext`                       | Security context for api pods. Defaults to `global.securityContext`.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                | `{}`                     |
+| `api.cabundle.configMapName`                | Specifies the name of an optional ConfigMap containing CA certs that is managed "out of band." Values in the ConfigMap named here should each contain a single PEM-encoded CA cert. If secretName is also defined, it will take precedence over this field.                                                                                                                                                                                                                                                                                                                                                                                                                         | `""`                     |
+| `api.cabundle.secretName`                   | Specifies the name of an optional Secret containing CA certs that is managed "out of band." Values in the Secret named here should each contain a single PEM-encoded CA cert. If defined, the value of this field takes precedence over any in configMapName.                                                                                                                                                                                                                                                                                                                                                                                                                       | `""`                     |
+| `api.env`                                   | Environment variables to add to API server pods.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                    | `[]`                     |
+| `api.envFrom`                               | Environment variables to add to API server pods from ConfigMaps or Secrets.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                         | `[]`                     |
+| `api.probes.enabled`                        | Whether liveness and readiness probes should be included in the API server deployment. It is sometimes advantageous to disable these during local development.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                      | `true`                   |
+| `api.tls.enabled`                           | Whether to enable TLS directly on the API server. This is helpful if you do not intend to use an ingress controller or if you require TLS end-to-end. All other settings in this section EXCEPT `terminatedUpstream` will be ignored when this is set to `false`.                                                                                                                                                                                                                                                                                                                                                                                                                   | `true`                   |
+| `api.tls.selfSignedCert`                    | Whether to generate a self-signed certificate for use by the API server. If `true`, `cert-manager` CRDs **must** be present in the cluster. Kargo will create and use its own namespaced issuer. If `false`, a cert secret named `kargo-api-cert` **must** be provided in the same namespace as Kargo.                                                                                                                                                                                                                                                                                                                                                                              | `true`                   |
+| `api.tls.terminatedUpstream`                | Whether TLS is terminated upstream, i.e. a load balancer, reverse-proxy, or an Ingress controller using a single wildcard cert is terminating it. Setting this to `true` forces all API server URLs to use HTTPS even if the Ingress (if applicable) or API server itself are listening for plain HTTP requests.                                                                                                                                                                                                                                                                                                                                                                    | `false`                  |
+| `api.permissiveCORSPolicyEnabled`           | Whether to enable a permissive CORS (Cross Origin Resource Sharing) policy. This is sometimes advantageous during local development, but otherwise, should generally be left disabled.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                              | `false`                  |
+| `api.ingress.enabled`                       | Whether to enable ingress by creating an Ingress resource. By default, this is disabled. Enabling ingress is advanced usage.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                        | `false`                  |
+| `api.ingress.annotations`                   | Annotations specified by your ingress controller to customize the behavior of the Ingress resource.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 | `{}`                     |
+| `api.ingress.ingressClassName`              | If implemented by your ingress controller, specifies the ingress class. If your ingress controller does not support this, use the `kubernetes.io/ingress.class` annotation instead.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 | `nil`                    |
+| `api.ingress.tls.enabled`                   | Whether to associate a certificate with the Ingress resource.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                       | `true`                   |
+| `api.ingress.tls.selfSignedCert`            | Whether to generate a self-signed certificate for use with the API server's Ingress resource. If `true`, `cert-manager` CRDs **must** be present in the cluster. Kargo will create and use its own namespaced issuer. If `false`, a cert secret named `kargo-api-ingress-cert` **must** be provided in the same namespace as Kargo. The value in this field has no effect if `api.ingress.tls.enabled` is `false`.                                                                                                                                                                                                                                                                  | `true`                   |
+| `api.ingress.pathType`                      | You may want to use `Prefix` for some controllers (like AWS LoadBalancer Ingress controller), which don't support `/` as wildcard path when pathType is set to `ImplementationSpecific`                                                                                                                                                                                                                                                                                                                                                                                                                                                                                             | `ImplementationSpecific` |
+| `api.service.type`                          | If you're not going to use an ingress controller, you may want to change this value to `LoadBalancer` for production deployments. If running locally, you may want to change it to `NodePort` OR leave it as `ClusterIP` and use `kubectl port-forward` to map a port on the local network interface to the service.                                                                                                                                                                                                                                                                                                                                                                | `ClusterIP`              |
+| `api.service.nodePort`                      | Host port the `Service` will be mapped to when `type` is either `NodePort` or `LoadBalancer`. If not specified, Kubernetes chooses.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 | `undefined`              |
+| `api.service.annotations`                   | Annotations to add to the API server's service. Merges with `global.annotations`, allowing you to override or add to the global annotations.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                        | `{}`                     |
+| `api.secret.name`                           | Specifies the name of an existing Secret which contains the `ADMIN_ACCOUNT_PASSWORD_HASH` and `ADMIN_ACCOUNT_TOKEN_SIGNING_KEY` values. By setting this, the Secret will **not** be generated by Helm.                                                                                                                                                                                                                                                                                                                                                                                                                                                                              | `""`                     |
+| `api.adminAccount.enabled`                  | Whether to enable the admin account.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                | `true`                   |
+| `api.adminAccount.passwordHash`             | Bcrypt password hash for the admin account. A value **must** be provided for this field unless `api.secret.name` is specified.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                      | `""`                     |
+| `api.adminAccount.tokenSigningKey`          | Key used to sign ID tokens (JWTs) for the admin account. It is suggested that you generate this using a password manager or a command like: `openssl rand -base64 29 \| tr -d "=+/" \| cut`. A value **must** be provided for this field, unless `api.secret.name` is specified.                                                                                                                                                                                                                                                                                                                                                                                                    | `""`                     |
+| `api.adminAccount.tokenTTL`                 | Specifies how long ID tokens for the admin account are valid. (i.e. The expiry will be the time of issue plus this duration.)                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                       | `24h`                    |
+| `api.oidc.enabled`                          | Whether to enable authentication using Open ID Connect.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                             | `false`                  |
+| `api.oidc.issuerURL`                        | The issuer URL for the identity provider. If Dex is enabled, this value will be ignored and the issuer URL will be automatically configured. If Dex is not enabled, this should be set to the issuer URL provided to you by your identity provider.                                                                                                                                                                                                                                                                                                                                                                                                                                 | `nil`                    |
+| `api.oidc.clientID`                         | The client ID for the OIDC client. If Dex is enabled, this value will be ignored and the client ID will be automatically configured. If Dex is not enabled, this should be set to the client ID provided to you by your identity provider.                                                                                                                                                                                                                                                                                                                                                                                                                                          | `nil`                    |
+| `api.oidc.cliClientID`                      | The client ID for the OIDC client used by CLI (optional). Needed by some OIDC providers (such as Dex) that require a separate Client ID for web app login vs. CLI login (`http://localhost`). If Dex is enabled, this value will be ignored and cli client ID will be automatically configured. If Dex is not enabled, and a different client app is configured for localhost CLI login, this should be the client ID configured in the IdP.                                                                                                                                                                                                                                        | `nil`                    |
+| `api.oidc.additionalScopes`                 | The additional scopes to send to the OIDC provider. This should be set to the scopes you wish to be provided to your identity provider from clients of Kargo, the scopes openid, profile and email are always requested and don't need to be added, this value is intended for any additional ones you require.                                                                                                                                                                                                                                                                                                                                                                     | `["groups"]`             |
+| `api.oidc.usernameClaim`                    | The claim to use as the username for the user.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                      | `email`                  |
+| `api.oidc.admins.claims`                    | Subjects having any of these claims will automatically be Kargo admins.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                             | `{}`                     |
+| `api.oidc.viewers.claims`                   | Subjects having any of these claims will automatically receive read-only access to all Kargo resources.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                             | `{}`                     |
+| `api.oidc.globalServiceAccounts.namespaces` | List of namespaces to look for shared service accounts.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                             | `[]`                     |
+| `api.oidc.dex.enabled`                      | Whether to enable Dex as the identity provider. When set to true, the Kargo installation will include a Dex server and the Kargo API server will be configured to make the /dex endpoint a reverse proxy for the Dex server.                                                                                                                                                                                                                                                                                                                                                                                                                                                        | `false`                  |
+| `api.oidc.dex.image.repository`             | Image repository of Dex                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                             | `ghcr.io/dexidp/dex`     |
+| `api.oidc.dex.image.tag`                    | Image tag for Dex.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  | `v2.37.0`                |
+| `api.oidc.dex.image.pullPolicy`             | Image pull policy for Dex.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                          | `IfNotPresent`           |
+| `api.oidc.dex.image.pullSecrets`            | List of imagePullSecrets.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                           | `[]`                     |
+| `api.oidc.dex.env`                          | Environment variables to add to Dex server pods. This is convenient for cases where api.oidc.dex.connectors needs to reference environment variables from a Secret that is managed "out of band" with a secret management solution such as Sealed Secrets.                                                                                                                                                                                                                                                                                                                                                                                                                          | `[]`                     |
+| `api.oidc.dex.envFrom`                      | Environment variables to add to Dex server pods from ConfigMaps or Secrets. This is especially convenient for cases where api.oidc.dex.connectors needs to reference environment variables from a Secret that is managed "out of band" with a secret management solution such as Sealed Secrets.                                                                                                                                                                                                                                                                                                                                                                                    | `[]`                     |
+| `api.oidc.dex.volumes`                      | Add additional volumes to Dex pods. This is convenient for cases where api.oidc.dex.connectors needs to reference mounted data from a Secret that is managed "out of band" with a secret management solution such as Sealed Secrets.                                                                                                                                                                                                                                                                                                                                                                                                                                                | `[]`                     |
+| `api.oidc.dex.volumeMounts`                 | Add additional volume mounts to Dex pods. This is convenient for cases where api.oidc.dex.connectors needs to reference mounted data from a Secret that is managed "out of band" with a secret management solution such as Sealed Secrets.                                                                                                                                                                                                                                                                                                                                                                                                                                          | `nil`                    |
+| `api.oidc.dex.probes.enabled`               | Whether liveness and readiness probes should be included in the Dex server deployment. It is sometimes advantageous to disable these during local development.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                      | `true`                   |
+| `api.oidc.dex.tls.selfSignedCert`           | Whether to generate a self-signed certificate for use with Dex. If `true`, `cert-manager` CRDs **must** be present in the cluster. Kargo will create and use its own namespaced issuer. If `false`, a cert secret named `kargo-dex-server-cert` **must** be provided in the same namespace as Kargo. There is no provision for running Dex without TLS.                                                                                                                                                                                                                                                                                                                             | `true`                   |
+| `api.oidc.dex.skipApprovalScreen`           | Whether to skip Dex's own approval screen. Since upstream identity providers will already request user consent, this second approval screen from Dex can be both superfluous and confusing.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                         | `true`                   |
+| `api.oidc.dex.connectors`                   | Configure [Dex connectors](https://dexidp.io/docs/connectors/) to one or more upstream identity providers.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                          | `[]`                     |
+| `api.oidc.dex.resources`                    | Resources limits and requests for the Dex server containers.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                        | `{}`                     |
+| `api.oidc.dex.nodeSelector`                 | Node selector for Dex server pods. Defaults to `global.nodeSelector`.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                               | `{}`                     |
+| `api.oidc.dex.tolerations`                  | Tolerations for Dex server pods. Defaults to `global.tolerations`.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  | `[]`                     |
+| `api.oidc.dex.affinity`                     | Specifies pod affinity for the Dex server pods. Defaults to `global.affinity`.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                      | `{}`                     |
+| `api.oidc.dex.annotations`                  | Annotations to add to the Dex server pods. Merges with `global.annotations`, allowing you to override or add to the global annotations.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                             | `{}`                     |
+| `api.oidc.dex.securityContext`              | Security context for Dex server pods. Defaults to `global.securityContext`.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                         | `{}`                     |
+| `api.argocd.urls`                           | Mapping of Argo CD shards names to URLs to support deep links to Argo CD URLs. If sharding is not used, map the empty string to the single Argo CD URL.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                             | `nil`                    |
+| `api.rollouts.integrationEnabled`           | Specifies whether Argo Rollouts integration is enabled. When not enabled, the API server will not be capable of creating/updating/applying AnalysesTemplate resources in the Kargo control plane. When enabled, the API server will perform a sanity check at startup. If Argo Rollouts CRDs are not found, the API server will proceed as if this integration had been explicitly disabled. Explicitly disabling is still preferable if this integration is not desired, as it will grant fewer permissions to the API server.                                                                                                                                                     | `true`                   |
+| `api.rollouts.logs.enabled`                 | Specifies whether support for streaming logs from AnalysisRuns using a JobMetric provider is enabled. This feature requires you to have forwarded and stored the logs yourself in a place where they can be retrieved with an HTTP GET.                                                                                                                                                                                                                                                                                                                                                                                                                                             | `false`                  |
+| `api.rollouts.logs.urlTemplate`             | Instructs Kargo on how to construct a URL for the retrieval of relevant logs via HTTP GET. Expressions offset by ${{ }} are supported with the following variables pre-defined and injected with values: project (name), namespace (always equal to the Project's name), stage (name), analysisRun (name), metricName (name of the JobMetric), jobNamespace (namespace of the Job; may be different that the Project namespace as the Job may actually execute in a different cluster), jobName, container (name; since a Pod associated with a Job could have more than one). Example: "https://logs.kargo.example.com/${{project}}/${{analysisRun}}/${{jobName}}/${{container}}". | `""`                     |
+| `api.rollouts.logs.tokenSecret.name`        | specifies the name of a Kubernetes Secret managed "out of band" that contains a token usable for accessing job metric logs.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                         | `nil`                    |
+| `api.rollouts.logs.tokenSecret.key`         | specifies the key in a Kubernetes Secret (named by name) that is managed "out of band" and contains a token usable for accessing job metric logs.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                   | `nil`                    |
+| `api.rollouts.logs.httpHeaders`             | Specifies HTTP headers to include in the HTTP GET request for log retrieval. These are typically used for authentication. The header values support expressions offset by ${{ }}, with the same variables documented for urlTemplate pre-defined and injected with values.                                                                                                                                                                                                                                                                                                                                                                                                          | `{}`                     |
 
 ### Controller
 
diff --git a/charts/kargo/resources/crds/kargo.akuity.io_clusterpromotiontasks.yaml b/charts/kargo/resources/crds/kargo.akuity.io_clusterpromotiontasks.yaml
index 73d9b15e..afbd4ed9 100644
--- a/charts/kargo/resources/crds/kargo.akuity.io_clusterpromotiontasks.yaml
+++ b/charts/kargo/resources/crds/kargo.akuity.io_clusterpromotiontasks.yaml
@@ -151,8 +151,12 @@ spec:
                         PromotionSpec.
                       items:
                         description: |-
-                          PromotionVariable describes a single variable that may be referenced by
-                          expressions in promotion steps.
+                          ExpressionVariable describes a single variable that may be referenced by
+                          expressions in the context of a ClusterPromotionTask, PromotionTask,
+                          Promotion, AnalysisRun arguments, or other objects that support expressions.
+
+                          It is used to pass information to the expression evaluation engine, and to
+                          allow for dynamic evaluation of expressions based on the variable values.
                         properties:
                           name:
                             description: Name is the name of the variable.
@@ -183,8 +187,12 @@ spec:
                   overridden by the step referencing the task.
                 items:
                   description: |-
-                    PromotionVariable describes a single variable that may be referenced by
-                    expressions in promotion steps.
+                    ExpressionVariable describes a single variable that may be referenced by
+                    expressions in the context of a ClusterPromotionTask, PromotionTask,
+                    Promotion, AnalysisRun arguments, or other objects that support expressions.
+
+                    It is used to pass information to the expression evaluation engine, and to
+                    allow for dynamic evaluation of expressions based on the variable values.
                   properties:
                     name:
                       description: Name is the name of the variable.
diff --git a/charts/kargo/resources/crds/kargo.akuity.io_promotions.yaml b/charts/kargo/resources/crds/kargo.akuity.io_promotions.yaml
index 89764547..876e1680 100644
--- a/charts/kargo/resources/crds/kargo.akuity.io_promotions.yaml
+++ b/charts/kargo/resources/crds/kargo.akuity.io_promotions.yaml
@@ -183,8 +183,12 @@ spec:
                         PromotionSpec.
                       items:
                         description: |-
-                          PromotionVariable describes a single variable that may be referenced by
-                          expressions in promotion steps.
+                          ExpressionVariable describes a single variable that may be referenced by
+                          expressions in the context of a ClusterPromotionTask, PromotionTask,
+                          Promotion, AnalysisRun arguments, or other objects that support expressions.
+
+                          It is used to pass information to the expression evaluation engine, and to
+                          allow for dynamic evaluation of expressions based on the variable values.
                         properties:
                           name:
                             description: Name is the name of the variable.
@@ -214,8 +218,12 @@ spec:
                   promotion steps.
                 items:
                   description: |-
-                    PromotionVariable describes a single variable that may be referenced by
-                    expressions in promotion steps.
+                    ExpressionVariable describes a single variable that may be referenced by
+                    expressions in the context of a ClusterPromotionTask, PromotionTask,
+                    Promotion, AnalysisRun arguments, or other objects that support expressions.
+
+                    It is used to pass information to the expression evaluation engine, and to
+                    allow for dynamic evaluation of expressions based on the variable values.
                   properties:
                     name:
                       description: Name is the name of the variable.
diff --git a/charts/kargo/resources/crds/kargo.akuity.io_promotiontasks.yaml b/charts/kargo/resources/crds/kargo.akuity.io_promotiontasks.yaml
index 7a005a84..cb7d0da8 100644
--- a/charts/kargo/resources/crds/kargo.akuity.io_promotiontasks.yaml
+++ b/charts/kargo/resources/crds/kargo.akuity.io_promotiontasks.yaml
@@ -151,8 +151,12 @@ spec:
                         PromotionSpec.
                       items:
                         description: |-
-                          PromotionVariable describes a single variable that may be referenced by
-                          expressions in promotion steps.
+                          ExpressionVariable describes a single variable that may be referenced by
+                          expressions in the context of a ClusterPromotionTask, PromotionTask,
+                          Promotion, AnalysisRun arguments, or other objects that support expressions.
+
+                          It is used to pass information to the expression evaluation engine, and to
+                          allow for dynamic evaluation of expressions based on the variable values.
                         properties:
                           name:
                             description: Name is the name of the variable.
@@ -183,8 +187,12 @@ spec:
                   overridden by the step referencing the task.
                 items:
                   description: |-
-                    PromotionVariable describes a single variable that may be referenced by
-                    expressions in promotion steps.
+                    ExpressionVariable describes a single variable that may be referenced by
+                    expressions in the context of a ClusterPromotionTask, PromotionTask,
+                    Promotion, AnalysisRun arguments, or other objects that support expressions.
+
+                    It is used to pass information to the expression evaluation engine, and to
+                    allow for dynamic evaluation of expressions based on the variable values.
                   properties:
                     name:
                       description: Name is the name of the variable.
diff --git a/charts/kargo/resources/crds/kargo.akuity.io_stages.yaml b/charts/kargo/resources/crds/kargo.akuity.io_stages.yaml
index 27c13d18..6edc1de4 100644
--- a/charts/kargo/resources/crds/kargo.akuity.io_stages.yaml
+++ b/charts/kargo/resources/crds/kargo.akuity.io_stages.yaml
@@ -173,8 +173,12 @@ spec:
                                 PromotionSpec.
                               items:
                                 description: |-
-                                  PromotionVariable describes a single variable that may be referenced by
-                                  expressions in promotion steps.
+                                  ExpressionVariable describes a single variable that may be referenced by
+                                  expressions in the context of a ClusterPromotionTask, PromotionTask,
+                                  Promotion, AnalysisRun arguments, or other objects that support expressions.
+
+                                  It is used to pass information to the expression evaluation engine, and to
+                                  allow for dynamic evaluation of expressions based on the variable values.
                                 properties:
                                   name:
                                     description: Name is the name of the variable.
@@ -204,8 +208,12 @@ spec:
                           promotion steps.
                         items:
                           description: |-
-                            PromotionVariable describes a single variable that may be referenced by
-                            expressions in promotion steps.
+                            ExpressionVariable describes a single variable that may be referenced by
+                            expressions in the context of a ClusterPromotionTask, PromotionTask,
+                            Promotion, AnalysisRun arguments, or other objects that support expressions.
+
+                            It is used to pass information to the expression evaluation engine, and to
+                            allow for dynamic evaluation of expressions based on the variable values.
                           properties:
                             name:
                               description: Name is the name of the variable.
@@ -320,6 +328,35 @@ spec:
                   kargo.akuity.io/shard label with the value of this field. When this field
                   is empty, the webhook will ensure that label is absent.
                 type: string
+              vars:
+                description: |-
+                  Vars is a list of variables that can be referenced anywhere in the
+                  StageSpec that supports expressions. For example, the PromotionTemplate
+                  and arguments of the Verification.
+                items:
+                  description: |-
+                    ExpressionVariable describes a single variable that may be referenced by
+                    expressions in the context of a ClusterPromotionTask, PromotionTask,
+                    Promotion, AnalysisRun arguments, or other objects that support expressions.
+
+                    It is used to pass information to the expression evaluation engine, and to
+                    allow for dynamic evaluation of expressions based on the variable values.
+                  properties:
+                    name:
+                      description: Name is the name of the variable.
+                      minLength: 1
+                      pattern: ^[a-zA-Z_]\w*$
+                      type: string
+                    value:
+                      description: |-
+                        Value is the value of the variable. It is allowed to utilize expressions
+                        in the value.
+                        See https://docs.kargo.io/user-guide/reference-docs/expressions for details.
+                      type: string
+                  required:
+                  - name
+                  type: object
+                type: array
               verification:
                 description: |-
                   Verification describes how to verify a Stage's current Freight is fit for
diff --git a/charts/kargo/templates/api/cluster-role.yaml b/charts/kargo/templates/api/cluster-role.yaml
index 0a35ca02..2f51b917 100644
--- a/charts/kargo/templates/api/cluster-role.yaml
+++ b/charts/kargo/templates/api/cluster-role.yaml
@@ -33,22 +33,6 @@ rules:
       - watch
       - create
       - patch
-  - apiGroups:
-      - ""
-    resources:
-      - configmaps
-    verbs:
-      - get
-      - list
-      - watch
-  - apiGroups:
-      - ""
-    resources:
-      - secrets
-    verbs:
-      - get
-      - list
-      - watch
   - apiGroups:
       - authorization.k8s.io
     resources:
diff --git a/charts/kargo/templates/api/configmap.yaml b/charts/kargo/templates/api/configmap.yaml
index ece6b8c6..cc369b68 100644
--- a/charts/kargo/templates/api/configmap.yaml
+++ b/charts/kargo/templates/api/configmap.yaml
@@ -49,6 +49,7 @@ data:
   {{- if .Values.api.oidc.cliClientID }}
   OIDC_CLI_CLIENT_ID: {{ quote .Values.api.oidc.cliClientID }}
   {{- end }}
+  OIDC_USERNAME_CLAIM: {{ quote .Values.api.oidc.usernameClaim | default "email" }}
   {{- end }}
   {{- end }}
   {{- if .Values.api.argocd.urls }}
diff --git a/charts/kargo/templates/controller/cluster-roles.yaml b/charts/kargo/templates/controller/cluster-roles.yaml
index f94ef1c2..45907b11 100644
--- a/charts/kargo/templates/controller/cluster-roles.yaml
+++ b/charts/kargo/templates/controller/cluster-roles.yaml
@@ -7,6 +7,14 @@ metadata:
     {{- include "kargo.labels" . | nindent 4 }}
     {{- include "kargo.controller.labels" . | nindent 4 }}
 rules:
+- apiGroups:
+  - ""
+  resources:
+  - configmaps
+  verbs:
+  - get
+  - list
+  - watch
 - apiGroups:
   - ""
   resources:
diff --git a/charts/kargo/values.yaml b/charts/kargo/values.yaml
index de9785dc..35f44c83 100755
--- a/charts/kargo/values.yaml
+++ b/charts/kargo/values.yaml
@@ -211,6 +211,8 @@ api:
     ## @param api.oidc.additionalScopes The additional scopes to send to the OIDC provider. This should be set to the scopes you wish to be provided to your identity provider from clients of Kargo, the scopes openid, profile and email are always requested and don't need to be added, this value is intended for any additional ones you require.
     additionalScopes:
     - groups
+    ## @param api.oidc.usernameClaim The claim to use as the username for the user.
+    usernameClaim: email
 
     admins:
       ## @param api.oidc.admins.claims Subjects having any of these claims will automatically be Kargo admins.
@@ -355,7 +357,7 @@ api:
     logs:
       ## @param api.rollouts.logs.enabled Specifies whether support for streaming logs from AnalysisRuns using a JobMetric provider is enabled. This feature requires you to have forwarded and stored the logs yourself in a place where they can be retrieved with an HTTP GET.
       enabled: false
-      ## @param api.rollouts.logs.urlTemplate Instructs Kargo on how to construct a URL for the retrieval of relevant logs via HTTP GET. Expressions offset by ${{ }} are supported with the following variables pre-defined and injected with values: project, namespace (the Project namespace), stage, analysisRun (its name), metricName (name of the JobMetric), jobNamespace (namespace of the Job; may be different that the Project namespace as the Job may actually execute in a different cluster), jobName, container (since a Pod associated with a Job could have more than one). Example: "https://logs.kargo.example.com/${{project}}/${{analysisRun}}/${{job}}/${{container}}".
+      ## @param api.rollouts.logs.urlTemplate Instructs Kargo on how to construct a URL for the retrieval of relevant logs via HTTP GET. Expressions offset by ${{ }} are supported with the following variables pre-defined and injected with values: project (name), namespace (always equal to the Project's name), stage (name), analysisRun (name), metricName (name of the JobMetric), jobNamespace (namespace of the Job; may be different that the Project namespace as the Job may actually execute in a different cluster), jobName, container (name; since a Pod associated with a Job could have more than one). Example: "https://logs.kargo.example.com/${{project}}/${{analysisRun}}/${{jobName}}/${{container}}".
       urlTemplate: ""
       tokenSecret:
         ## @param api.rollouts.logs.tokenSecret.name specifies the name of a Kubernetes Secret managed "out of band" that contains a token usable for accessing job metric logs.
diff --git a/cmd/controlplane/controller.go b/cmd/controlplane/controller.go
index f479e802..1807b040 100644
--- a/cmd/controlplane/controller.go
+++ b/cmd/controlplane/controller.go
@@ -209,12 +209,18 @@ func (o *controllerOptions) setupKargoManager(
 			PprofBindAddress: o.PprofBindAddress,
 			Client: client.Options{
 				Cache: &client.CacheOptions{
-					// The controller does not have cluster-wide permissions, to
-					// get/list/watch Secrets. Its access to Secrets grows and shrinks
-					// dynamically as Projects are created and deleted. We disable caching
-					// here since the underlying informer will not be able to watch
-					// Secrets in all namespaces.
-					DisableFor: []client.Object{&corev1.Secret{}},
+					DisableFor: []client.Object{
+						// The controller does not have cluster-wide permissions, to
+						// get/list/watch Secrets. Its access to Secrets grows and shrinks
+						// dynamically as Projects are created and deleted. We disable
+						// caching here since the underlying informer will not be able to
+						// watch Secrets in all namespaces.
+						&corev1.Secret{},
+						// The controller has cluster-wide permissions to get/list/watch
+						// ConfigMaps, but ConfigMaps have the potential to be quite large,
+						// so we prefer to not cache them.
+						&corev1.ConfigMap{},
+					},
 				},
 			},
 			Cache: cache.Options{
diff --git a/docs/docs/40-operator-guide/20-advanced-installation/30-common-configurations.md b/docs/docs/40-operator-guide/20-advanced-installation/30-common-configurations.md
index 2d97df95..afc18a20 100644
--- a/docs/docs/40-operator-guide/20-advanced-installation/30-common-configurations.md
+++ b/docs/docs/40-operator-guide/20-advanced-installation/30-common-configurations.md
@@ -370,8 +370,9 @@ any logs that it can access via an HTTP GET request.
 
 To facilitate this, operators may, at the time of installation, provide a URL
 template that the API server can use to construct the URL for any job metric logs
-as a function of attributes such as `Project` name, `Stage` name, `AnalysisRun`
-name, and more.
+as a function of `project` (name), `namespace` (always equal to the `Project`'s
+name), `shard`, `stage` (name), `analysisRun` (name), `metricName`,
+`jobNamespace`, `jobName`, and `container` (name).
 
 A token can be specified by referencing a Kubernetes `Secret` that is managed
 "out of band." HTTP headers may also be specified, and may reference the token
@@ -385,7 +386,7 @@ api:
     integrationEnabled: true
     logs:
       enabled: true
-      urlTemplate: https://logs.kargo.example.com/${{project}}/${{analysisRun}}/${{job}}/${{container}}
+      urlTemplate: https://logs.kargo.example.com/${{project}}/${{analysisRun}}/${{jobName}}/${{container}}
       tokenSecret:
         name: kargo-logs-token
         key: token
diff --git a/docs/docs/40-operator-guide/40-security/40-managing-credentials.md b/docs/docs/40-operator-guide/40-security/40-managing-credentials.md
index 958eacb8..ab8fec21 100644
--- a/docs/docs/40-operator-guide/40-security/40-managing-credentials.md
+++ b/docs/docs/40-operator-guide/40-security/40-managing-credentials.md
@@ -310,10 +310,11 @@ Google Artifact Registry repositories.
 :::
 
 :::info
-Unlike in the case of EKS Pod Identity or IRSA, the Kargo controller does not
-fall back on using its own IAM principal directly if it is unable to impersonate
-a project-specific Google service account, although that capability is
-anticipated in a future release.
+Like in the case of EKS Pod Identity or IRSA if project-specific credentials could
+not be assumed by the Kargo controller will fall back on on using its own 
+IAM principal directly. For organizations without strict tenancy requirements, this 
+can eliminate the need to manage a large number of project-specific IAM roles. 
+While useful, this approach is not strictly recommended.
 :::
 
 ### Azure Container Registry (ACR)
diff --git a/docs/docs/50-user-guide/60-reference-docs/30-promotion-steps/compose-output.md b/docs/docs/50-user-guide/60-reference-docs/30-promotion-steps/compose-output.md
index 9156823f..a9fbd8ca 100644
--- a/docs/docs/50-user-guide/60-reference-docs/30-promotion-steps/compose-output.md
+++ b/docs/docs/50-user-guide/60-reference-docs/30-promotion-steps/compose-output.md
@@ -38,24 +38,25 @@ vars:
 - name: repoURL
   value: https://github.com/example/repo
 steps:
+# Clone, prepare the contents of ./out, commit, push as part of a `push` step etc...
 - uses: git-open-pr
   as: open-pr
   config:
     repoURL: ${{ vars.repoURL }}
     createTargetBranch: true
-    sourceBranch: ${{ outputs.push.branch }}
+    sourceBranch: ${{ task.outputs.push.branch }}
     targetBranch: stage/${{ ctx.stage }}
 - uses: compose-output
   as: pr-link
   config:
-    url: ${{ vars.repoURL }}/pull/${{ outputs['open-pr'].prNumber }}
+    url: ${{ vars.repoURL }}/pull/${{ task.outputs['open-pr'].prNumber }}
 - uses: http
   config:
     method: POST
     url: https://slack.com/api/chat.postMessage
     headers:
     - name: Authorization
-      value: Bearer ${{ secrets.slack.token }}
+      value: Bearer ${{ secret('slack').token }}
     - name: Content-Type
       value: application/json
     body: |
@@ -66,7 +67,7 @@ steps:
             "type": "section",
             "text": {
               "type": "mrkdwn",
-              "text": "A new PR has been opened: ${{ outputs['pr-link'].url }}"
+              "text": "A new PR has been opened: " + task.outputs['pr-link'].url
             }
           }
         ]
diff --git a/docs/docs/50-user-guide/60-reference-docs/30-promotion-steps/helm-template.md b/docs/docs/50-user-guide/60-reference-docs/30-promotion-steps/helm-template.md
index eb34bbee..4325b011 100644
--- a/docs/docs/50-user-guide/60-reference-docs/30-promotion-steps/helm-template.md
+++ b/docs/docs/50-user-guide/60-reference-docs/30-promotion-steps/helm-template.md
@@ -26,6 +26,9 @@ commonly preceded by a [`git-clear` step](git-clear.md) and followed by
 | `skipTests` | `boolean` | N | Whether to skip tests when rendering the manifests. This is `false` by default. |
 | `kubeVersion` | `string` | N | Optionally specifies a Kubernetes version to be assumed when rendering manifests. This is useful for charts that may contain logic specific to different Kubernetes versions. |
 | `apiVersions` | `[]string` | N | Allows a manual set of supported API versions to be specified. |
+| `setValues` | `[]object` | N | Allows for amending chart configuration inline as one would with the `helm template` command's `--set` flag. |
+| `setValues[].key` | `string` | N | The key whose value should be set. For nested values, use dots to delimit key parts. e.g. `image.tag`. |
+| `setValues[].value` | `string` | N | The new value for the key. |
 
 ## Examples
 
diff --git a/docs/docs/50-user-guide/60-reference-docs/30-promotion-steps/http.md b/docs/docs/50-user-guide/60-reference-docs/30-promotion-steps/http.md
index 67275ace..dcd613c6 100644
--- a/docs/docs/50-user-guide/60-reference-docs/30-promotion-steps/http.md
+++ b/docs/docs/50-user-guide/60-reference-docs/30-promotion-steps/http.md
@@ -172,7 +172,7 @@ steps:
     url: https://slack.com/api/chat.postMessage
     headers:
     - name: Authorization
-      value: Bearer ${{ secrets.slack.token }}
+      value: Bearer ${{ secret('slack').token }}
     - name: Content-Type
       value: application/json
     body: |
diff --git a/docs/docs/50-user-guide/60-reference-docs/40-expressions.md b/docs/docs/50-user-guide/60-reference-docs/40-expressions.md
index 507b58f2..3fd6614c 100644
--- a/docs/docs/50-user-guide/60-reference-docs/40-expressions.md
+++ b/docs/docs/50-user-guide/60-reference-docs/40-expressions.md
@@ -145,7 +145,7 @@ Expect other useful variables to be added in the future.
 |------|------|-------------|
 | `ctx` | `object` | Contains contextual information about the promotion. See detailed structure below. |
 | `outputs` | `object` | A map of output from previous promotion steps indexed by step aliases. |
-| `secrets` | `object` | A map of maps indexed by the names of all Kubernetes `Secret`s in the `Promotion`'s `Project` and the keys within the `Data` block of each. |
+| `secrets` | `object` | A map of maps indexed by the names of all Kubernetes `Secret`s in the `Promotion`'s `Project` and the keys within the `Data` block of each.<br/><br/>__Deprecated: Use the [`secret()` function](#secretname) instead. Will be removed in v1.7.0.__ |
 | `vars` | `object` | A user-defined map of variable names to static values of any type. The map is derived from a `Promotion`'s `spec.promotionTemplate.spec.vars` field. Variable names must observe standard Go variable-naming rules. Variables values may, themselves, be defined using an expression. `vars` (contains previously defined variables) and `ctx` are available to expressions defining the values of variables, however, `outputs` and `secrets` are not. |
 | `task` | `object` | A map containing output from previous steps within the same PromotionTask under the `outputs` field, indexed by step aliases. Only available within `(Cluster)PromotionTask` steps. |
 
@@ -269,6 +269,34 @@ config:
   strField: ${{ quote(40 + 2) }} # Will be treated as a string
 ```
 
+### `configMap(name)`
+
+The `configMap()` function returns the `Data` field (a `map[string]string`) of a
+Kubernetes `ConfigMap` with the specified name from the `Project`'s namespace.
+If no such `ConfigMap` exists, an empty map is returned.
+
+Example:
+
+```yaml
+config:
+  repoURL: ${{ configMap('my-config').repoURL }}
+```
+
+### `secret(name)`
+
+The `secret()` function returns the `Data` field of a Kubernetes `Secret` with
+the specified name from the `Project`'s namespace decoded into a
+`map[string]string`. If no such `Secret` exists, an empty map is returned.
+
+Examples:
+
+```yaml
+config:
+  headers:
+  - name: Authorization
+    value: Bearer ${{ secret('slack').token }}
+```
+
 ### `warehouse(name)`
 
 The `warehouse()` function returns a `FreightOrigin` object representing a
@@ -286,7 +314,14 @@ The returned `FreightOrigin` object has the following fields:
 
 The `FreightOrigin` object can be used as an optional argument to the
 `commitFrom()`, `imageFrom()`, or `chartFrom()` functions to disambiguate the
-desired source of an artifact when necessary.
+desired source of an artifact when necessary. These functions return `nil` when
+relevant `Freight` is not found from the `FreightCollection`. 
+
+:::tip
+You can handle `nil` values gracefully in Expr using its
+[nil coalescing](https://expr-lang.org/docs/language-definition#nil-coalescing) and
+[optional chaining](https://expr-lang.org/docs/language-definition#optional-chaining) features.
+:::
 
 ### `commitFrom(repoURL, [freightOrigin])`
 
@@ -315,6 +350,8 @@ The optional `freightOrigin` argument should be used when a `Stage` requests
 `Freight` from multiple origins (`Warehouse`s) and more than one can provide a
 `GitCommit` object from the specified repository.
 
+If a commit is not found from the `FreightCollection`, returns `nil`.
+
 Examples:
 
 ```yaml
@@ -352,6 +389,8 @@ The optional `freightOrigin` argument should be used when a `Stage` requests
 `Freight` from multiple origins (`Warehouse`s) and more than one can provide a
 `Image` object from the specified repository.
 
+If an image is not found from the `FreightCollection`, returns `nil`.
+
 Examples:
 
 ```yaml
@@ -396,6 +435,8 @@ The optional `freightOrigin` argument should be used when a `Stage` requests
 `Freight` from multiple origins (`Warehouse`s) and more than one can provide a
 `Chart` object from the specified repository.
 
+If a chart is not found from the `FreightCollection`, returns `nil`.
+
 Examples:
 
 ```yaml
diff --git a/docs/docs/50-user-guide/60-reference-docs/50-analysis-templates.md b/docs/docs/50-user-guide/60-reference-docs/50-analysis-templates.md
index b2d68695..5498c242 100644
--- a/docs/docs/50-user-guide/60-reference-docs/50-analysis-templates.md
+++ b/docs/docs/50-user-guide/60-reference-docs/50-analysis-templates.md
@@ -97,6 +97,40 @@ spec:
         jsonPath: "{$.results.ok}"
 ```
 
+## Interval and Count
+
+To collect multiple measurements over a longer duration, use the 
+`count` and `interval` fields. This allows you to define how many
+measurements to take and how frequently to take them.
+
+In the example below, the analysis is configured to take 20
+measurements at 3-minute intervals, resulting in a total duration
+of approximately 1 hour:
+
+```yaml
+apiVersion: argoproj.io/v1alpha1
+kind: AnalysisTemplate
+metadata:
+  name: count-interval-example
+spec:
+  metrics:
+  - name: test
+    # Number of measurements to take
+    count: 20
+    # Time interval between each measurement as a duration string
+    interval: 3m
+    provider:
+      web:
+        url: https://pokeapi.co/api/v2/pokemon/pikachu
+```
+
+:::caution
+When specifying an `interval`, you must also specify a `count`. Without 
+it, the AnalysisRun would collect an indefinite amount of measurements
+and never complete (until terminated).
+:::
+
+
 ## Success Condition
 
 When interpreting the result of a query, an
@@ -150,6 +184,7 @@ spec:
   metrics:
   - name: total-errors
     interval: 5m
+    count: 3
     failureCondition: result[0] >= 10
     failureLimit: 3
     provider:
diff --git a/go.mod b/go.mod
index 528beaae..b2432183 100644
--- a/go.mod
+++ b/go.mod
@@ -18,10 +18,10 @@ require (
 	github.com/akuity/kargo/api v0.0.0
 	github.com/akuity/kargo/pkg v0.0.0
 	github.com/aws/aws-sdk-go-v2 v1.36.3
-	github.com/aws/aws-sdk-go-v2/config v1.29.13
-	github.com/aws/aws-sdk-go-v2/credentials v1.17.66
-	github.com/aws/aws-sdk-go-v2/service/ecr v1.43.2
-	github.com/aws/aws-sdk-go-v2/service/sts v1.33.18
+	github.com/aws/aws-sdk-go-v2/config v1.29.14
+	github.com/aws/aws-sdk-go-v2/credentials v1.17.67
+	github.com/aws/aws-sdk-go-v2/service/ecr v1.43.3
+	github.com/aws/aws-sdk-go-v2/service/sts v1.33.19
 	github.com/bacongobbler/browser v1.1.0
 	github.com/bmatcuk/doublestar/v4 v4.8.1
 	github.com/bombsimon/logrusr/v4 v4.1.0
@@ -31,7 +31,7 @@ require (
 	github.com/expr-lang/expr v1.17.2
 	github.com/fatih/structtag v1.2.0
 	github.com/fluxcd/pkg/kustomize v1.16.0
-	github.com/go-git/go-git/v5 v5.14.0
+	github.com/go-git/go-git/v5 v5.15.0
 	github.com/go-logr/logr v1.4.2
 	github.com/gogo/protobuf v1.3.2
 	github.com/golang-jwt/jwt/v5 v5.2.2
@@ -59,7 +59,7 @@ require (
 	gitlab.com/gitlab-org/api/client-go v0.127.0
 	go.uber.org/ratelimit v0.3.1
 	golang.org/x/crypto v0.37.0
-	golang.org/x/net v0.38.0
+	golang.org/x/net v0.39.0
 	golang.org/x/oauth2 v0.29.0
 	golang.org/x/sync v0.13.0
 	golang.org/x/text v0.24.0
@@ -67,7 +67,7 @@ require (
 	google.golang.org/grpc v1.71.1
 	google.golang.org/protobuf v1.36.6
 	gopkg.in/yaml.v3 v3.0.1
-	helm.sh/helm/v3 v3.17.2
+	helm.sh/helm/v3 v3.17.3
 	k8s.io/api v0.32.3
 	k8s.io/apiextensions-apiserver v0.32.3
 	k8s.io/apimachinery v0.32.3
diff --git a/go.sum b/go.sum
index 40dc1dcd..04bbe752 100644
--- a/go.sum
+++ b/go.sum
@@ -52,10 +52,10 @@ github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3d
 github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw=
 github.com/aws/aws-sdk-go-v2 v1.36.3 h1:mJoei2CxPutQVxaATCzDUjcZEjVRdpsiiXi2o38yqWM=
 github.com/aws/aws-sdk-go-v2 v1.36.3/go.mod h1:LLXuLpgzEbD766Z5ECcRmi8AzSwfZItDtmABVkRLGzg=
-github.com/aws/aws-sdk-go-v2/config v1.29.13 h1:RgdPqWoE8nPpIekpVpDJsBckbqT4Liiaq9f35pbTh1Y=
-github.com/aws/aws-sdk-go-v2/config v1.29.13/go.mod h1:NI28qs/IOUIRhsR7GQ/JdexoqRN9tDxkIrYZq0SOF44=
-github.com/aws/aws-sdk-go-v2/credentials v1.17.66 h1:aKpEKaTy6n4CEJeYI1MNj97oSDLi4xro3UzQfwf5RWE=
-github.com/aws/aws-sdk-go-v2/credentials v1.17.66/go.mod h1:xQ5SusDmHb/fy55wU0QqTy0yNfLqxzec59YcsRZB+rI=
+github.com/aws/aws-sdk-go-v2/config v1.29.14 h1:f+eEi/2cKCg9pqKBoAIwRGzVb70MRKqWX4dg1BDcSJM=
+github.com/aws/aws-sdk-go-v2/config v1.29.14/go.mod h1:wVPHWcIFv3WO89w0rE10gzf17ZYy+UVS1Geq8Iei34g=
+github.com/aws/aws-sdk-go-v2/credentials v1.17.67 h1:9KxtdcIA/5xPNQyZRgUSpYOE6j9Bc4+D7nZua0KGYOM=
+github.com/aws/aws-sdk-go-v2/credentials v1.17.67/go.mod h1:p3C44m+cfnbv763s52gCqrjaqyPikj9Sg47kUVaNZQQ=
 github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.30 h1:x793wxmUWVDhshP8WW2mlnXuFrO4cOd3HLBroh1paFw=
 github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.30/go.mod h1:Jpne2tDnYiFascUEs2AWHJL9Yp7A5ZVy3TNyxaAjD6M=
 github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.34 h1:ZK5jHhnrioRkUNOc+hOgQKlUL5JeC3S6JgLxtQ+Rm0Q=
@@ -64,8 +64,8 @@ github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.34 h1:SZwFm17ZUNNg5Np0io
 github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.34/go.mod h1:dFZsC0BLo346mvKQLWmoJxT+Sjp+qcVR1tRVHQGOH9Q=
 github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 h1:bIqFDwgGXXN1Kpp99pDOdKMTTb5d2KyU5X/BZxjOkRo=
 github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3/go.mod h1:H5O/EsxDWyU+LP/V8i5sm8cxoZgc2fdNR9bxlOFrQTo=
-github.com/aws/aws-sdk-go-v2/service/ecr v1.43.2 h1:G6Rj8A8zgukXWSSB8evVE8az2S3Ci/Q3CpSMxJsK4G0=
-github.com/aws/aws-sdk-go-v2/service/ecr v1.43.2/go.mod h1:iQ1skgw1XRK+6Lgkb0I9ODatAP72WoTILh0zXQ5DtbU=
+github.com/aws/aws-sdk-go-v2/service/ecr v1.43.3 h1:YyH8Hk73bYzdbvf6S8NF5z/fb/1stpiMnFSfL6jSfRA=
+github.com/aws/aws-sdk-go-v2/service/ecr v1.43.3/go.mod h1:iQ1skgw1XRK+6Lgkb0I9ODatAP72WoTILh0zXQ5DtbU=
 github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.3 h1:eAh2A4b5IzM/lum78bZ590jy36+d/aFLgKF/4Vd1xPE=
 github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.3/go.mod h1:0yKJC/kb8sAnmlYa6Zs3QVYqaC8ug2AbnNChv5Ox3uA=
 github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.15 h1:dM9/92u2F1JbDaGooxTq18wmmFzbJRfXfVfy96/1CXM=
@@ -74,8 +74,8 @@ github.com/aws/aws-sdk-go-v2/service/sso v1.25.3 h1:1Gw+9ajCV1jogloEv1RRnvfRFia2
 github.com/aws/aws-sdk-go-v2/service/sso v1.25.3/go.mod h1:qs4a9T5EMLl/Cajiw2TcbNt2UNo/Hqlyp+GiuG4CFDI=
 github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.1 h1:hXmVKytPfTy5axZ+fYbR5d0cFmC3JvwLm5kM83luako=
 github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.1/go.mod h1:MlYRNmYu/fGPoxBQVvBYr9nyr948aY/WLUvwBMBJubs=
-github.com/aws/aws-sdk-go-v2/service/sts v1.33.18 h1:xz7WvTMfSStb9Y8NpCT82FXLNC3QasqBfuAFHY4Pk5g=
-github.com/aws/aws-sdk-go-v2/service/sts v1.33.18/go.mod h1:cQnB8CUnxbMU82JvlqjKR2HBOm3fe9pWorWBza6MBJ4=
+github.com/aws/aws-sdk-go-v2/service/sts v1.33.19 h1:1XuUZ8mYJw9B6lzAkXhqHlJd/XvaX32evhproijJEZY=
+github.com/aws/aws-sdk-go-v2/service/sts v1.33.19/go.mod h1:cQnB8CUnxbMU82JvlqjKR2HBOm3fe9pWorWBza6MBJ4=
 github.com/aws/smithy-go v1.22.2 h1:6D9hW43xKFrRx/tXXfAlIZc4JI+yQe6snnWcQyxSyLQ=
 github.com/aws/smithy-go v1.22.2/go.mod h1:irrKGvNn1InZwb2d7fkIRNucdfwR8R+Ts3wxYa/cJHg=
 github.com/bacongobbler/browser v1.1.0 h1:6YTctUlzcApit1vpWgh+myjh8lQUyQRD2Ltoyvy2EoM=
@@ -186,8 +186,8 @@ github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66D
 github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic=
 github.com/go-git/go-billy/v5 v5.6.2 h1:6Q86EsPXMa7c3YZ3aLAQsMA0VlWmy43r6FHqa/UNbRM=
 github.com/go-git/go-billy/v5 v5.6.2/go.mod h1:rcFC2rAsp/erv7CMz9GczHcuD0D32fWzH+MJAU+jaUU=
-github.com/go-git/go-git/v5 v5.14.0 h1:/MD3lCrGjCen5WfEAzKg00MJJffKhC8gzS80ycmCi60=
-github.com/go-git/go-git/v5 v5.14.0/go.mod h1:Z5Xhoia5PcWA3NF8vRLURn9E5FRhSl7dGj9ItW3Wk5k=
+github.com/go-git/go-git/v5 v5.15.0 h1:f5Qn0W0F7ry1iN0ZwIU5m/n7/BKB4hiZfc+zlZx7ly0=
+github.com/go-git/go-git/v5 v5.15.0/go.mod h1:4Ge4alE/5gPs30F2H1esi2gPd69R0C39lolkucHBOp8=
 github.com/go-gorp/gorp/v3 v3.1.0 h1:ItKF/Vbuj31dmV4jxA1qblpSwkl9g1typ24xoe70IGs=
 github.com/go-gorp/gorp/v3 v3.1.0/go.mod h1:dLEjIyyRNiXvNZ8PSmzpt1GsWAUK8kjVhEpjH8TixEw=
 github.com/go-jose/go-jose/v4 v4.0.5 h1:M6T8+mKZl/+fNNuFHvGIzDz7BTLQPIounk/b9dw3AaE=
@@ -577,8 +577,8 @@ golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLL
 golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
 golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
 golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
-golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8=
-golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
+golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY=
+golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E=
 golang.org/x/oauth2 v0.29.0 h1:WdYw2tdTK1S8olAzWHdgeqfy+Mtm9XNhv/xJsY65d98=
 golang.org/x/oauth2 v0.29.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8=
 golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -660,8 +660,8 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
 gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 gotest.tools/v3 v3.4.0 h1:ZazjZUfuVeZGLAmlKKuyv3IKP5orXcwtOwDQH6YVr6o=
 gotest.tools/v3 v3.4.0/go.mod h1:CtbdzLSsqVhDgMtKsx03ird5YTGB3ar27v0u/yKBW5g=
-helm.sh/helm/v3 v3.17.2 h1:agYQ5ew2jq5vdx2K7q5W44KyKQrnSubUMCQsjkiv3/o=
-helm.sh/helm/v3 v3.17.2/go.mod h1:+uJKMH/UiMzZQOALR3XUf3BLIoczI2RKKD6bMhPh4G8=
+helm.sh/helm/v3 v3.17.3 h1:3n5rW3D0ArjFl0p4/oWO8IbY/HKaNNwJtOQFdH2AZHg=
+helm.sh/helm/v3 v3.17.3/go.mod h1:+uJKMH/UiMzZQOALR3XUf3BLIoczI2RKKD6bMhPh4G8=
 k8s.io/api v0.32.3 h1:Hw7KqxRusq+6QSplE3NYG4MBxZw1BZnq4aP4cJVINls=
 k8s.io/api v0.32.3/go.mod h1:2wEDTXADtm/HA7CCMD8D8bK4yuBUptzaRhYcYEEYA3k=
 k8s.io/apiextensions-apiserver v0.32.3 h1:4D8vy+9GWerlErCwVIbcQjsWunF9SUGNu7O7hiQTyPY=
diff --git a/hack/codegen/proto.sh b/hack/codegen/proto.sh
index f6905d07..9b5cce90 100755
--- a/hack/codegen/proto.sh
+++ b/hack/codegen/proto.sh
@@ -20,6 +20,36 @@ readonly APIMACHINERY_PKGS=(
   "-k8s.io/apimachinery/pkg/apis/meta/v1"
 )
 
+# Default to cleaning up the temporary directory
+CLEANUP_TMP="true"
+
+# Function to display usage
+function usage() {
+  echo "Usage: $0 [options]"
+  echo "Options:"
+  echo "  --no-cleanup    Don't clean up temporary directory after execution"
+  echo "  -h, --help      Display this help message"
+}
+
+# Parse command line arguments
+while [[ $# -gt 0 ]]; do
+  case "$1" in
+    --no-cleanup)
+      CLEANUP_TMP="false"
+      shift
+      ;;
+    -h|--help)
+      usage
+      exit 0
+      ;;
+    *)
+      echo "Unknown option: $1"
+      usage
+      exit 1
+      ;;
+  esac
+done
+
 function main() {
   set -x
 
@@ -78,9 +108,9 @@ function main() {
   { msg "Copying generated .proto and .pb.go files back to the project root..."; } 2> /dev/null
   find "$build_src_dir/api" \( \
     -name '*.proto' -o \
-    -name '*.pg.go' \
+    -name '*.pb.go' \
   \) -type f | while read -r file; do
-    rel_path="${file#$build_src_dir/}"
+    rel_path="${file#"$build_src_dir"/}"
     dest_file="$proj_dir/$rel_path"
     dest_dir=$(dirname "$dest_file")
     mkdir -p "$dest_dir"
@@ -138,6 +168,14 @@ function clean() {
   { msg "Cleaning up all intermediate resources..."; } 2> /dev/null
   rm -rf "${proj_dir}/vendor" || true
   rm -rf "${proj_dir}/tmp" || true
+
+  if [[ "${CLEANUP_TMP}" == "true" ]]; then
+    { msg "Cleaning up temporary directory..."; } 2> /dev/null
+    rm -rf "${tmp_dir}" || true
+  else
+    { msg "Temporary directory '${tmp_dir}' preserved"; } 2> /dev/null
+  fi
+
   { msg "Done"; } 2> /dev/null
 }
 
diff --git a/hack/tools/go.mod b/hack/tools/go.mod
index 831c4a05..7ed9f5c2 100644
--- a/hack/tools/go.mod
+++ b/hack/tools/go.mod
@@ -3,10 +3,10 @@ module tools
 go 1.24.1
 
 require (
-	github.com/bufbuild/buf v1.51.0
+	github.com/bufbuild/buf v1.52.1
 	github.com/golangci/golangci-lint v1.64.8
-	golang.org/x/tools v0.31.0
-	helm.sh/helm/v3 v3.17.2
+	golang.org/x/tools v0.32.0
+	helm.sh/helm/v3 v3.17.3
 	k8s.io/code-generator v0.32.3
 	sigs.k8s.io/controller-tools v0.17.3
 )
@@ -14,21 +14,21 @@ require (
 require (
 	4d63.com/gocheckcompilerdirectives v1.3.0 // indirect
 	4d63.com/gochecknoglobals v0.2.2 // indirect
-	buf.build/gen/go/bufbuild/bufplugin/protocolbuffers/go v1.36.5-20250121211742-6d880cc6cc8d.1 // indirect
-	buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.5-20250307204501-0409229c3780.1 // indirect
+	buf.build/gen/go/bufbuild/bufplugin/protocolbuffers/go v1.36.6-20250121211742-6d880cc6cc8d.1 // indirect
+	buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.6-20250307204501-0409229c3780.1 // indirect
 	buf.build/gen/go/bufbuild/registry/connectrpc/go v1.18.1-20250116203702-1c024d64352b.1 // indirect
-	buf.build/gen/go/bufbuild/registry/protocolbuffers/go v1.36.5-20250116203702-1c024d64352b.1 // indirect
-	buf.build/gen/go/pluginrpc/pluginrpc/protocolbuffers/go v1.36.4-20241007202033-cf42259fcbfc.1 // indirect
+	buf.build/gen/go/bufbuild/registry/protocolbuffers/go v1.36.6-20250116203702-1c024d64352b.1 // indirect
+	buf.build/gen/go/pluginrpc/pluginrpc/protocolbuffers/go v1.36.6-20241007202033-cf42259fcbfc.1 // indirect
 	buf.build/go/bufplugin v0.8.0 // indirect
-	buf.build/go/protoyaml v0.3.1 // indirect
+	buf.build/go/protoyaml v0.3.2 // indirect
 	buf.build/go/spdx v0.2.0 // indirect
-	cel.dev/expr v0.21.2 // indirect
+	cel.dev/expr v0.23.1 // indirect
 	connectrpc.com/connect v1.18.1 // indirect
 	connectrpc.com/otelconnect v0.7.2 // indirect
 	dario.cat/mergo v1.0.1 // indirect
 	github.com/4meepo/tagalign v1.4.2 // indirect
 	github.com/Abirdcfly/dupword v0.1.3 // indirect
-	github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24 // indirect
+	github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6 // indirect
 	github.com/Antonboom/errname v1.0.0 // indirect
 	github.com/Antonboom/nilnil v1.0.1 // indirect
 	github.com/Antonboom/testifylint v1.5.2 // indirect
@@ -63,8 +63,8 @@ require (
 	github.com/breml/bidichk v0.3.2 // indirect
 	github.com/breml/errchkjson v0.4.0 // indirect
 	github.com/bufbuild/protocompile v0.14.1 // indirect
-	github.com/bufbuild/protoplugin v0.0.0-20250106231243-3a819552c9d9 // indirect
-	github.com/bufbuild/protovalidate-go v0.9.3-0.20250317160558-38a17488914d // indirect
+	github.com/bufbuild/protoplugin v0.0.0-20250218205857-750e09ce93e1 // indirect
+	github.com/bufbuild/protovalidate-go v0.9.3-0.20250403190939-663657418457 // indirect
 	github.com/butuzov/ireturn v0.3.1 // indirect
 	github.com/butuzov/mirror v1.3.0 // indirect
 	github.com/catenacyber/perfsprint v0.8.2 // indirect
@@ -88,10 +88,10 @@ require (
 	github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
 	github.com/denis-tingaikin/go-header v0.5.0 // indirect
 	github.com/distribution/reference v0.6.0 // indirect
-	github.com/docker/cli v27.5.1+incompatible // indirect
+	github.com/docker/cli v28.0.4+incompatible // indirect
 	github.com/docker/distribution v2.8.3+incompatible // indirect
-	github.com/docker/docker v28.0.2+incompatible // indirect
-	github.com/docker/docker-credential-helpers v0.8.2 // indirect
+	github.com/docker/docker v28.0.4+incompatible // indirect
+	github.com/docker/docker-credential-helpers v0.9.3 // indirect
 	github.com/docker/go-connections v0.5.0 // indirect
 	github.com/docker/go-metrics v0.0.1 // indirect
 	github.com/docker/go-units v0.5.0 // indirect
@@ -146,7 +146,7 @@ require (
 	github.com/google/go-cmp v0.7.0 // indirect
 	github.com/google/go-containerregistry v0.20.3 // indirect
 	github.com/google/gofuzz v1.2.0 // indirect
-	github.com/google/pprof v0.0.0-20250302191652-9094ed2288e7 // indirect
+	github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 // indirect
 	github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
 	github.com/google/uuid v1.6.0 // indirect
 	github.com/gordonklaus/ineffassign v0.1.0 // indirect
@@ -216,7 +216,7 @@ require (
 	github.com/moby/sys/mountinfo v0.7.2 // indirect
 	github.com/moby/sys/reexec v0.1.0 // indirect
 	github.com/moby/sys/sequential v0.6.0 // indirect
-	github.com/moby/sys/user v0.3.0 // indirect
+	github.com/moby/sys/user v0.4.0 // indirect
 	github.com/moby/sys/userns v0.1.0 // indirect
 	github.com/moby/term v0.5.2 // indirect
 	github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
@@ -231,9 +231,9 @@ require (
 	github.com/nishanths/predeclared v0.2.2 // indirect
 	github.com/nunnatsa/ginkgolinter v0.19.1 // indirect
 	github.com/olekukonko/tablewriter v0.0.5 // indirect
-	github.com/onsi/ginkgo/v2 v2.22.2 // indirect
+	github.com/onsi/ginkgo/v2 v2.23.4 // indirect
 	github.com/opencontainers/go-digest v1.0.0 // indirect
-	github.com/opencontainers/image-spec v1.1.0 // indirect
+	github.com/opencontainers/image-spec v1.1.1 // indirect
 	github.com/pelletier/go-toml v1.9.5 // indirect
 	github.com/pelletier/go-toml/v2 v2.2.3 // indirect
 	github.com/peterbourgon/diskv v2.0.1+incompatible // indirect
@@ -315,30 +315,30 @@ require (
 	go.lsp.dev/protocol v0.12.0 // indirect
 	go.lsp.dev/uri v0.3.0 // indirect
 	go.opentelemetry.io/auto/sdk v1.1.0 // indirect
-	go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.59.0 // indirect
-	go.opentelemetry.io/otel v1.34.0 // indirect
-	go.opentelemetry.io/otel/metric v1.34.0 // indirect
-	go.opentelemetry.io/otel/trace v1.34.0 // indirect
+	go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 // indirect
+	go.opentelemetry.io/otel v1.35.0 // indirect
+	go.opentelemetry.io/otel/metric v1.35.0 // indirect
+	go.opentelemetry.io/otel/trace v1.35.0 // indirect
 	go.uber.org/automaxprocs v1.6.0 // indirect
 	go.uber.org/mock v0.5.0 // indirect
 	go.uber.org/multierr v1.11.0 // indirect
 	go.uber.org/zap v1.27.0 // indirect
 	go.uber.org/zap/exp v0.3.0 // indirect
-	golang.org/x/crypto v0.36.0 // indirect
-	golang.org/x/exp v0.0.0-20250228200357-dead58393ab7 // indirect
+	golang.org/x/crypto v0.37.0 // indirect
+	golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 // indirect
 	golang.org/x/exp/typeparams v0.0.0-20250210185358-939b2ce775ac // indirect
 	golang.org/x/mod v0.24.0 // indirect
-	golang.org/x/net v0.37.0 // indirect
+	golang.org/x/net v0.39.0 // indirect
 	golang.org/x/oauth2 v0.26.0 // indirect
-	golang.org/x/sync v0.12.0 // indirect
-	golang.org/x/sys v0.31.0 // indirect
-	golang.org/x/term v0.30.0 // indirect
-	golang.org/x/text v0.23.0 // indirect
+	golang.org/x/sync v0.13.0 // indirect
+	golang.org/x/sys v0.32.0 // indirect
+	golang.org/x/term v0.31.0 // indirect
+	golang.org/x/text v0.24.0 // indirect
 	golang.org/x/time v0.10.0 // indirect
-	google.golang.org/genproto/googleapis/api v0.0.0-20250224174004-546df14abb99 // indirect
-	google.golang.org/genproto/googleapis/rpc v0.0.0-20250224174004-546df14abb99 // indirect
-	google.golang.org/grpc v1.70.0 // indirect
-	google.golang.org/protobuf v1.36.5 // indirect
+	google.golang.org/genproto/googleapis/api v0.0.0-20250404141209-ee84b53bf3d0 // indirect
+	google.golang.org/genproto/googleapis/rpc v0.0.0-20250404141209-ee84b53bf3d0 // indirect
+	google.golang.org/grpc v1.71.0 // indirect
+	google.golang.org/protobuf v1.36.6 // indirect
 	gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect
 	gopkg.in/inf.v0 v0.9.1 // indirect
 	gopkg.in/ini.v1 v1.67.0 // indirect
diff --git a/hack/tools/go.sum b/hack/tools/go.sum
index d934f44d..685da4d8 100644
--- a/hack/tools/go.sum
+++ b/hack/tools/go.sum
@@ -2,24 +2,24 @@
 4d63.com/gocheckcompilerdirectives v1.3.0/go.mod h1:ofsJ4zx2QAuIP/NO/NAh1ig6R1Fb18/GI7RVMwz7kAY=
 4d63.com/gochecknoglobals v0.2.2 h1:H1vdnwnMaZdQW/N+NrkT1SZMTBmcwHe9Vq8lJcYYTtU=
 4d63.com/gochecknoglobals v0.2.2/go.mod h1:lLxwTQjL5eIesRbvnzIP3jZtG140FnTdz+AlMa+ogt0=
-buf.build/gen/go/bufbuild/bufplugin/protocolbuffers/go v1.36.5-20250121211742-6d880cc6cc8d.1 h1:z/NYWpgoeKkKL3+LYF+8QK58Rjz3qkMAshpdzJTaJ7o=
-buf.build/gen/go/bufbuild/bufplugin/protocolbuffers/go v1.36.5-20250121211742-6d880cc6cc8d.1/go.mod h1:LpnZWZGTs6IBCnY9WHAkR9X4/NbpL5nwOXivQdXILTs=
-buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.5-20250307204501-0409229c3780.1 h1:j+l4+E1EEo83GVIxuqinfFOTyImSQUH90WfufE86xaI=
-buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.5-20250307204501-0409229c3780.1/go.mod h1:eOqrCVUfhh7SLo00urDe/XhJHljj0dWMZirS0aX7cmc=
+buf.build/gen/go/bufbuild/bufplugin/protocolbuffers/go v1.36.6-20250121211742-6d880cc6cc8d.1 h1:f6miF8tK6H+Ktad24WpnNfpHO75GRGk0rhJ1mxPXqgA=
+buf.build/gen/go/bufbuild/bufplugin/protocolbuffers/go v1.36.6-20250121211742-6d880cc6cc8d.1/go.mod h1:rvbyamNtvJ4o3ExeCmaG5/6iHnu0vy0E+UQ+Ph0om8s=
+buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.6-20250307204501-0409229c3780.1 h1:zgJPqo17m28+Lf5BW4xv3PvU20BnrmTcGYrog22lLIU=
+buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.6-20250307204501-0409229c3780.1/go.mod h1:avRlCjnFzl98VPaeCtJ24RrV/wwHFzB8sWXhj26+n/U=
 buf.build/gen/go/bufbuild/registry/connectrpc/go v1.18.1-20250116203702-1c024d64352b.1 h1:1SDs5tEGoWWv2vmKLx2B0Bp+yfhlxiU4DaZUII8+Pvs=
 buf.build/gen/go/bufbuild/registry/connectrpc/go v1.18.1-20250116203702-1c024d64352b.1/go.mod h1:o2AgVM1j3MczvxnMqfZTpiqGwK1VD4JbEagseY0QcjE=
-buf.build/gen/go/bufbuild/registry/protocolbuffers/go v1.36.5-20250116203702-1c024d64352b.1 h1:MTNYELBBDEj2ddEwWb/vuAm5PRLyWtZe7CLnc6WZ5qQ=
-buf.build/gen/go/bufbuild/registry/protocolbuffers/go v1.36.5-20250116203702-1c024d64352b.1/go.mod h1:E8bt4dG1/NfuocvVmlDNWIfKoLK0B4AgGq4ubwEGBvo=
-buf.build/gen/go/pluginrpc/pluginrpc/protocolbuffers/go v1.36.4-20241007202033-cf42259fcbfc.1 h1:XmYgi9W/9oST2ZrfT3ucGWkzD9+Vd0ls9yhyZ8ae0KQ=
-buf.build/gen/go/pluginrpc/pluginrpc/protocolbuffers/go v1.36.4-20241007202033-cf42259fcbfc.1/go.mod h1:cxFpqWIC80Wm8YNo1038ocBmrF84uQ0IfL0uVdAu9ZY=
+buf.build/gen/go/bufbuild/registry/protocolbuffers/go v1.36.6-20250116203702-1c024d64352b.1 h1:O1sbHpYA7yAIZpDWSEw0mNibv1gov2KH8mSzPruCNhk=
+buf.build/gen/go/bufbuild/registry/protocolbuffers/go v1.36.6-20250116203702-1c024d64352b.1/go.mod h1:ee69ieBAzwc/oY/Vde0K4r6JWvrk093q4Z/FXexPMmA=
+buf.build/gen/go/pluginrpc/pluginrpc/protocolbuffers/go v1.36.6-20241007202033-cf42259fcbfc.1 h1:trcsXBDm8exui7mvndZnvworCyBq1xuMnod2N0j79K8=
+buf.build/gen/go/pluginrpc/pluginrpc/protocolbuffers/go v1.36.6-20241007202033-cf42259fcbfc.1/go.mod h1:OUbhXurY+VHFGn9FBxcRy8UB7HXk9NvJ2qCgifOMypQ=
 buf.build/go/bufplugin v0.8.0 h1:YgR1+CNGmzR69jt85oRWTa5FioZoX/tOrHV+JxfNnnk=
 buf.build/go/bufplugin v0.8.0/go.mod h1:rcm0Esd3P/GM2rtYTvz3+9Gf8w9zdo7rG8dKSxYHHIE=
-buf.build/go/protoyaml v0.3.1 h1:ucyzE7DRnjX+mQ6AH4JzN0Kg50ByHHu+yrSKbgQn2D4=
-buf.build/go/protoyaml v0.3.1/go.mod h1:0TzNpFQDXhwbkXb/ajLvxIijqbve+vMQvWY/b3/Dzxg=
+buf.build/go/protoyaml v0.3.2 h1:QJF3k7btMameIadLLcK3Rry81OK3gYA5nZMXirV1Bs4=
+buf.build/go/protoyaml v0.3.2/go.mod h1:rUlMqwfZeONS/BAt00wB6jV5ay/eHXUzxgiKSIyrvyc=
 buf.build/go/spdx v0.2.0 h1:IItqM0/cMxvFJJumcBuP8NrsIzMs/UYjp/6WSpq8LTw=
 buf.build/go/spdx v0.2.0/go.mod h1:bXdwQFem9Si3nsbNy8aJKGPoaPi5DKwdeEp5/ArZ6w8=
-cel.dev/expr v0.21.2 h1:o+Wj235dy4gFYlYin3JsMpp3EEfMrPm/6tdoyjT98S0=
-cel.dev/expr v0.21.2/go.mod h1:MrpN08Q+lEBs+bGYdLxxHkZoUSsCp0nSKTs0nTymJgw=
+cel.dev/expr v0.23.1 h1:K4KOtPCJQjVggkARsjG9RWXP6O4R73aHeJMa/dmCQQg=
+cel.dev/expr v0.23.1/go.mod h1:hLPLo1W4QUmuYdA72RBX06QTs6MXw941piREPl3Yfiw=
 connectrpc.com/connect v1.18.1 h1:PAg7CjSAGvscaf6YZKUefjoih5Z/qYkyaTrBW8xvYPw=
 connectrpc.com/connect v1.18.1/go.mod h1:0292hj1rnx8oFrStN7cB4jjVBeqs+Yx5yDIC2prWDO8=
 connectrpc.com/otelconnect v0.7.2 h1:WlnwFzaW64dN06JXU+hREPUGeEzpz3Acz2ACOmN8cMI=
@@ -32,8 +32,8 @@ github.com/4meepo/tagalign v1.4.2 h1:0hcLHPGMjDyM1gHG58cS73aQF8J4TdVR96TZViorO9E
 github.com/4meepo/tagalign v1.4.2/go.mod h1:+p4aMyFM+ra7nb41CnFG6aSDXqRxU/w1VQqScKqDARI=
 github.com/Abirdcfly/dupword v0.1.3 h1:9Pa1NuAsZvpFPi9Pqkd93I7LIYRURj+A//dFd5tgBeE=
 github.com/Abirdcfly/dupword v0.1.3/go.mod h1:8VbB2t7e10KRNdwTVoxdBaxla6avbhGzb8sCTygUMhw=
-github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24 h1:bvDV9vkmnHYOMsOr4WLk+Vo07yKIzd94sVoIqshQ4bU=
-github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8=
+github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6 h1:He8afgbRMd7mFxO99hRNu+6tazq8nFF9lIwo9JFroBk=
+github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8=
 github.com/Antonboom/errname v1.0.0 h1:oJOOWR07vS1kRusl6YRSlat7HFnb3mSfMl6sDMRoTBA=
 github.com/Antonboom/errname v1.0.0/go.mod h1:gMOBFzK/vrTiXN9Oh+HFs+e6Ndl0eTFbtsRTSRdXyGI=
 github.com/Antonboom/nilnil v1.0.1 h1:C3Tkm0KUxgfO4Duk3PM+ztPncTFlOf0b2qadmS0s4xs=
@@ -116,14 +116,14 @@ github.com/breml/errchkjson v0.4.0 h1:gftf6uWZMtIa/Is3XJgibewBm2ksAQSY/kABDNFTAd
 github.com/breml/errchkjson v0.4.0/go.mod h1:AuBOSTHyLSaaAFlWsRSuRBIroCh3eh7ZHh5YeelDIk8=
 github.com/bshuster-repo/logrus-logstash-hook v1.0.0 h1:e+C0SB5R1pu//O4MQ3f9cFuPGoOVeF2fE4Og9otCc70=
 github.com/bshuster-repo/logrus-logstash-hook v1.0.0/go.mod h1:zsTqEiSzDgAa/8GZR7E1qaXrhYNDKBYy5/dWPTIflbk=
-github.com/bufbuild/buf v1.51.0 h1:k2we7gmuSDeIqxkv16F/8s5Kk0l2ZfvMHpvC1n6o5Rk=
-github.com/bufbuild/buf v1.51.0/go.mod h1:TbX4Df3BfE0Lugd3Y3sFr7QTxqmCfPkuiEexe29KZeE=
+github.com/bufbuild/buf v1.52.1 h1:tAQ9rr9c8j8XRc9xvVvRFv5ztpiIyu32a6AUanWp34Y=
+github.com/bufbuild/buf v1.52.1/go.mod h1:htcJKxeVf7xZTCUmPZlh4OrHenMvTsvEkkxiLrEXf2I=
 github.com/bufbuild/protocompile v0.14.1 h1:iA73zAf/fyljNjQKwYzUHD6AD4R8KMasmwa/FBatYVw=
 github.com/bufbuild/protocompile v0.14.1/go.mod h1:ppVdAIhbr2H8asPk6k4pY7t9zB1OU5DoEw9xY/FUi1c=
-github.com/bufbuild/protoplugin v0.0.0-20250106231243-3a819552c9d9 h1:kAWER21DzhzU7ys8LL1WkSfbGkwXv+tM30hyEsYrW2k=
-github.com/bufbuild/protoplugin v0.0.0-20250106231243-3a819552c9d9/go.mod h1:c5D8gWRIZ2HLWO3gXYTtUfw/hbJyD8xikv2ooPxnklQ=
-github.com/bufbuild/protovalidate-go v0.9.3-0.20250317160558-38a17488914d h1:Y6Yp/LwSaRG8gw9GyyQD7jensL9NXqPlkbuulaAvCEE=
-github.com/bufbuild/protovalidate-go v0.9.3-0.20250317160558-38a17488914d/go.mod h1:SZN6Qr3lPWuKMoQtIhKdhESkb+3m2vk0lqN9WMuZDDU=
+github.com/bufbuild/protoplugin v0.0.0-20250218205857-750e09ce93e1 h1:V1xulAoqLqVg44rY97xOR+mQpD2N+GzhMHVwJ030WEU=
+github.com/bufbuild/protoplugin v0.0.0-20250218205857-750e09ce93e1/go.mod h1:c5D8gWRIZ2HLWO3gXYTtUfw/hbJyD8xikv2ooPxnklQ=
+github.com/bufbuild/protovalidate-go v0.9.3-0.20250403190939-663657418457 h1:Sa5rWJF1c3HdWoF5QcBDyCIoqPdIQf1Jh4HTos7UZsM=
+github.com/bufbuild/protovalidate-go v0.9.3-0.20250403190939-663657418457/go.mod h1:2lUDP6fNd3wxznRNH3Nj64VB07+PySeslamkerwP6tE=
 github.com/bugsnag/bugsnag-go v0.0.0-20141110184014-b1d153021fcd h1:rFt+Y/IK1aEZkEHchZRSq9OQbsSzIT/OrI8YFFmRIng=
 github.com/bugsnag/bugsnag-go v0.0.0-20141110184014-b1d153021fcd/go.mod h1:2oa8nejYd4cQ/b0hMIopN0lCRxU0bueqREvZLWFrtK8=
 github.com/bugsnag/osext v0.0.0-20130617224835-0dd3f918b21b h1:otBG+dV+YK+Soembjv71DPz3uX/V/6MMlSyD9JBQ6kQ=
@@ -200,14 +200,14 @@ github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5Qvfr
 github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
 github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI=
 github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
-github.com/docker/cli v27.5.1+incompatible h1:JB9cieUT9YNiMITtIsguaN55PLOHhBSz3LKVc6cqWaY=
-github.com/docker/cli v27.5.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
+github.com/docker/cli v28.0.4+incompatible h1:pBJSJeNd9QeIWPjRcV91RVJihd/TXB77q1ef64XEu4A=
+github.com/docker/cli v28.0.4+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
 github.com/docker/distribution v2.8.3+incompatible h1:AtKxIZ36LoNK51+Z6RpzLpddBirtxJnzDrHLEKxTAYk=
 github.com/docker/distribution v2.8.3+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
-github.com/docker/docker v28.0.2+incompatible h1:9BILleFwug5FSSqWBgVevgL3ewDJfWWWyZVqlDMttE8=
-github.com/docker/docker v28.0.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
-github.com/docker/docker-credential-helpers v0.8.2 h1:bX3YxiGzFP5sOXWc3bTPEXdEaZSeVMrFgOr3T+zrFAo=
-github.com/docker/docker-credential-helpers v0.8.2/go.mod h1:P3ci7E3lwkZg6XiHdRKft1KckHiO9a2rNtyFbZ/ry9M=
+github.com/docker/docker v28.0.4+incompatible h1:JNNkBctYKurkw6FrHfKqY0nKIDf5nrbxjVBtS+cdcok=
+github.com/docker/docker v28.0.4+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
+github.com/docker/docker-credential-helpers v0.9.3 h1:gAm/VtF9wgqJMoxzT3Gj5p4AqIjCBS4wrsOh9yRqcz8=
+github.com/docker/docker-credential-helpers v0.9.3/go.mod h1:x+4Gbw9aGmChi3qTLZj8Dfn0TD20M/fuWy0E5+WDeCo=
 github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c=
 github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc=
 github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c h1:+pKlWGMw7gf6bQ+oDZB4KHQFypsfjYlq/C4rfL7D3g8=
@@ -365,8 +365,8 @@ github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
 github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
 github.com/google/pprof v0.0.0-20211214055906-6f57359322fd/go.mod h1:KgnwoLYCZ8IQu3XUZ8Nc/bM9CCZFOyjUNOSygVozoDg=
 github.com/google/pprof v0.0.0-20240227163752-401108e1b7e7/go.mod h1:czg5+yv1E0ZGTi6S6vVK1mke0fV+FaUhNGcd6VRS9Ik=
-github.com/google/pprof v0.0.0-20250302191652-9094ed2288e7 h1:+J3r2e8+RsmN3vKfo75g0YSY61ms37qzPglu4p0sGro=
-github.com/google/pprof v0.0.0-20250302191652-9094ed2288e7/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144=
+github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 h1:BHT72Gu3keYf3ZEu2J0b1vyeLSOYI8bm5wbJM/8yDe8=
+github.com/google/pprof v0.0.0-20250403155104-27863c87afa6/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
 github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4=
 github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ=
 github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
@@ -552,8 +552,8 @@ github.com/moby/sys/reexec v0.1.0 h1:RrBi8e0EBTLEgfruBOFcxtElzRGTEUkeIFaVXgU7wok
 github.com/moby/sys/reexec v0.1.0/go.mod h1:EqjBg8F3X7iZe5pU6nRZnYCMUTXoxsjiIfHup5wYIN8=
 github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU=
 github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko=
-github.com/moby/sys/user v0.3.0 h1:9ni5DlcW5an3SvRSx4MouotOygvzaXbaSrc/wGDFWPo=
-github.com/moby/sys/user v0.3.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85uoHjs=
+github.com/moby/sys/user v0.4.0 h1:jhcMKit7SA80hivmFJcbB1vqmw//wU61Zdui2eQXuMs=
+github.com/moby/sys/user v0.4.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85uoHjs=
 github.com/moby/sys/userns v0.1.0 h1:tVLXkFOxVu9A64/yh59slHVv9ahO9UIev4JZusOLG/g=
 github.com/moby/sys/userns v0.1.0/go.mod h1:IHUYgu/kao6N8YZlp9Cf444ySSvCmDlmzUcYfDHOl28=
 github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ=
@@ -590,16 +590,16 @@ github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N
 github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
 github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE=
 github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU=
-github.com/onsi/ginkgo/v2 v2.22.2 h1:/3X8Panh8/WwhU/3Ssa6rCKqPLuAkVY2I0RoyDLySlU=
-github.com/onsi/ginkgo/v2 v2.22.2/go.mod h1:oeMosUL+8LtarXBHu/c0bx2D/K9zyQ6uX3cTyztHwsk=
-github.com/onsi/gomega v1.36.2 h1:koNYke6TVk6ZmnyHrCXba/T/MoLBXFjeC1PtvYgw0A8=
-github.com/onsi/gomega v1.36.2/go.mod h1:DdwyADRjrc825LhMEkD76cHR5+pUnjhUN8GlHlRPHzY=
+github.com/onsi/ginkgo/v2 v2.23.4 h1:ktYTpKJAVZnDT4VjxSbiBenUjmlL/5QkBEocaWXiQus=
+github.com/onsi/ginkgo/v2 v2.23.4/go.mod h1:Bt66ApGPBFzHyR+JO10Zbt0Gsp4uWxu5mIOTusL46e8=
+github.com/onsi/gomega v1.36.3 h1:hID7cr8t3Wp26+cYnfcjR6HpJ00fdogN6dqZ1t6IylU=
+github.com/onsi/gomega v1.36.3/go.mod h1:8D9+Txp43QWKhM24yyOBEdpkzN8FvJyAwecBgsU4KU0=
 github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
 github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
-github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug=
-github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM=
-github.com/opencontainers/selinux v1.11.0 h1:+5Zbo97w3Lbmb3PeqQtpmTkMwsW5nRI3YaLpt7tQ7oU=
-github.com/opencontainers/selinux v1.11.0/go.mod h1:E5dMC3VPuVvVHDYmi78qvhJp8+M586T4DlDRYpFkyec=
+github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040=
+github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M=
+github.com/opencontainers/selinux v1.12.0 h1:6n5JV4Cf+4y0KNXW48TLj5DwfXpvWlxXplUkdTrmPb8=
+github.com/opencontainers/selinux v1.12.0/go.mod h1:BTPX+bjVbWGXw7ZZWUbdENt8w0htPSrlgOOysQaU62U=
 github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde/go.mod h1:nZgzbfBr3hhjoZnS66nKrHmduYNpc34ny7RK4z5/HM0=
 github.com/otiai10/copy v1.2.0/go.mod h1:rrF5dJ5F0t/EWSYODDu4j9/vEeYHMkc8jt0zJChqQWw=
 github.com/otiai10/copy v1.14.0 h1:dCI/t1iTdYGtkvCuBG2BgR6KZa83PTclw4U5n2wAllU=
@@ -831,24 +831,24 @@ go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
 go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
 go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
 go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
-go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.59.0 h1:CV7UdSGJt/Ao6Gp4CXckLxVRRsRgDHoI8XjbL3PDl8s=
-go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.59.0/go.mod h1:FRmFuRJfag1IZ2dPkHnEoSFVgTVPUd2qf5Vi69hLb8I=
-go.opentelemetry.io/otel v1.34.0 h1:zRLXxLCgL1WyKsPVrgbSdMN4c0FMkDAskSTQP+0hdUY=
-go.opentelemetry.io/otel v1.34.0/go.mod h1:OWFPOQ+h4G8xpyjgqo4SxJYdDQ/qmRH+wivy7zzx9oI=
-go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.28.0 h1:3Q/xZUyC1BBkualc9ROb4G8qkH90LXEIICcs5zv1OYY=
-go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.28.0/go.mod h1:s75jGIWA9OfCMzF0xr+ZgfrB5FEbbV7UuYo32ahUiFI=
+go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 h1:sbiXRNDSWJOTobXh5HyQKjq6wUC5tNybqjIqDpAY4CU=
+go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0/go.mod h1:69uWxva0WgAA/4bu2Yy70SLDBwZXuQ6PbBpbsa5iZrQ=
+go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ=
+go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0 h1:1fTNlAIJZGWLP5FVu0fikVry1IsiUnXjf7QFvoNN3Xw=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0/go.mod h1:zjPK58DtkqQFn+YUMbx0M2XV3QgKU0gS9LeGohREyK4=
 go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.33.0 h1:wpMfgF8E1rkrT1Z6meFh1NDtownE9Ii3n3X2GJYjsaU=
 go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.33.0/go.mod h1:wAy0T/dUbs468uOlkT31xjvqQgEVXv58BRFWEgn5v/0=
-go.opentelemetry.io/otel/metric v1.34.0 h1:+eTR3U0MyfWjRDhmFMxe2SsW64QrZ84AOhvqS7Y+PoQ=
-go.opentelemetry.io/otel/metric v1.34.0/go.mod h1:CEDrp0fy2D0MvkXE+dPV7cMi8tWZwX3dmaIhwPOaqHE=
-go.opentelemetry.io/otel/sdk v1.33.0 h1:iax7M131HuAm9QkZotNHEfstof92xM+N8sr3uHXc2IM=
-go.opentelemetry.io/otel/sdk v1.33.0/go.mod h1:A1Q5oi7/9XaMlIWzPSxLRWOI8nG3FnzHJNbiENQuihM=
-go.opentelemetry.io/otel/sdk/metric v1.32.0 h1:rZvFnvmvawYb0alrYkjraqJq0Z4ZUJAiyYCU9snn1CU=
-go.opentelemetry.io/otel/sdk/metric v1.32.0/go.mod h1:PWeZlq0zt9YkYAp3gjKZ0eicRYvOh1Gd+X99x6GHpCQ=
-go.opentelemetry.io/otel/trace v1.34.0 h1:+ouXS2V8Rd4hp4580a8q23bg0azF2nI8cqLYnC8mh/k=
-go.opentelemetry.io/otel/trace v1.34.0/go.mod h1:Svm7lSjQD7kG7KJ/MUHPVXSDGz2OX4h0M2jHBhmSfRE=
-go.opentelemetry.io/proto/otlp v1.3.1 h1:TrMUixzpM0yuc/znrFTP9MMRh8trP93mkCiDVeXrui0=
-go.opentelemetry.io/proto/otlp v1.3.1/go.mod h1:0X1WI4de4ZsLrrJNLAQbFeLCm3T7yBkR0XqQ7niQU+8=
+go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M=
+go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE=
+go.opentelemetry.io/otel/sdk v1.35.0 h1:iPctf8iprVySXSKJffSS79eOjl9pvxV9ZqOWT0QejKY=
+go.opentelemetry.io/otel/sdk v1.35.0/go.mod h1:+ga1bZliga3DxJ3CQGg3updiaAJoNECOgJREo9KHGQg=
+go.opentelemetry.io/otel/sdk/metric v1.35.0 h1:1RriWBmCKgkeHEhM7a2uMjMUfP7MsOF5JpUCaEqEI9o=
+go.opentelemetry.io/otel/sdk/metric v1.35.0/go.mod h1:is6XYCUMpcKi+ZsOvfluY5YstFnhW0BidkR+gL+qN+w=
+go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs=
+go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc=
+go.opentelemetry.io/proto/otlp v1.5.0 h1:xJvq7gMzB31/d406fB8U5CBdyQGw4P399D1aQWU/3i4=
+go.opentelemetry.io/proto/otlp v1.5.0/go.mod h1:keN8WnHxOy8PG0rQZjJJ5A2ebUoafqWp0eVQ4yIXvJ4=
 go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs=
 go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8=
 go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
@@ -868,10 +868,10 @@ golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPh
 golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
 golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
 golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
-golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34=
-golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc=
-golang.org/x/exp v0.0.0-20250228200357-dead58393ab7 h1:aWwlzYV971S4BXRS9AmqwDLAD85ouC6X+pocatKY58c=
-golang.org/x/exp v0.0.0-20250228200357-dead58393ab7/go.mod h1:BHOTPb3L19zxehTsLoJXVaTktb06DFgmdW6Wb9s8jqk=
+golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE=
+golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc=
+golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 h1:nDVHiLt8aIbd/VzvPWN6kSOPE7+F/fNFDSXLVYkE/Iw=
+golang.org/x/exp v0.0.0-20250305212735-054e65f0b394/go.mod h1:sIifuuw/Yco/y6yb6+bDNfyeQ/MdPUy/hKEMYQV17cM=
 golang.org/x/exp/typeparams v0.0.0-20220428152302-39d4317da171/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk=
 golang.org/x/exp/typeparams v0.0.0-20230203172020-98cc5a0785f9/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk=
 golang.org/x/exp/typeparams v0.0.0-20250210185358-939b2ce775ac h1:TSSpLIG4v+p0rPv1pNOQtl1I8knsO4S9trOxNMOLVP4=
@@ -906,8 +906,8 @@ golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
 golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
 golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
 golang.org/x/net v0.16.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
-golang.org/x/net v0.37.0 h1:1zLorHbz+LYj7MQlSf1+2tPIIgibq2eL5xkrGk6f+2c=
-golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
+golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY=
+golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E=
 golang.org/x/oauth2 v0.26.0 h1:afQXWNNaeC4nvZ0Ed9XvCCzXM6UHJG7iCg0W4fPqSBE=
 golang.org/x/oauth2 v0.26.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
 golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -921,8 +921,8 @@ golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJ
 golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
 golang.org/x/sync v0.4.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
-golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw=
-golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
+golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610=
+golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
 golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@@ -951,8 +951,8 @@ golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
-golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
+golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20=
+golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
 golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
 golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
@@ -961,8 +961,8 @@ golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
 golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
 golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
 golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U=
-golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y=
-golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g=
+golang.org/x/term v0.31.0 h1:erwDkOK1Msy6offm1mOgvspSkslFnIGsFnxOKoufg3o=
+golang.org/x/term v0.31.0/go.mod h1:R4BeIy7D95HzImkxGkTW1UQTtP54tio2RyHz7PwK0aw=
 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
 golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
@@ -973,8 +973,8 @@ golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
 golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
 golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
 golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
-golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
-golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
+golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0=
+golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU=
 golang.org/x/time v0.10.0 h1:3usCWA8tQn0L8+hFJQNgzpWbd89begxN66o1Ojdn5L4=
 golang.org/x/time v0.10.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
 golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
@@ -997,20 +997,20 @@ golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
 golang.org/x/tools v0.7.0/go.mod h1:4pg6aUX35JBAogB10C9AtvVL+qowtN4pT3CGSQex14s=
 golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
 golang.org/x/tools v0.14.0/go.mod h1:uYBEerGOWcJyEORxN+Ek8+TT266gXkNlHdJBwexUsBg=
-golang.org/x/tools v0.31.0 h1:0EedkvKDbh+qistFTd0Bcwe/YLh4vHwWEkiI0toFIBU=
-golang.org/x/tools v0.31.0/go.mod h1:naFTU+Cev749tSJRXJlna0T3WxKvb1kWEx15xA4SdmQ=
+golang.org/x/tools v0.32.0 h1:Q7N1vhpkQv7ybVzLFtTjvQya2ewbwNDZzUgfXGqtMWU=
+golang.org/x/tools v0.32.0/go.mod h1:ZxrU41P/wAbZD8EDa6dDCa6XfpkhJ7HFMjHJXfBDu8s=
 golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
-google.golang.org/genproto/googleapis/api v0.0.0-20250224174004-546df14abb99 h1:ilJhrCga0AptpJZXmUYG4MCrx/zf3l1okuYz7YK9PPw=
-google.golang.org/genproto/googleapis/api v0.0.0-20250224174004-546df14abb99/go.mod h1:Xsh8gBVxGCcbV8ZeTB9wI5XPyZ5RvC6V3CTeeplHbiA=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20250224174004-546df14abb99 h1:ZSlhAUqC4r8TPzqLXQ0m3upBNZeF+Y8jQ3c4CR3Ujms=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20250224174004-546df14abb99/go.mod h1:LuRYeWDFV6WOn90g357N17oMCaxpgCnbi/44qJvDn2I=
-google.golang.org/grpc v1.70.0 h1:pWFv03aZoHzlRKHWicjsZytKAiYCtNS0dHbXnIdq7jQ=
-google.golang.org/grpc v1.70.0/go.mod h1:ofIJqVKDXx/JiXrwr2IG4/zwdH9txy3IlF40RmcJSQw=
-google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM=
-google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
+google.golang.org/genproto/googleapis/api v0.0.0-20250404141209-ee84b53bf3d0 h1:Qbb5RVn5xzI4naMJSpJ7lhvmos6UwZkbekd5Uz7rt9E=
+google.golang.org/genproto/googleapis/api v0.0.0-20250404141209-ee84b53bf3d0/go.mod h1:6T35kB3IPpdw7Wul09by0G/JuOuIFkXV6OOvt8IZeT8=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20250404141209-ee84b53bf3d0 h1:0K7wTWyzxZ7J+L47+LbFogJW1nn/gnnMCN0vGXNYtTI=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20250404141209-ee84b53bf3d0/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
+google.golang.org/grpc v1.71.0 h1:kF77BGdPTQ4/JZWMlb9VpJ5pa25aqvVqogsxNHHdeBg=
+google.golang.org/grpc v1.71.0/go.mod h1:H0GRtasmQOh9LkFoCPDu3ZrwUtD1YGE+b2vYBYd/8Ec=
+google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
+google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
 gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
@@ -1032,8 +1032,8 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
 gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 gotest.tools/v3 v3.4.0 h1:ZazjZUfuVeZGLAmlKKuyv3IKP5orXcwtOwDQH6YVr6o=
 gotest.tools/v3 v3.4.0/go.mod h1:CtbdzLSsqVhDgMtKsx03ird5YTGB3ar27v0u/yKBW5g=
-helm.sh/helm/v3 v3.17.2 h1:agYQ5ew2jq5vdx2K7q5W44KyKQrnSubUMCQsjkiv3/o=
-helm.sh/helm/v3 v3.17.2/go.mod h1:+uJKMH/UiMzZQOALR3XUf3BLIoczI2RKKD6bMhPh4G8=
+helm.sh/helm/v3 v3.17.3 h1:3n5rW3D0ArjFl0p4/oWO8IbY/HKaNNwJtOQFdH2AZHg=
+helm.sh/helm/v3 v3.17.3/go.mod h1:+uJKMH/UiMzZQOALR3XUf3BLIoczI2RKKD6bMhPh4G8=
 honnef.co/go/tools v0.6.1 h1:R094WgE8K4JirYjBaOpz/AvTyUu/3wbmAoskKN/pxTI=
 honnef.co/go/tools v0.6.1/go.mod h1:3puzxxljPCe8RGJX7BIy1plGbxEOZni5mR2aXe3/uk4=
 k8s.io/api v0.32.2 h1:bZrMLEkgizC24G9eViHGOPbW+aRo9duEISRIJKfdJuw=
diff --git a/internal/api/event.go b/internal/api/event.go
index 02d183d3..ce8d250a 100644
--- a/internal/api/event.go
+++ b/internal/api/event.go
@@ -1,6 +1,8 @@
 package api
 
 import (
+	"fmt"
+	"os"
 	"time"
 
 	authnv1 "k8s.io/api/authentication/v1"
@@ -21,28 +23,23 @@ func FormatEventControllerActor(name string) string {
 // 3. If the subject is available, it returns subject in "subject:<subject>" format.
 // 4. Otherwise, it returns EventActorUnknown.
 func FormatEventUserActor(u user.Info) string {
-	var email, subject string
+	if u.IsAdmin {
+		return kargoapi.EventActorAdmin
+	}
+	if u.Username != "" {
+		return formatOIDCUsername(u.Username)
+	}
 	if emailClaim, ok := u.Claims["email"]; ok {
-		if emailStr, ok := emailClaim.(string); ok {
-			email = emailStr
+		if email, ok := emailClaim.(string); ok {
+			return kargoapi.EventActorEmailPrefix + email
 		}
 	}
 	if subClaim, ok := u.Claims["sub"]; ok {
-		if subStr, ok := subClaim.(string); ok {
-			subject = subStr
+		if sub, ok := subClaim.(string); ok {
+			return kargoapi.EventActorSubjectPrefix + sub
 		}
 	}
-
-	switch {
-	case u.IsAdmin:
-		return kargoapi.EventActorAdmin
-	case email != "":
-		return kargoapi.EventActorEmailPrefix + email
-	case subject != "":
-		return kargoapi.EventActorSubjectPrefix + subject
-	default:
-		return kargoapi.EventActorUnknown
-	}
+	return kargoapi.EventActorUnknown
 }
 
 func FormatEventKubernetesUserActor(u authnv1.UserInfo) string {
@@ -62,3 +59,7 @@ func NewFreightApprovedEventAnnotations(actor string, f *kargoapi.Freight, stage
 	}
 	return annotations
 }
+
+func formatOIDCUsername(oidcUsername string) string {
+	return fmt.Sprintf("%s:%s", os.Getenv("OIDC_USERNAME_CLAIM"), oidcUsername)
+}
diff --git a/internal/controller/freight/finder.go b/internal/controller/freight/finder.go
index 2a4c0127..49b509b0 100644
--- a/internal/controller/freight/finder.go
+++ b/internal/controller/freight/finder.go
@@ -73,9 +73,7 @@ func FindCommit(
 		}
 	}
 	if desiredOrigin == nil {
-		return nil, NotFoundError{
-			msg: fmt.Sprintf("commit from repo %s not found in referenced Freight", repoURL),
-		}
+		return nil, nil
 	}
 	// We know exactly what we're after, so this should be easy
 	for i := range freight {
@@ -92,9 +90,7 @@ func FindCommit(
 	// If we get to here, we looked at all the FreightReferences and didn't find
 	// any that came from the desired origin. This could be because no Freight
 	// from the desired origin has been promoted yet.
-	return nil, NotFoundError{
-		msg: fmt.Sprintf("commit from repo %s not found in referenced Freight", repoURL),
-	}
+	return nil, nil
 }
 
 func FindImage(
@@ -147,9 +143,7 @@ func FindImage(
 	if desiredOrigin == nil {
 		// There is no chance of finding the commit we're looking for. Just return
 		// nil and let the caller decide what to do.
-		return nil, NotFoundError{
-			msg: fmt.Sprintf("image from repo %s not found in referenced Freight", repoURL),
-		}
+		return nil, nil
 	}
 	// We know exactly what we're after, so this should be easy
 	for _, f := range freight {
@@ -163,10 +157,9 @@ func FindImage(
 	}
 	// If we get to here, we looked at all the FreightReferences and didn't find
 	// any that came from the desired origin. This could be because no Freight
-	// from the desired origin has been promoted yet.
-	return nil, NotFoundError{
-		msg: fmt.Sprintf("image from repo %s not found in referenced Freight", repoURL),
-	}
+	// from the desired origin has been promoted yet. We return nil to indicate
+	// that none was found
+	return nil, nil
 }
 
 func HasAmbiguousImageRequest(
@@ -285,12 +278,5 @@ func FindChart(
 	// If we get to here, we looked at all the FreightReferences and didn't find
 	// any that came from the desired origin. This could be because no Freight
 	// from the desired origin has been promoted yet.
-	if chartName == "" {
-		return nil, NotFoundError{
-			msg: fmt.Sprintf("chart from repo %s not found in referenced Freight", repoURL),
-		}
-	}
-	return nil, NotFoundError{
-		msg: fmt.Sprintf("chart %q from repo %s not found in referenced Freight", chartName, repoURL),
-	}
+	return nil, nil
 }
diff --git a/internal/controller/promotions/promotions.go b/internal/controller/promotions/promotions.go
index d6540e50..733e6021 100644
--- a/internal/controller/promotions/promotions.go
+++ b/internal/controller/promotions/promotions.go
@@ -104,7 +104,7 @@ func SetupReconcilerWithManager(
 		ctx,
 		&kargoapi.Promotion{},
 		indexer.RunningPromotionsByArgoCDApplicationsField,
-		indexer.RunningPromotionsByArgoCDApplications(ctx, cfg.ShardName),
+		indexer.RunningPromotionsByArgoCDApplications(ctx, kargoMgr.GetClient(), cfg.ShardName),
 	); err != nil {
 		return fmt.Errorf("index running Promotions by Argo CD Applications: %w", err)
 	}
@@ -416,10 +416,8 @@ func (r *reconciler) Reconcile(
 
 	// If the promotion is still running, we'll need to periodically check on
 	// it.
-	//
-	// TODO: Make this configurable
 	if newStatus.Phase == kargoapi.PromotionPhaseRunning {
-		return ctrl.Result{RequeueAfter: 5 * time.Minute}, nil
+		return ctrl.Result{RequeueAfter: calculateRequeueInterval(promo)}, nil
 	}
 	return ctrl.Result{}, nil
 }
@@ -665,3 +663,20 @@ func parseCreateActorAnnotation(promo *kargoapi.Promotion) string {
 	}
 	return creator
 }
+
+func calculateRequeueInterval(p *kargoapi.Promotion) time.Duration {
+	defaultRequeueInterval := 5 * time.Minute
+	step := p.Spec.Steps[p.Status.CurrentStep]
+	runner := promotion.GetStepRunner(step.Uses)
+
+	timeout := (&promotion.Step{
+		Retry: step.Retry,
+	}).GetTimeout(runner)
+
+	md := p.Status.StepExecutionMetadata[p.Status.CurrentStep]
+	targetTimeout := md.StartedAt.Time.Add(*timeout)
+	if targetTimeout.Before(time.Now().Add(defaultRequeueInterval)) {
+		return time.Until(targetTimeout)
+	}
+	return defaultRequeueInterval
+}
diff --git a/internal/controller/stages/regular_stages.go b/internal/controller/stages/regular_stages.go
index 70596f8c..b4b0c369 100644
--- a/internal/controller/stages/regular_stages.go
+++ b/internal/controller/stages/regular_stages.go
@@ -1330,12 +1330,15 @@ func (r *RegularStageReconciler) startVerification(
 					"stage":   stage.Name,
 				},
 			},
-			Options: exprfn.FreightOperations(
-				ctx,
-				r.client,
-				stage.Namespace,
-				stage.Spec.RequestedFreight,
-				freight.References(),
+			Options: append(
+				exprfn.FreightOperations(
+					ctx,
+					r.client,
+					stage.Namespace,
+					stage.Spec.RequestedFreight,
+					freight.References(),
+				),
+				exprfn.DataOperations(ctx, r.client, stage.Namespace)...,
 			),
 		},
 	}
diff --git a/internal/credentials/kubernetes/gar/workload_identity_federation.go b/internal/credentials/kubernetes/gar/workload_identity_federation.go
index 1bb5b976..3a2530ad 100644
--- a/internal/credentials/kubernetes/gar/workload_identity_federation.go
+++ b/internal/credentials/kubernetes/gar/workload_identity_federation.go
@@ -2,11 +2,16 @@ package gar
 
 import (
 	"context"
+	"errors"
 	"fmt"
+	"net/http"
 	"time"
 
 	"cloud.google.com/go/compute/metadata"
 	"github.com/patrickmn/go-cache"
+	"golang.org/x/oauth2"
+	"golang.org/x/oauth2/google"
+	"google.golang.org/api/googleapi"
 	"google.golang.org/api/iamcredentials/v1"
 
 	"github.com/akuity/kargo/internal/credentials"
@@ -18,11 +23,14 @@ const (
 )
 
 type WorkloadIdentityFederationProvider struct {
-	tokenCache *cache.Cache
+	tokenCache       *cache.Cache // For short-lived Project-specific tokens
+	tokenSourceCache *cache.Cache // For long-lived token sources
 
 	projectID string
 
 	getAccessTokenFn func(ctx context.Context, project string) (string, error)
+
+	tokenSource oauth2.TokenSource
 }
 
 func NewWorkloadIdentityFederationProvider(ctx context.Context) credentials.Provider {
@@ -36,18 +44,37 @@ func NewWorkloadIdentityFederationProvider(ctx context.Context) credentials.Prov
 
 	projectID, err := metadata.ProjectIDWithContext(ctx)
 	if err != nil {
-		logger.Error(err, "error getting GCP project ID")
+		logger.Error(
+			err,
+			"error getting GCP project ID; GCP Workload Identity Federation disabled",
+		)
 		return nil
 	}
 	logger.Debug("got GCP project ID", "project", projectID)
 
+	tokenSource, err := google.DefaultTokenSource(ctx, iamcredentials.CloudPlatformScope)
+	if err != nil {
+		logger.Error(
+			err,
+			"error getting GCP default token source; GCP Workload Identity Federation disabled",
+		)
+		return nil
+	}
+
 	p := &WorkloadIdentityFederationProvider{
 		tokenCache: cache.New(
 			// Access tokens live for one hour. We'll hang on to them for 40 minutes.
 			40*time.Minute, // Default ttl for each entry
 			time.Hour,      // Cleanup interval
 		),
-		projectID: projectID,
+		tokenSourceCache: cache.New(
+			// Token sources are long-lived. We could hang on to them indefinitely,
+			// but we'll cap it at 12 hours to prevent memory leaks.
+			12*time.Hour, // Default ttl for each entry
+			time.Hour,    // Cleanup interval
+		),
+		projectID:   projectID,
+		tokenSource: tokenSource,
 	}
 	p.getAccessTokenFn = p.getAccessToken
 	return p
@@ -82,7 +109,7 @@ func (p *WorkloadIdentityFederationProvider) GetCredentials(
 
 	var cacheKey = tokenCacheKey(project)
 
-	// Check the cache for the token
+	// Check the token cache for a Project-specific token
 	if entry, exists := p.tokenCache.Get(cacheKey); exists {
 		return &credentials.Credentials{
 			Username: accessTokenUsername,
@@ -90,23 +117,43 @@ func (p *WorkloadIdentityFederationProvider) GetCredentials(
 		}, nil
 	}
 
-	// Cache miss, get a new token
+	// Check the token source cache for a long-lived token source
+	if entry, exists := p.tokenSourceCache.Get(cacheKey); exists {
+		tokenSource := entry.(oauth2.TokenSource) // nolint: forcetypeassert
+		token, err := tokenSource.Token()
+		if err != nil {
+			return nil, fmt.Errorf("error getting GCP access token: %w", err)
+		}
+		return &credentials.Credentials{
+			Username: accessTokenUsername,
+			Password: token.AccessToken,
+		}, nil
+	}
+
+	// We had a miss in both caches, so we'll try to get a new Project-specific
+	// token.
 	accessToken, err := p.getAccessTokenFn(ctx, project)
 	if err != nil {
 		return nil, fmt.Errorf("error getting GCP access token: %w", err)
 	}
-
-	// If we didn't get a token, we'll treat this as no credentials found
-	if accessToken == "" {
-		return nil, nil
+	if accessToken != "" {
+		p.tokenCache.Set(cacheKey, accessToken, cache.DefaultExpiration)
+		return &credentials.Credentials{
+			Username: accessTokenUsername,
+			Password: accessToken,
+		}, nil
 	}
 
-	// Cache the token
-	p.tokenCache.Set(cacheKey, accessToken, cache.DefaultExpiration)
-
+	// If we get to here, we found no Project-specific token and we'll cache the
+	// token source instead.
+	p.tokenSourceCache.Set(cacheKey, p.tokenSource, cache.DefaultExpiration)
+	token, err := p.tokenSource.Token()
+	if err != nil {
+		return nil, fmt.Errorf("error getting GCP access token: %w", err)
+	}
 	return &credentials.Credentials{
 		Username: accessTokenUsername,
-		Password: accessToken,
+		Password: token.AccessToken,
 	}, nil
 }
 
@@ -135,6 +182,11 @@ func (p *WorkloadIdentityFederationProvider) getAccessToken(
 		},
 	).Do()
 	if err != nil {
+		var googleErr *googleapi.Error
+		if errors.As(err, &googleErr) && googleErr.Code == http.StatusNotFound {
+			logger.Debug("no Project-specific service account found; will fall back to default token source")
+			return "", nil
+		}
 		logger.Error(err, "error generating access token")
 		return "", nil
 	}
diff --git a/internal/expressions/function/functions.go b/internal/expressions/function/functions.go
index 2f79a55d..28293776 100644
--- a/internal/expressions/function/functions.go
+++ b/internal/expressions/function/functions.go
@@ -5,6 +5,8 @@ import (
 	"fmt"
 
 	"github.com/expr-lang/expr"
+	corev1 "k8s.io/api/core/v1"
+	kubeerr "k8s.io/apimachinery/pkg/api/errors"
 	"sigs.k8s.io/controller-runtime/pkg/client"
 
 	kargoapi "github.com/akuity/kargo/api/v1alpha1"
@@ -35,6 +37,15 @@ func FreightOperations(
 	}
 }
 
+// DataOperations returns a slice of expr.Option containing functions for
+// data operations, such as accessing ConfigMaps and Secrets.
+func DataOperations(ctx context.Context, c client.Client, project string) []expr.Option {
+	return []expr.Option{
+		ConfigMap(ctx, c, project),
+		Secret(ctx, c, project),
+	}
+}
+
 // Warehouse returns an expr.Option that provides a `warehouse()` function
 // for use in expressions.
 //
@@ -109,6 +120,26 @@ func ChartFrom(
 	)
 }
 
+// ConfigMap returns an expr.Option that provides a `configMap()` function for
+// use in expressions.
+func ConfigMap(ctx context.Context, c client.Client, project string) expr.Option {
+	return expr.Function(
+		"configMap",
+		getConfigMap(ctx, c, project),
+		new(func(name string) map[string]string),
+	)
+}
+
+// Secret returns an expr.Option that provides a `secret()` function for use in
+// expressions.
+func Secret(ctx context.Context, c client.Client, project string) expr.Option {
+	return expr.Function(
+		"secret",
+		getSecret(ctx, c, project),
+		new(func(name string) map[string]string),
+	)
+}
+
 // warehouse creates a FreightOrigin of kind Warehouse with the specified name.
 //
 // It returns an error if the argument count is incorrect or if the name is not
@@ -277,3 +308,68 @@ func getChart(
 		)
 	}
 }
+
+func getConfigMap(ctx context.Context, c client.Client, project string) exprFn {
+	return func(a ...any) (any, error) {
+		if len(a) != 1 {
+			return nil, fmt.Errorf("expected 1 argument, got %d", len(a))
+		}
+
+		name, ok := a[0].(string)
+		if !ok {
+			return nil, fmt.Errorf("argument must be string, got %T", a[0])
+		}
+
+		var cfgMap corev1.ConfigMap
+		if err := c.Get(
+			ctx,
+			client.ObjectKey{
+				Namespace: project,
+				Name:      name,
+			},
+			&cfgMap,
+		); err != nil {
+			if kubeerr.IsNotFound(err) {
+				return map[string]string{}, nil
+			}
+			return nil, fmt.Errorf("failed to get configmap %s: %w", name, err)
+		}
+
+		return cfgMap.Data, nil
+	}
+}
+
+func getSecret(ctx context.Context, c client.Client, project string) exprFn {
+	return func(a ...any) (any, error) {
+		if len(a) != 1 {
+			return nil, fmt.Errorf("expected 1 argument, got %d", len(a))
+		}
+
+		name, ok := a[0].(string)
+		if !ok {
+			return nil, fmt.Errorf("argument must be string, got %T", a[0])
+		}
+
+		var secret corev1.Secret
+		if err := c.Get(
+			ctx,
+			client.ObjectKey{
+				Namespace: project,
+				Name:      name,
+			},
+			&secret,
+		); err != nil {
+			if kubeerr.IsNotFound(err) {
+				return map[string]string{}, nil
+			}
+			return nil, fmt.Errorf("failed to get secret %s: %w", name, err)
+		}
+
+		data := make(map[string]string, len(secret.Data))
+		for k, v := range secret.Data {
+			data[k] = string(v)
+		}
+
+		return data, nil
+	}
+}
diff --git a/internal/gitprovider/gitlab/gitlab.go b/internal/gitprovider/gitlab/gitlab.go
index 7c88c3de..5a0ea4bb 100644
--- a/internal/gitprovider/gitlab/gitlab.go
+++ b/internal/gitprovider/gitlab/gitlab.go
@@ -120,12 +120,11 @@ func (p *provider) CreatePullRequest(
 		opts = &gitprovider.CreatePullRequestOpts{}
 	}
 	glMR, _, err := p.client.CreateMergeRequest(p.projectName, &gitlab.CreateMergeRequestOptions{
-		Title:              &opts.Title,
-		Description:        &opts.Description,
-		Labels:             (*gitlab.LabelOptions)(&opts.Labels),
-		SourceBranch:       &opts.Head,
-		TargetBranch:       &opts.Base,
-		RemoveSourceBranch: gitlab.Ptr(true),
+		Title:        &opts.Title,
+		Description:  &opts.Description,
+		Labels:       (*gitlab.LabelOptions)(&opts.Labels),
+		SourceBranch: &opts.Head,
+		TargetBranch: &opts.Base,
 	})
 	if err != nil {
 		return nil, err
diff --git a/internal/indexer/indexer.go b/internal/indexer/indexer.go
index c2aef870..c33255fd 100644
--- a/internal/indexer/indexer.go
+++ b/internal/indexer/indexer.go
@@ -114,6 +114,7 @@ func PromotionsByStage(obj client.Object) []string {
 // Promotions not labeled with a shardName are indexed.
 func RunningPromotionsByArgoCDApplications(
 	ctx context.Context,
+	cl client.Client,
 	shardName string,
 ) client.IndexerFunc {
 	logger := logging.LoggerFromContext(ctx)
@@ -176,7 +177,7 @@ func RunningPromotionsByArgoCDApplications(
 
 			// As step-level variables are allowed to reference to output, we
 			// need to provide the state.
-			vars, err := dirStep.GetVars(promoCtx, promoCtx.State)
+			vars, err := dirStep.GetVars(ctx, cl, promoCtx, promoCtx.State)
 			if err != nil {
 				logger.Error(
 					err,
diff --git a/internal/kargo/promotion_builder.go b/internal/kargo/promotion_builder.go
index c0dc6927..a368bf66 100644
--- a/internal/kargo/promotion_builder.go
+++ b/internal/kargo/promotion_builder.go
@@ -71,6 +71,11 @@ func (b *PromotionBuilder) Build(
 		annotations[kargoapi.AnnotationKeyCreateActor] = api.FormatEventUserActor(u)
 	}
 
+	// Merge Stage variables with PromotionTemplate variables
+	vars := make([]kargoapi.ExpressionVariable, 0, len(stage.Spec.Vars)+len(stage.Spec.PromotionTemplate.Spec.Vars))
+	vars = append(vars, stage.Spec.Vars...)
+	vars = append(vars, stage.Spec.PromotionTemplate.Spec.Vars...)
+
 	promotion := kargoapi.Promotion{
 		ObjectMeta: metav1.ObjectMeta{
 			Name:        generatePromotionName(stage.Name, freight),
@@ -80,7 +85,7 @@ func (b *PromotionBuilder) Build(
 		Spec: kargoapi.PromotionSpec{
 			Stage:   stage.Name,
 			Freight: freight,
-			Vars:    stage.Spec.PromotionTemplate.Spec.Vars,
+			Vars:    vars,
 			Steps:   stage.Spec.PromotionTemplate.Spec.Steps,
 		},
 	}
@@ -121,7 +126,7 @@ func (b *PromotionBuilder) InflateSteps(ctx context.Context, promo *kargoapi.Pro
 func (b *PromotionBuilder) inflateTaskSteps(
 	ctx context.Context,
 	project, taskAlias string,
-	promoVars []kargoapi.PromotionVariable,
+	promoVars []kargoapi.ExpressionVariable,
 	taskStep kargoapi.PromotionStep,
 ) ([]kargoapi.PromotionStep, error) {
 	task, err := b.getTaskSpec(ctx, project, taskStep.Task)
@@ -220,8 +225,8 @@ func generatePromotionTaskStepAlias(taskAlias, stepAlias string) string {
 // variables and maps them to variables which can be used by the inflated
 // PromotionStep.
 func promotionTaskVarsToStepVars(
-	taskVars, promoVars, stepVars []kargoapi.PromotionVariable,
-) ([]kargoapi.PromotionVariable, error) {
+	taskVars, promoVars, stepVars []kargoapi.ExpressionVariable,
+) ([]kargoapi.ExpressionVariable, error) {
 	// Promotion variables can be used to set (or override) the variables
 	// required by the PromotionTask, but they are not inflated into the
 	// variables for the step. This map is used to check if a variable is
@@ -244,7 +249,7 @@ func promotionTaskVarsToStepVars(
 		}
 	}
 
-	var vars []kargoapi.PromotionVariable
+	var vars []kargoapi.ExpressionVariable
 
 	// Set the PromotionTask variable default values, but only if the variable
 	// is not set on the Promotion.
diff --git a/internal/promotion/promotion.go b/internal/promotion/promotion.go
index fe4f34f8..7acd8922 100644
--- a/internal/promotion/promotion.go
+++ b/internal/promotion/promotion.go
@@ -54,7 +54,7 @@ type Context struct {
 	State promotion.State
 	// Vars is a list of variables definitions that can be used by the
 	// Steps.
-	Vars []kargoapi.PromotionVariable
+	Vars []kargoapi.ExpressionVariable
 	// Secrets is a map of secrets that can be used by the Steps.
 	Secrets map[string]map[string]string
 	// Actor is the name of the actor triggering the Promotion.
@@ -81,7 +81,7 @@ type Step struct {
 	Retry *kargoapi.PromotionStepRetry
 	// Vars is a list of variables definitions that can be used by the
 	// Step.
-	Vars []kargoapi.PromotionVariable
+	Vars []kargoapi.ExpressionVariable
 	// Config is an opaque JSON to be passed to the StepRunner executing this
 	// step.
 	Config []byte
@@ -186,12 +186,17 @@ func (s *Step) BuildEnv(promoCtx Context, opts ...StepEnvOption) map[string]any
 
 // Skip returns true if the Step should be skipped based on the If condition.
 // The If condition is evaluated against the provided Context and State.
-func (s *Step) Skip(promoCtx Context, state promotion.State) (bool, error) {
+func (s *Step) Skip(
+	ctx context.Context,
+	cl client.Client,
+	promoCtx Context,
+	state promotion.State,
+) (bool, error) {
 	if s.If == "" {
 		return false, nil
 	}
 
-	vars, err := s.GetVars(promoCtx, state)
+	vars, err := s.GetVars(ctx, cl, promoCtx, state)
 	if err != nil {
 		return false, err
 	}
@@ -203,7 +208,20 @@ func (s *Step) Skip(promoCtx Context, state promotion.State) (bool, error) {
 		StepEnvWithVars(vars),
 	)
 
-	v, err := expressions.EvaluateTemplate(s.If, env)
+	v, err := expressions.EvaluateTemplate(
+		s.If,
+		env,
+		append(
+			exprfn.FreightOperations(
+				ctx,
+				cl,
+				promoCtx.Project,
+				promoCtx.FreightRequests,
+				promoCtx.Freight.References(),
+			),
+			exprfn.DataOperations(ctx, cl, promoCtx.Project)...,
+		)...,
+	)
 	if err != nil {
 		return false, err
 	}
@@ -228,7 +246,7 @@ func (s *Step) GetConfig(
 		return nil, nil
 	}
 
-	vars, err := s.GetVars(promoCtx, state)
+	vars, err := s.GetVars(ctx, cl, promoCtx, state)
 	if err != nil {
 		return nil, err
 	}
@@ -244,12 +262,15 @@ func (s *Step) GetConfig(
 	evaledCfgJSON, err := expressions.EvaluateJSONTemplate(
 		s.Config,
 		env,
-		exprfn.FreightOperations(
-			ctx,
-			cl,
-			promoCtx.Project,
-			promoCtx.FreightRequests,
-			promoCtx.Freight.References(),
+		append(
+			exprfn.FreightOperations(
+				ctx,
+				cl,
+				promoCtx.Project,
+				promoCtx.FreightRequests,
+				promoCtx.Freight.References(),
+			),
+			exprfn.DataOperations(ctx, cl, promoCtx.Project)...,
 		)...,
 	)
 	if err != nil {
@@ -264,7 +285,12 @@ func (s *Step) GetConfig(
 
 // GetVars returns the variables defined in the Step. The variables are
 // evaluated against the provided Context.
-func (s *Step) GetVars(promoCtx Context, state promotion.State) (map[string]any, error) {
+func (s *Step) GetVars(
+	ctx context.Context,
+	cl client.Client,
+	promoCtx Context,
+	state promotion.State,
+) (map[string]any, error) {
 	vars := make(map[string]any)
 
 	// Evaluate the global variables defined in the Promotion itself, these
@@ -273,6 +299,10 @@ func (s *Step) GetVars(promoCtx Context, state promotion.State) (map[string]any,
 		newVar, err := expressions.EvaluateTemplate(
 			v.Value,
 			s.BuildEnv(promoCtx, StepEnvWithVars(vars)),
+			append(
+				exprfn.FreightOperations(ctx, cl, promoCtx.Project, promoCtx.FreightRequests, promoCtx.Freight.References()),
+				exprfn.DataOperations(ctx, cl, promoCtx.Project)...,
+			)...,
 		)
 		if err != nil {
 			return nil, fmt.Errorf("error pre-processing promotion variable %q: %w", v.Name, err)
@@ -291,6 +321,10 @@ func (s *Step) GetVars(promoCtx Context, state promotion.State) (map[string]any,
 				StepEnvWithTaskOutputs(s.Alias, state),
 				StepEnvWithVars(vars),
 			),
+			append(
+				exprfn.FreightOperations(ctx, cl, promoCtx.Project, promoCtx.FreightRequests, promoCtx.Freight.References()),
+				exprfn.DataOperations(ctx, cl, promoCtx.Project)...,
+			)...,
 		)
 		if err != nil {
 			return nil, fmt.Errorf("error pre-processing promotion variable %q: %w", v.Name, err)
diff --git a/internal/promotion/registry.go b/internal/promotion/registry.go
index d1217d21..1b22e148 100644
--- a/internal/promotion/registry.go
+++ b/internal/promotion/registry.go
@@ -1,6 +1,8 @@
 package promotion
 
-import "github.com/akuity/kargo/pkg/promotion"
+import (
+	"github.com/akuity/kargo/pkg/promotion"
+)
 
 // stepRunnerRegistry is a registry of StepRunners.
 type stepRunnerRegistry map[string]promotion.StepRunner
@@ -24,3 +26,9 @@ var stepRunnerReg = stepRunnerRegistry{}
 func RegisterStepRunner(runner promotion.StepRunner) {
 	stepRunnerReg.register(runner)
 }
+
+// GetStepRunner returns a StepRunner from the package's internal registry. If
+// no StepRunner is registered with the given name, nil is returned instead.
+func GetStepRunner(name string) promotion.StepRunner {
+	return stepRunnerReg.getStepRunner(name)
+}
diff --git a/internal/promotion/runner/builtin/helm_template_runner.go b/internal/promotion/runner/builtin/helm_template_runner.go
index 727740e4..4b7dc256 100644
--- a/internal/promotion/runner/builtin/helm_template_runner.go
+++ b/internal/promotion/runner/builtin/helm_template_runner.go
@@ -81,7 +81,7 @@ func (h *helmTemplateRunner) run(
 	stepCtx *promotion.StepContext,
 	cfg builtin.HelmTemplateConfig,
 ) (promotion.StepResult, error) {
-	composedValues, err := h.composeValues(stepCtx.WorkDir, cfg.ValuesFiles)
+	composedValues, err := h.composeValues(stepCtx.WorkDir, cfg)
 	if err != nil {
 		return promotion.StepResult{Status: kargoapi.PromotionPhaseErrored},
 			fmt.Errorf("failed to compose values: %w", err)
@@ -123,17 +123,26 @@ func (h *helmTemplateRunner) run(
 	return promotion.StepResult{Status: kargoapi.PromotionPhaseSucceeded}, nil
 }
 
-// composeValues composes the values from the given values files. It merges the
-// values in the order they are provided.
-func (h *helmTemplateRunner) composeValues(workDir string, valuesFiles []string) (map[string]any, error) {
+// composeValues composes the values from the given values files and set values.
+// It merges the value files in the order they are provided. Set values are merged
+// into last as overrides
+func (h *helmTemplateRunner) composeValues(
+	workDir string,
+	cfg builtin.HelmTemplateConfig,
+) (map[string]any, error) {
 	valueOpts := &values.Options{}
-	for _, p := range valuesFiles {
+	for _, p := range cfg.ValuesFiles {
 		absValuesPath, err := securejoin.SecureJoin(workDir, p)
 		if err != nil {
 			return nil, fmt.Errorf("failed to join path %q: %w", p, err)
 		}
 		valueOpts.ValueFiles = append(valueOpts.ValueFiles, absValuesPath)
 	}
+	setValueStrVal := make([]string, len(cfg.SetValues))
+	for i, setValue := range cfg.SetValues {
+		setValueStrVal[i] = fmt.Sprintf("%s=%s", setValue.Key, setValue.Value)
+	}
+	valueOpts.Values = append(valueOpts.Values, setValueStrVal...)
 	return valueOpts.MergeValues(nil)
 }
 
diff --git a/internal/promotion/runner/builtin/schemas/helm-template-config.json b/internal/promotion/runner/builtin/schemas/helm-template-config.json
index 69aecdfe..149d9717 100644
--- a/internal/promotion/runner/builtin/schemas/helm-template-config.json
+++ b/internal/promotion/runner/builtin/schemas/helm-template-config.json
@@ -1,6 +1,26 @@
 {
   "$schema": "https://json-schema.org/draft/2020-12/schema",
   "title": "HelmTemplateConfig",
+
+  "definitions": {
+    "setValues": {
+      "type": "object",
+      "additionalProperties": false,
+      "properties": {
+        "key": {
+          "type": "string",
+          "description": "The key whose value should be set. For nested values, use dots to delimit key parts. e.g. `image.tag`.",
+          "minLength": 1
+        },
+        "value": {
+          "type": "string",
+          "description": "The new value for the key."
+        }
+      },
+      "required": ["key", "value"]
+    }
+  },
+
   "type": "object",
   "additionalProperties": false,
   "required": ["outPath", "path", "releaseName"],
@@ -37,6 +57,13 @@
         "minLength": 1
       }
     },
+    "setValues": {
+      "type": "array",
+      "description": "Allows for amending chart configuration inline as one would with the `helm template` command's `--set` flag.",
+      "items": {
+        "$ref": "#/definitions/setValues"
+      }
+    },
     "includeCRDs": {
       "type": "boolean",
       "description": "Whether to include CRDs in the rendered manifests.",
diff --git a/internal/promotion/simple_engine.go b/internal/promotion/simple_engine.go
index a5377acc..2b7491e2 100644
--- a/internal/promotion/simple_engine.go
+++ b/internal/promotion/simple_engine.go
@@ -143,7 +143,7 @@ func (e *simpleEngine) executeSteps(
 
 		// Check if the step should be skipped.
 		var skip bool
-		if skip, err = step.Skip(promoCtx, state); err != nil {
+		if skip, err = step.Skip(ctx, e.kargoClient, promoCtx, state); err != nil {
 			return Result{
 				Status:                kargoapi.PromotionPhaseErrored,
 				CurrentStep:           i,
diff --git a/internal/rollouts/analysis_run_builder.go b/internal/rollouts/analysis_run_builder.go
index b08a135a..2d0b3526 100644
--- a/internal/rollouts/analysis_run_builder.go
+++ b/internal/rollouts/analysis_run_builder.go
@@ -24,6 +24,9 @@ import (
 // controller instance.
 const controllerInstanceIDLabelKey = "argo-rollouts.argoproj.io/controller-instance-id"
 
+// varsEnvKey is the key for variables in the ArgumentEvaluationConfig.Env map.
+const varsEnvKey = "vars"
+
 // Config holds the configuration for the AnalysisRunBuilder.
 type Config struct {
 	// ControllerInstanceID is the unique identifier for the Argo Rollouts
@@ -192,6 +195,28 @@ func (b *AnalysisRunBuilder) buildArgs(
 		exprCfg = &ArgumentEvaluationConfig{}
 	}
 
+	if len(exprCfg.Vars) > 0 {
+		if exprCfg.Env == nil {
+			exprCfg.Env = make(map[string]any)
+		}
+		if exprCfg.Env[varsEnvKey] == nil {
+			exprCfg.Env[varsEnvKey] = make(map[string]any)
+		}
+		// NB: This will cause direct modifications to the map in the config.
+		vars, ok := exprCfg.Env[varsEnvKey].(map[string]any)
+		if !ok {
+			return nil, fmt.Errorf("vars in argument evaluation config env is of unexpected type %T", exprCfg.Env["vars"])
+		}
+
+		for _, v := range exprCfg.Vars {
+			value, err := expressions.EvaluateTemplate(v.Value, exprCfg.Env, exprCfg.Options...)
+			if err != nil {
+				return nil, fmt.Errorf("evaluate variable %q: %w", v.Name, err)
+			}
+			vars[v.Name] = value
+		}
+	}
+
 	rolloutsArgs := make([]rolloutsapi.Argument, len(args))
 	for i, arg := range args {
 		rolloutsArgs[i] = rolloutsapi.Argument{
diff --git a/internal/rollouts/options.go b/internal/rollouts/options.go
index a26d6aa3..80b52b42 100644
--- a/internal/rollouts/options.go
+++ b/internal/rollouts/options.go
@@ -45,19 +45,41 @@ type Owner struct {
 }
 
 // ArgumentEvaluationConfig holds the configuration for the evaluation of
-// expressions in the AnalysisRun arguments.
+// expressions in the AnalysisRun arguments. It contains the environment
+// variables, the variables, and the options for the expression evaluation.
+//
+// Note: The builder may modify the Env map to add additional information.
+// For example, to add any evaluated Vars. Because of this, it is recommended
+// not to reuse the same ArgumentEvaluationConfig or Env map for multiple
+// builds without making use of a deep copy.
 type ArgumentEvaluationConfig struct {
 	// Env is a (nested) map of variables that can be used in the expressions.
 	// The keys are the variable names, and the values are the variable values.
 	// When the value itself is a map, it is considered a nested variable and
 	// can be accessed using dot notation. e.g. `${{ foo.bar }}`.
+	//
+	// Note: The Env map can be modified by the builder to add additional
+	// information. For example, to add any evaluated Vars.
 	Env map[string]any
+	// Vars are the variables that can be used in the expressions. They are
+	// evaluated in the order they are defined. After the evaluation of the
+	// variables, they are available in the Env map as `${{ vars.<name>}}`.
+	Vars []ArgumentVariable
 	// Options are the options for the expression evaluation. It can be used to
 	// configure the behavior of the expression evaluation and the functions
 	// available.
 	Options []expr.Option
 }
 
+// ArgumentVariable represents a variable that can be used in the AnalysisRun
+// arguments.
+type ArgumentVariable struct {
+	// Name is the name of the variable.
+	Name string
+	// Value is the value of the variable.
+	Value string
+}
+
 // Apply applies the given options to the AnalysisRunOptions.
 func (o *AnalysisRunOptions) Apply(opts ...AnalysisRunOption) {
 	for _, opt := range opts {
diff --git a/internal/server/oidc/config.go b/internal/server/oidc/config.go
index 1a67e88b..76804202 100644
--- a/internal/server/oidc/config.go
+++ b/internal/server/oidc/config.go
@@ -22,6 +22,8 @@ type Config struct {
 	// AdditionalScopes are any more scopes to be requested during the authorization code flow
 	// on top of the default Scopes.
 	AdditionalScopes []string `envconfig:"OIDC_ADDITIONAL_SCOPES"`
+	// UsernameClaim is the claim to use as the username for the user.
+	UsernameClaim string `envconfig:"OIDC_USERNAME_CLAIM" default:"email"`
 
 	// GlobalServiceAccountNamespaces is the list of namespaces to look up
 	// for shared service accounts.
diff --git a/internal/server/option/auth.go b/internal/server/option/auth.go
index 085c09be..d648b756 100644
--- a/internal/server/option/auth.go
+++ b/internal/server/option/auth.go
@@ -398,12 +398,23 @@ func (a *authInterceptor) authenticate(
 		if err != nil {
 			return ctx, fmt.Errorf("list service accounts for user: %w", err)
 		}
+		var username string
+		if un, ok := c[a.cfg.OIDCConfig.UsernameClaim]; ok {
+			if username, ok = un.(string); !ok {
+				return ctx, fmt.Errorf(
+					"claim %q must be a string; got %T",
+					a.cfg.OIDCConfig.UsernameClaim,
+					un,
+				)
+			}
+		}
 		return user.ContextWithInfo(
 			ctx,
 			user.Info{
 				Claims:                     c,
 				ServiceAccountsByNamespace: sa,
 				BearerToken:                rawToken,
+				Username:                   username,
 			},
 		), nil
 
diff --git a/internal/server/refresh_stage_v1alpha1.go b/internal/server/refresh_stage_v1alpha1.go
index 5b961e6e..88bcedf9 100644
--- a/internal/server/refresh_stage_v1alpha1.go
+++ b/internal/server/refresh_stage_v1alpha1.go
@@ -31,7 +31,7 @@ func (s *server) RefreshStage(
 		Namespace: project,
 		Name:      name,
 	}
-	stage, err := api.RefreshStage(ctx, s.client, objKey)
+	stage, err := api.RefreshStage(ctx, s.client.InternalClient(), objKey)
 	if err != nil {
 		return nil, err
 	}
diff --git a/internal/server/refresh_warehouse_v1alpha1.go b/internal/server/refresh_warehouse_v1alpha1.go
index 0ade4460..1b8ed18e 100644
--- a/internal/server/refresh_warehouse_v1alpha1.go
+++ b/internal/server/refresh_warehouse_v1alpha1.go
@@ -28,7 +28,7 @@ func (s *server) RefreshWarehouse(
 		return nil, err
 	}
 
-	warehouse, err := api.RefreshWarehouse(ctx, s.client, client.ObjectKey{
+	warehouse, err := api.RefreshWarehouse(ctx, s.client.InternalClient(), client.ObjectKey{
 		Namespace: project,
 		Name:      name,
 	})
diff --git a/internal/server/user/user.go b/internal/server/user/user.go
index 0f9774eb..bd19a82d 100644
--- a/internal/server/user/user.go
+++ b/internal/server/user/user.go
@@ -27,6 +27,9 @@ type Info struct {
 	// ServiceAccountsByNamespace is the mapping of namespace names to sets of
 	// ServiceAccounts that a user has been mapped to.
 	ServiceAccountsByNamespace map[string]map[types.NamespacedName]struct{}
+	// Username is the username of the user. This is often the email address
+	// of the user, but may be different depending on configuration.
+	Username string
 }
 
 // ContextWithInfo returns a context.Context that has been augmented with
diff --git a/pkg/x/promotion/runner/builtin/zz_config_types.go b/pkg/x/promotion/runner/builtin/zz_config_types.go
index c95e938c..ba4f7071 100644
--- a/pkg/x/promotion/runner/builtin/zz_config_types.go
+++ b/pkg/x/promotion/runner/builtin/zz_config_types.go
@@ -231,6 +231,9 @@ type HelmTemplateConfig struct {
 	Path string `json:"path"`
 	// ReleaseName to use for the rendered manifests.
 	ReleaseName string `json:"releaseName"`
+	// Allows for amending chart configuration inline as one would with the `helm template`
+	// command's `--set` flag.
+	SetValues []SetValues `json:"setValues,omitempty"`
 	// Whether to skip tests when rendering the manifests.
 	SkipTests bool `json:"skipTests,omitempty"`
 	// Whether to use the release name in the output path (instead of the chart name). This only
@@ -240,6 +243,14 @@ type HelmTemplateConfig struct {
 	ValuesFiles []string `json:"valuesFiles,omitempty"`
 }
 
+type SetValues struct {
+	// The key whose value should be set. For nested values, use dots to delimit key parts. e.g.
+	// `image.tag`.
+	Key string `json:"key"`
+	// The new value for the key.
+	Value string `json:"value"`
+}
+
 type HelmUpdateChartConfig struct {
 	// A list of chart dependencies which should receive updates.
 	Charts []Chart `json:"charts"`
diff --git a/ui/package.json b/ui/package.json
index 61372929..aa0bbb3b 100644
--- a/ui/package.json
+++ b/ui/package.json
@@ -16,6 +16,8 @@
   "devDependencies": {
     "@apidevtools/json-schema-ref-parser": "^11.9.3",
     "@eslint/compat": "^1.2.7",
+    "@eslint/eslintrc": "^3.3.0",
+    "@eslint/js": "^9.22.0",
     "@openapi-contrib/openapi-schema-to-json-schema": "^5.1.0",
     "@tanstack/react-query-devtools": "^5.71.0",
     "@types/git-url-parse": "^9.0.3",
@@ -43,7 +45,7 @@
     "prettier": "^3.5.3",
     "tailwindcss": "^3.4.17",
     "typescript": "^5.8.2",
-    "vite": "^6.2.4",
+    "vite": "^6.2.5",
     "vite-plugin-compression": "^0.5.1",
     "vite-tsconfig-paths": "^5.1.4",
     "vitest": "^3.0.9",
diff --git a/ui/pnpm-lock.yaml b/ui/pnpm-lock.yaml
index f0e9affa..4c774820 100644
--- a/ui/pnpm-lock.yaml
+++ b/ui/pnpm-lock.yaml
@@ -123,6 +123,12 @@ importers:
       '@eslint/compat':
         specifier: ^1.2.7
         version: 1.2.7(eslint@9.23.0(jiti@1.21.7))
+      '@eslint/eslintrc':
+        specifier: ^3.3.0
+        version: 3.3.1
+      '@eslint/js':
+        specifier: ^9.22.0
+        version: 9.23.0
       '@openapi-contrib/openapi-schema-to-json-schema':
         specifier: ^5.1.0
         version: 5.1.0
@@ -158,7 +164,7 @@ importers:
         version: 8.28.0(eslint@9.23.0(jiti@1.21.7))(typescript@5.8.2)
       '@vitejs/plugin-react':
         specifier: ^4.3.4
-        version: 4.3.4(vite@6.2.4(@types/node@22.13.14)(jiti@1.21.7)(less@4.2.2)(yaml@2.7.1))
+        version: 4.3.4(vite@6.2.5(@types/node@22.13.14)(jiti@1.21.7)(less@4.2.2)(yaml@2.7.1))
       autoprefixer:
         specifier: ^10.4.21
         version: 10.4.21(postcss@8.5.3)
@@ -205,14 +211,14 @@ importers:
         specifier: ^5.8.2
         version: 5.8.2
       vite:
-        specifier: ^6.2.4
-        version: 6.2.4(@types/node@22.13.14)(jiti@1.21.7)(less@4.2.2)(yaml@2.7.1)
+        specifier: ^6.2.5
+        version: 6.2.5(@types/node@22.13.14)(jiti@1.21.7)(less@4.2.2)(yaml@2.7.1)
       vite-plugin-compression:
         specifier: ^0.5.1
-        version: 0.5.1(vite@6.2.4(@types/node@22.13.14)(jiti@1.21.7)(less@4.2.2)(yaml@2.7.1))
+        version: 0.5.1(vite@6.2.5(@types/node@22.13.14)(jiti@1.21.7)(less@4.2.2)(yaml@2.7.1))
       vite-tsconfig-paths:
         specifier: ^5.1.4
-        version: 5.1.4(typescript@5.8.2)(vite@6.2.4(@types/node@22.13.14)(jiti@1.21.7)(less@4.2.2)(yaml@2.7.1))
+        version: 5.1.4(typescript@5.8.2)(vite@6.2.5(@types/node@22.13.14)(jiti@1.21.7)(less@4.2.2)(yaml@2.7.1))
       vitest:
         specifier: ^3.0.9
         version: 3.0.9(@types/node@22.13.14)(jiti@1.21.7)(less@4.2.2)(yaml@2.7.1)
@@ -788,103 +794,103 @@ packages:
     peerDependencies:
       '@rjsf/utils': ^5.24.x
 
-  '@rollup/rollup-android-arm-eabi@4.38.0':
-    resolution: {integrity: sha512-ldomqc4/jDZu/xpYU+aRxo3V4mGCV9HeTgUBANI3oIQMOL+SsxB+S2lxMpkFp5UamSS3XuTMQVbsS24R4J4Qjg==}
+  '@rollup/rollup-android-arm-eabi@4.39.0':
+    resolution: {integrity: sha512-lGVys55Qb00Wvh8DMAocp5kIcaNzEFTmGhfFd88LfaogYTRKrdxgtlO5H6S49v2Nd8R2C6wLOal0qv6/kCkOwA==}
     cpu: [arm]
     os: [android]
 
-  '@rollup/rollup-android-arm64@4.38.0':
-    resolution: {integrity: sha512-VUsgcy4GhhT7rokwzYQP+aV9XnSLkkhlEJ0St8pbasuWO/vwphhZQxYEKUP3ayeCYLhk6gEtacRpYP/cj3GjyQ==}
+  '@rollup/rollup-android-arm64@4.39.0':
+    resolution: {integrity: sha512-It9+M1zE31KWfqh/0cJLrrsCPiF72PoJjIChLX+rEcujVRCb4NLQ5QzFkzIZW8Kn8FTbvGQBY5TkKBau3S8cCQ==}
     cpu: [arm64]
     os: [android]
 
-  '@rollup/rollup-darwin-arm64@4.38.0':
-    resolution: {integrity: sha512-buA17AYXlW9Rn091sWMq1xGUvWQFOH4N1rqUxGJtEQzhChxWjldGCCup7r/wUnaI6Au8sKXpoh0xg58a7cgcpg==}
+  '@rollup/rollup-darwin-arm64@4.39.0':
+    resolution: {integrity: sha512-lXQnhpFDOKDXiGxsU9/l8UEGGM65comrQuZ+lDcGUx+9YQ9dKpF3rSEGepyeR5AHZ0b5RgiligsBhWZfSSQh8Q==}
     cpu: [arm64]
     os: [darwin]
 
-  '@rollup/rollup-darwin-x64@4.38.0':
-    resolution: {integrity: sha512-Mgcmc78AjunP1SKXl624vVBOF2bzwNWFPMP4fpOu05vS0amnLcX8gHIge7q/lDAHy3T2HeR0TqrriZDQS2Woeg==}
+  '@rollup/rollup-darwin-x64@4.39.0':
+    resolution: {integrity: sha512-mKXpNZLvtEbgu6WCkNij7CGycdw9cJi2k9v0noMb++Vab12GZjFgUXD69ilAbBh034Zwn95c2PNSz9xM7KYEAQ==}
     cpu: [x64]
     os: [darwin]
 
-  '@rollup/rollup-freebsd-arm64@4.38.0':
-    resolution: {integrity: sha512-zzJACgjLbQTsscxWqvrEQAEh28hqhebpRz5q/uUd1T7VTwUNZ4VIXQt5hE7ncs0GrF+s7d3S4on4TiXUY8KoQA==}
+  '@rollup/rollup-freebsd-arm64@4.39.0':
+    resolution: {integrity: sha512-jivRRlh2Lod/KvDZx2zUR+I4iBfHcu2V/BA2vasUtdtTN2Uk3jfcZczLa81ESHZHPHy4ih3T/W5rPFZ/hX7RtQ==}
     cpu: [arm64]
     os: [freebsd]
 
-  '@rollup/rollup-freebsd-x64@4.38.0':
-    resolution: {integrity: sha512-hCY/KAeYMCyDpEE4pTETam0XZS4/5GXzlLgpi5f0IaPExw9kuB+PDTOTLuPtM10TlRG0U9OSmXJ+Wq9J39LvAg==}
+  '@rollup/rollup-freebsd-x64@4.39.0':
+    resolution: {integrity: sha512-8RXIWvYIRK9nO+bhVz8DwLBepcptw633gv/QT4015CpJ0Ht8punmoHU/DuEd3iw9Hr8UwUV+t+VNNuZIWYeY7Q==}
     cpu: [x64]
     os: [freebsd]
 
-  '@rollup/rollup-linux-arm-gnueabihf@4.38.0':
-    resolution: {integrity: sha512-mimPH43mHl4JdOTD7bUMFhBdrg6f9HzMTOEnzRmXbOZqjijCw8LA5z8uL6LCjxSa67H2xiLFvvO67PT05PRKGg==}
+  '@rollup/rollup-linux-arm-gnueabihf@4.39.0':
+    resolution: {integrity: sha512-mz5POx5Zu58f2xAG5RaRRhp3IZDK7zXGk5sdEDj4o96HeaXhlUwmLFzNlc4hCQi5sGdR12VDgEUqVSHer0lI9g==}
     cpu: [arm]
     os: [linux]
 
-  '@rollup/rollup-linux-arm-musleabihf@4.38.0':
-    resolution: {integrity: sha512-tPiJtiOoNuIH8XGG8sWoMMkAMm98PUwlriOFCCbZGc9WCax+GLeVRhmaxjJtz6WxrPKACgrwoZ5ia/uapq3ZVg==}
+  '@rollup/rollup-linux-arm-musleabihf@4.39.0':
+    resolution: {integrity: sha512-+YDwhM6gUAyakl0CD+bMFpdmwIoRDzZYaTWV3SDRBGkMU/VpIBYXXEvkEcTagw/7VVkL2vA29zU4UVy1mP0/Yw==}
     cpu: [arm]
     os: [linux]
 
-  '@rollup/rollup-linux-arm64-gnu@4.38.0':
-    resolution: {integrity: sha512-wZco59rIVuB0tjQS0CSHTTUcEde+pXQWugZVxWaQFdQQ1VYub/sTrNdY76D1MKdN2NB48JDuGABP6o6fqos8mA==}
+  '@rollup/rollup-linux-arm64-gnu@4.39.0':
+    resolution: {integrity: sha512-EKf7iF7aK36eEChvlgxGnk7pdJfzfQbNvGV/+l98iiMwU23MwvmV0Ty3pJ0p5WQfm3JRHOytSIqD9LB7Bq7xdQ==}
     cpu: [arm64]
     os: [linux]
 
-  '@rollup/rollup-linux-arm64-musl@4.38.0':
-    resolution: {integrity: sha512-fQgqwKmW0REM4LomQ+87PP8w8xvU9LZfeLBKybeli+0yHT7VKILINzFEuggvnV9M3x1Ed4gUBmGUzCo/ikmFbQ==}
+  '@rollup/rollup-linux-arm64-musl@4.39.0':
+    resolution: {integrity: sha512-vYanR6MtqC7Z2SNr8gzVnzUul09Wi1kZqJaek3KcIlI/wq5Xtq4ZPIZ0Mr/st/sv/NnaPwy/D4yXg5x0B3aUUA==}
     cpu: [arm64]
     os: [linux]
 
-  '@rollup/rollup-linux-loongarch64-gnu@4.38.0':
-    resolution: {integrity: sha512-hz5oqQLXTB3SbXpfkKHKXLdIp02/w3M+ajp8p4yWOWwQRtHWiEOCKtc9U+YXahrwdk+3qHdFMDWR5k+4dIlddg==}
+  '@rollup/rollup-linux-loongarch64-gnu@4.39.0':
+    resolution: {integrity: sha512-NMRUT40+h0FBa5fb+cpxtZoGAggRem16ocVKIv5gDB5uLDgBIwrIsXlGqYbLwW8YyO3WVTk1FkFDjMETYlDqiw==}
     cpu: [loong64]
     os: [linux]
 
-  '@rollup/rollup-linux-powerpc64le-gnu@4.38.0':
-    resolution: {integrity: sha512-NXqygK/dTSibQ+0pzxsL3r4Xl8oPqVoWbZV9niqOnIHV/J92fe65pOir0xjkUZDRSPyFRvu+4YOpJF9BZHQImw==}
+  '@rollup/rollup-linux-powerpc64le-gnu@4.39.0':
+    resolution: {integrity: sha512-0pCNnmxgduJ3YRt+D+kJ6Ai/r+TaePu9ZLENl+ZDV/CdVczXl95CbIiwwswu4L+K7uOIGf6tMo2vm8uadRaICQ==}
     cpu: [ppc64]
     os: [linux]
 
-  '@rollup/rollup-linux-riscv64-gnu@4.38.0':
-    resolution: {integrity: sha512-GEAIabR1uFyvf/jW/5jfu8gjM06/4kZ1W+j1nWTSSB3w6moZEBm7iBtzwQ3a1Pxos2F7Gz+58aVEnZHU295QTg==}
+  '@rollup/rollup-linux-riscv64-gnu@4.39.0':
+    resolution: {integrity: sha512-t7j5Zhr7S4bBtksT73bO6c3Qa2AV/HqiGlj9+KB3gNF5upcVkx+HLgxTm8DK4OkzsOYqbdqbLKwvGMhylJCPhQ==}
     cpu: [riscv64]
     os: [linux]
 
-  '@rollup/rollup-linux-riscv64-musl@4.38.0':
-    resolution: {integrity: sha512-9EYTX+Gus2EGPbfs+fh7l95wVADtSQyYw4DfSBcYdUEAmP2lqSZY0Y17yX/3m5VKGGJ4UmIH5LHLkMJft3bYoA==}
+  '@rollup/rollup-linux-riscv64-musl@4.39.0':
+    resolution: {integrity: sha512-m6cwI86IvQ7M93MQ2RF5SP8tUjD39Y7rjb1qjHgYh28uAPVU8+k/xYWvxRO3/tBN2pZkSMa5RjnPuUIbrwVxeA==}
     cpu: [riscv64]
     os: [linux]
 
-  '@rollup/rollup-linux-s390x-gnu@4.38.0':
-    resolution: {integrity: sha512-Mpp6+Z5VhB9VDk7RwZXoG2qMdERm3Jw07RNlXHE0bOnEeX+l7Fy4bg+NxfyN15ruuY3/7Vrbpm75J9QHFqj5+Q==}
+  '@rollup/rollup-linux-s390x-gnu@4.39.0':
+    resolution: {integrity: sha512-iRDJd2ebMunnk2rsSBYlsptCyuINvxUfGwOUldjv5M4tpa93K8tFMeYGpNk2+Nxl+OBJnBzy2/JCscGeO507kA==}
     cpu: [s390x]
     os: [linux]
 
-  '@rollup/rollup-linux-x64-gnu@4.38.0':
-    resolution: {integrity: sha512-vPvNgFlZRAgO7rwncMeE0+8c4Hmc+qixnp00/Uv3ht2x7KYrJ6ERVd3/R0nUtlE6/hu7/HiiNHJ/rP6knRFt1w==}
+  '@rollup/rollup-linux-x64-gnu@4.39.0':
+    resolution: {integrity: sha512-t9jqYw27R6Lx0XKfEFe5vUeEJ5pF3SGIM6gTfONSMb7DuG6z6wfj2yjcoZxHg129veTqU7+wOhY6GX8wmf90dA==}
     cpu: [x64]
     os: [linux]
 
-  '@rollup/rollup-linux-x64-musl@4.38.0':
-    resolution: {integrity: sha512-q5Zv+goWvQUGCaL7fU8NuTw8aydIL/C9abAVGCzRReuj5h30TPx4LumBtAidrVOtXnlB+RZkBtExMsfqkMfb8g==}
+  '@rollup/rollup-linux-x64-musl@4.39.0':
+    resolution: {integrity: sha512-ThFdkrFDP55AIsIZDKSBWEt/JcWlCzydbZHinZ0F/r1h83qbGeenCt/G/wG2O0reuENDD2tawfAj2s8VK7Bugg==}
     cpu: [x64]
     os: [linux]
 
-  '@rollup/rollup-win32-arm64-msvc@4.38.0':
-    resolution: {integrity: sha512-u/Jbm1BU89Vftqyqbmxdq14nBaQjQX1HhmsdBWqSdGClNaKwhjsg5TpW+5Ibs1mb8Es9wJiMdl86BcmtUVXNZg==}
+  '@rollup/rollup-win32-arm64-msvc@4.39.0':
+    resolution: {integrity: sha512-jDrLm6yUtbOg2TYB3sBF3acUnAwsIksEYjLeHL+TJv9jg+TmTwdyjnDex27jqEMakNKf3RwwPahDIt7QXCSqRQ==}
     cpu: [arm64]
     os: [win32]
 
-  '@rollup/rollup-win32-ia32-msvc@4.38.0':
-    resolution: {integrity: sha512-mqu4PzTrlpNHHbu5qleGvXJoGgHpChBlrBx/mEhTPpnAL1ZAYFlvHD7rLK839LLKQzqEQMFJfGrrOHItN4ZQqA==}
+  '@rollup/rollup-win32-ia32-msvc@4.39.0':
+    resolution: {integrity: sha512-6w9uMuza+LbLCVoNKL5FSLE7yvYkq9laSd09bwS0tMjkwXrmib/4KmoJcrKhLWHvw19mwU+33ndC69T7weNNjQ==}
     cpu: [ia32]
     os: [win32]
 
-  '@rollup/rollup-win32-x64-msvc@4.38.0':
-    resolution: {integrity: sha512-jjqy3uWlecfB98Psxb5cD6Fny9Fupv9LrDSPTQZUROqjvZmcCqNu4UMl7qqhlUUGpwiAkotj6GYu4SZdcr/nLw==}
+  '@rollup/rollup-win32-x64-msvc@4.39.0':
+    resolution: {integrity: sha512-yAkUOkIKZlK5dl7u6dg897doBgLXmUHhIINM2c+sND3DZwnrdQkkSiDh7N75Ll4mM4dxSkYfXqU9fW3lLkMFug==}
     cpu: [x64]
     os: [win32]
 
@@ -3029,8 +3035,8 @@ packages:
     resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==}
     engines: {iojs: '>=1.0.0', node: '>=0.10.0'}
 
-  rollup@4.38.0:
-    resolution: {integrity: sha512-5SsIRtJy9bf1ErAOiFMFzl64Ex9X5V7bnJ+WlFMb+zmP459OSWCEG7b0ERZ+PEU7xPt4OG3RHbrp1LJlXxYTrw==}
+  rollup@4.39.0:
+    resolution: {integrity: sha512-thI8kNc02yNvnmJp8dr3fNWJ9tCONDhp6TV35X6HkKGGs9E6q7YWCHbe5vKiTa7TAiNcFEmXKj3X/pG2b3ci0g==}
     engines: {node: '>=18.0.0', npm: '>=8.0.0'}
     hasBin: true
 
@@ -3393,8 +3399,8 @@ packages:
       vite:
         optional: true
 
-  vite@6.2.4:
-    resolution: {integrity: sha512-veHMSew8CcRzhL5o8ONjy8gkfmFJAd5Ac16oxBUjlwgX3Gq2Wqr+qNC3TjPIpy7TPV/KporLga5GT9HqdrCizw==}
+  vite@6.2.5:
+    resolution: {integrity: sha512-j023J/hCAa4pRIUH6J9HemwYfjB5llR2Ps0CWeikOtdR8+pAURAk0DoJC5/mm9kd+UgdnIy7d6HE4EAvlYhPhA==}
     engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0}
     hasBin: true
     peerDependencies:
@@ -4118,64 +4124,64 @@ snapshots:
       lodash: 4.17.21
       lodash-es: 4.17.21
 
-  '@rollup/rollup-android-arm-eabi@4.38.0':
+  '@rollup/rollup-android-arm-eabi@4.39.0':
     optional: true
 
-  '@rollup/rollup-android-arm64@4.38.0':
+  '@rollup/rollup-android-arm64@4.39.0':
     optional: true
 
-  '@rollup/rollup-darwin-arm64@4.38.0':
+  '@rollup/rollup-darwin-arm64@4.39.0':
     optional: true
 
-  '@rollup/rollup-darwin-x64@4.38.0':
+  '@rollup/rollup-darwin-x64@4.39.0':
     optional: true
 
-  '@rollup/rollup-freebsd-arm64@4.38.0':
+  '@rollup/rollup-freebsd-arm64@4.39.0':
     optional: true
 
-  '@rollup/rollup-freebsd-x64@4.38.0':
+  '@rollup/rollup-freebsd-x64@4.39.0':
     optional: true
 
-  '@rollup/rollup-linux-arm-gnueabihf@4.38.0':
+  '@rollup/rollup-linux-arm-gnueabihf@4.39.0':
     optional: true
 
-  '@rollup/rollup-linux-arm-musleabihf@4.38.0':
+  '@rollup/rollup-linux-arm-musleabihf@4.39.0':
     optional: true
 
-  '@rollup/rollup-linux-arm64-gnu@4.38.0':
+  '@rollup/rollup-linux-arm64-gnu@4.39.0':
     optional: true
 
-  '@rollup/rollup-linux-arm64-musl@4.38.0':
+  '@rollup/rollup-linux-arm64-musl@4.39.0':
     optional: true
 
-  '@rollup/rollup-linux-loongarch64-gnu@4.38.0':
+  '@rollup/rollup-linux-loongarch64-gnu@4.39.0':
     optional: true
 
-  '@rollup/rollup-linux-powerpc64le-gnu@4.38.0':
+  '@rollup/rollup-linux-powerpc64le-gnu@4.39.0':
     optional: true
 
-  '@rollup/rollup-linux-riscv64-gnu@4.38.0':
+  '@rollup/rollup-linux-riscv64-gnu@4.39.0':
     optional: true
 
-  '@rollup/rollup-linux-riscv64-musl@4.38.0':
+  '@rollup/rollup-linux-riscv64-musl@4.39.0':
     optional: true
 
-  '@rollup/rollup-linux-s390x-gnu@4.38.0':
+  '@rollup/rollup-linux-s390x-gnu@4.39.0':
     optional: true
 
-  '@rollup/rollup-linux-x64-gnu@4.38.0':
+  '@rollup/rollup-linux-x64-gnu@4.39.0':
     optional: true
 
-  '@rollup/rollup-linux-x64-musl@4.38.0':
+  '@rollup/rollup-linux-x64-musl@4.39.0':
     optional: true
 
-  '@rollup/rollup-win32-arm64-msvc@4.38.0':
+  '@rollup/rollup-win32-arm64-msvc@4.39.0':
     optional: true
 
-  '@rollup/rollup-win32-ia32-msvc@4.38.0':
+  '@rollup/rollup-win32-ia32-msvc@4.39.0':
     optional: true
 
-  '@rollup/rollup-win32-x64-msvc@4.38.0':
+  '@rollup/rollup-win32-x64-msvc@4.39.0':
     optional: true
 
   '@rtsao/scc@1.1.0': {}
@@ -4374,14 +4380,14 @@ snapshots:
       '@typescript-eslint/types': 8.28.0
       eslint-visitor-keys: 4.2.0
 
-  '@vitejs/plugin-react@4.3.4(vite@6.2.4(@types/node@22.13.14)(jiti@1.21.7)(less@4.2.2)(yaml@2.7.1))':
+  '@vitejs/plugin-react@4.3.4(vite@6.2.5(@types/node@22.13.14)(jiti@1.21.7)(less@4.2.2)(yaml@2.7.1))':
     dependencies:
       '@babel/core': 7.26.0
       '@babel/plugin-transform-react-jsx-self': 7.25.9(@babel/core@7.26.0)
       '@babel/plugin-transform-react-jsx-source': 7.25.9(@babel/core@7.26.0)
       '@types/babel__core': 7.20.5
       react-refresh: 0.14.2
-      vite: 6.2.4(@types/node@22.13.14)(jiti@1.21.7)(less@4.2.2)(yaml@2.7.1)
+      vite: 6.2.5(@types/node@22.13.14)(jiti@1.21.7)(less@4.2.2)(yaml@2.7.1)
     transitivePeerDependencies:
       - supports-color
 
@@ -4392,13 +4398,13 @@ snapshots:
       chai: 5.2.0
       tinyrainbow: 2.0.0
 
-  '@vitest/mocker@3.0.9(vite@6.2.4(@types/node@22.13.14)(jiti@1.21.7)(less@4.2.2)(yaml@2.7.1))':
+  '@vitest/mocker@3.0.9(vite@6.2.5(@types/node@22.13.14)(jiti@1.21.7)(less@4.2.2)(yaml@2.7.1))':
     dependencies:
       '@vitest/spy': 3.0.9
       estree-walker: 3.0.3
       magic-string: 0.30.17
     optionalDependencies:
-      vite: 6.2.4(@types/node@22.13.14)(jiti@1.21.7)(less@4.2.2)(yaml@2.7.1)
+      vite: 6.2.5(@types/node@22.13.14)(jiti@1.21.7)(less@4.2.2)(yaml@2.7.1)
 
   '@vitest/pretty-format@3.0.9':
     dependencies:
@@ -6706,30 +6712,30 @@ snapshots:
 
   reusify@1.0.4: {}
 
-  rollup@4.38.0:
+  rollup@4.39.0:
     dependencies:
       '@types/estree': 1.0.7
     optionalDependencies:
-      '@rollup/rollup-android-arm-eabi': 4.38.0
-      '@rollup/rollup-android-arm64': 4.38.0
-      '@rollup/rollup-darwin-arm64': 4.38.0
-      '@rollup/rollup-darwin-x64': 4.38.0
-      '@rollup/rollup-freebsd-arm64': 4.38.0
-      '@rollup/rollup-freebsd-x64': 4.38.0
-      '@rollup/rollup-linux-arm-gnueabihf': 4.38.0
-      '@rollup/rollup-linux-arm-musleabihf': 4.38.0
-      '@rollup/rollup-linux-arm64-gnu': 4.38.0
-      '@rollup/rollup-linux-arm64-musl': 4.38.0
-      '@rollup/rollup-linux-loongarch64-gnu': 4.38.0
-      '@rollup/rollup-linux-powerpc64le-gnu': 4.38.0
-      '@rollup/rollup-linux-riscv64-gnu': 4.38.0
-      '@rollup/rollup-linux-riscv64-musl': 4.38.0
-      '@rollup/rollup-linux-s390x-gnu': 4.38.0
-      '@rollup/rollup-linux-x64-gnu': 4.38.0
-      '@rollup/rollup-linux-x64-musl': 4.38.0
-      '@rollup/rollup-win32-arm64-msvc': 4.38.0
-      '@rollup/rollup-win32-ia32-msvc': 4.38.0
-      '@rollup/rollup-win32-x64-msvc': 4.38.0
+      '@rollup/rollup-android-arm-eabi': 4.39.0
+      '@rollup/rollup-android-arm64': 4.39.0
+      '@rollup/rollup-darwin-arm64': 4.39.0
+      '@rollup/rollup-darwin-x64': 4.39.0
+      '@rollup/rollup-freebsd-arm64': 4.39.0
+      '@rollup/rollup-freebsd-x64': 4.39.0
+      '@rollup/rollup-linux-arm-gnueabihf': 4.39.0
+      '@rollup/rollup-linux-arm-musleabihf': 4.39.0
+      '@rollup/rollup-linux-arm64-gnu': 4.39.0
+      '@rollup/rollup-linux-arm64-musl': 4.39.0
+      '@rollup/rollup-linux-loongarch64-gnu': 4.39.0
+      '@rollup/rollup-linux-powerpc64le-gnu': 4.39.0
+      '@rollup/rollup-linux-riscv64-gnu': 4.39.0
+      '@rollup/rollup-linux-riscv64-musl': 4.39.0
+      '@rollup/rollup-linux-s390x-gnu': 4.39.0
+      '@rollup/rollup-linux-x64-gnu': 4.39.0
+      '@rollup/rollup-linux-x64-musl': 4.39.0
+      '@rollup/rollup-win32-arm64-msvc': 4.39.0
+      '@rollup/rollup-win32-ia32-msvc': 4.39.0
+      '@rollup/rollup-win32-x64-msvc': 4.39.0
       fsevents: 2.3.3
 
   run-parallel@1.2.0:
@@ -7149,7 +7155,7 @@ snapshots:
       debug: 4.4.0
       es-module-lexer: 1.6.0
       pathe: 2.0.3
-      vite: 6.2.4(@types/node@22.13.14)(jiti@1.21.7)(less@4.2.2)(yaml@2.7.1)
+      vite: 6.2.5(@types/node@22.13.14)(jiti@1.21.7)(less@4.2.2)(yaml@2.7.1)
     transitivePeerDependencies:
       - '@types/node'
       - jiti
@@ -7164,12 +7170,12 @@ snapshots:
       - tsx
       - yaml
 
-  vite-plugin-compression@0.5.1(vite@6.2.4(@types/node@22.13.14)(jiti@1.21.7)(less@4.2.2)(yaml@2.7.1)):
+  vite-plugin-compression@0.5.1(vite@6.2.5(@types/node@22.13.14)(jiti@1.21.7)(less@4.2.2)(yaml@2.7.1)):
     dependencies:
       chalk: 4.1.2
       debug: 4.4.0
       fs-extra: 10.1.0
-      vite: 6.2.4(@types/node@22.13.14)(jiti@1.21.7)(less@4.2.2)(yaml@2.7.1)
+      vite: 6.2.5(@types/node@22.13.14)(jiti@1.21.7)(less@4.2.2)(yaml@2.7.1)
     transitivePeerDependencies:
       - supports-color
 
@@ -7177,22 +7183,22 @@ snapshots:
     dependencies:
       monaco-editor: 0.52.2
 
-  vite-tsconfig-paths@5.1.4(typescript@5.8.2)(vite@6.2.4(@types/node@22.13.14)(jiti@1.21.7)(less@4.2.2)(yaml@2.7.1)):
+  vite-tsconfig-paths@5.1.4(typescript@5.8.2)(vite@6.2.5(@types/node@22.13.14)(jiti@1.21.7)(less@4.2.2)(yaml@2.7.1)):
     dependencies:
       debug: 4.4.0
       globrex: 0.1.2
       tsconfck: 3.1.4(typescript@5.8.2)
     optionalDependencies:
-      vite: 6.2.4(@types/node@22.13.14)(jiti@1.21.7)(less@4.2.2)(yaml@2.7.1)
+      vite: 6.2.5(@types/node@22.13.14)(jiti@1.21.7)(less@4.2.2)(yaml@2.7.1)
     transitivePeerDependencies:
       - supports-color
       - typescript
 
-  vite@6.2.4(@types/node@22.13.14)(jiti@1.21.7)(less@4.2.2)(yaml@2.7.1):
+  vite@6.2.5(@types/node@22.13.14)(jiti@1.21.7)(less@4.2.2)(yaml@2.7.1):
     dependencies:
       esbuild: 0.25.2
       postcss: 8.5.3
-      rollup: 4.38.0
+      rollup: 4.39.0
     optionalDependencies:
       '@types/node': 22.13.14
       fsevents: 2.3.3
@@ -7203,7 +7209,7 @@ snapshots:
   vitest@3.0.9(@types/node@22.13.14)(jiti@1.21.7)(less@4.2.2)(yaml@2.7.1):
     dependencies:
       '@vitest/expect': 3.0.9
-      '@vitest/mocker': 3.0.9(vite@6.2.4(@types/node@22.13.14)(jiti@1.21.7)(less@4.2.2)(yaml@2.7.1))
+      '@vitest/mocker': 3.0.9(vite@6.2.5(@types/node@22.13.14)(jiti@1.21.7)(less@4.2.2)(yaml@2.7.1))
       '@vitest/pretty-format': 3.0.9
       '@vitest/runner': 3.0.9
       '@vitest/snapshot': 3.0.9
@@ -7219,7 +7225,7 @@ snapshots:
       tinyexec: 0.3.2
       tinypool: 1.0.2
       tinyrainbow: 2.0.0
-      vite: 6.2.4(@types/node@22.13.14)(jiti@1.21.7)(less@4.2.2)(yaml@2.7.1)
+      vite: 6.2.5(@types/node@22.13.14)(jiti@1.21.7)(less@4.2.2)(yaml@2.7.1)
       vite-node: 3.0.9(@types/node@22.13.14)(jiti@1.21.7)(less@4.2.2)(yaml@2.7.1)
       why-is-node-running: 2.3.0
     optionalDependencies:
diff --git a/ui/src/app.tsx b/ui/src/app.tsx
index 18ef7353..836ac64d 100644
--- a/ui/src/app.tsx
+++ b/ui/src/app.tsx
@@ -43,15 +43,6 @@ export const App = () => (
                   <Route path={paths.warehouse} element={<Project />} />
                   <Route path={paths.downloads} element={<Downloads />} />
                   <Route path={paths.user} element={<User />} />
-                  <Route path={paths.settings} element={<Settings />} />
-                  <Route
-                    path={paths.settingsAnalysisTemplates}
-                    element={<Settings section='verification' />}
-                  />
-                  <Route
-                    path={paths.settingsClusterPromotionTasks}
-                    element={<Settings section='cluster-promotion-tasks' />}
-                  />
                   <Route
                     path={paths.createStage}
                     element={<Project tab='pipelines' creatingStage={true} />}
@@ -65,6 +56,10 @@ export const App = () => (
                     <Route index element={<ProjectSettings />} />
                     <Route path='*' element={<ProjectSettings />} />
                   </Route>
+                  <Route path={paths.settings}>
+                    <Route index element={<Settings />} />
+                    <Route path='*' element={<Settings />} />
+                  </Route>
                 </Route>
                 <Route path={paths.analysisRunLogs} element={<AnalysisRunLogsPage />} />
               </Route>
diff --git a/ui/src/config/paths.ts b/ui/src/config/paths.ts
index 63524413..3e3262ce 100644
--- a/ui/src/config/paths.ts
+++ b/ui/src/config/paths.ts
@@ -10,9 +10,7 @@ export const paths = {
   createWarehouse: '/project/:name/create-warehouse',
   projectSettings: '/project/:name/settings',
   user: '/user',
-  settings: '/settings/',
-  settingsAnalysisTemplates: '/settings/analysis-templates',
-  settingsClusterPromotionTasks: '/settings/cluster-promotion-tasks',
+  settings: '/settings',
   analysisRunLogs: '/ext/project/:name/stage/:stageName/analysis-run/:analysisRunId/logs',
   promotionTasks: '/project/:name/promotion-tasks',
 
diff --git a/ui/src/extensions/extensions-context.ts b/ui/src/extensions/extensions-context.ts
new file mode 100644
index 00000000..c408ebef
--- /dev/null
+++ b/ui/src/extensions/extensions-context.ts
@@ -0,0 +1,17 @@
+import React from 'react';
+
+import { Extension } from './types';
+
+type ExtensionsContextType = {
+  extensions: Extension[];
+};
+
+export const ExtensionsContext = React.createContext<ExtensionsContextType | null>(null);
+
+export const useExtensionsContext = () => {
+  const ctx = React.useContext(ExtensionsContext);
+
+  return {
+    stageTabs: ctx?.extensions.filter((extension) => extension.type === 'stageTab') || []
+  };
+};
diff --git a/ui/src/extensions/types.ts b/ui/src/extensions/types.ts
new file mode 100644
index 00000000..3a5e9d1a
--- /dev/null
+++ b/ui/src/extensions/types.ts
@@ -0,0 +1,16 @@
+import React from 'react';
+
+import { Stage } from '@ui/gen/api/v1alpha1/generated_pb';
+
+export type ExtensionStageTabComponentProps = {
+  stage: Stage;
+};
+
+export type ExtensionStageTab = {
+  type: 'stageTab';
+  component: ({ stage }: ExtensionStageTabComponentProps) => React.ReactNode;
+  label: string;
+  icon?: React.ReactNode;
+};
+
+export type Extension = ExtensionStageTab;
diff --git a/ui/src/features/settings/analysis-templates/analysis-templates.tsx b/ui/src/features/settings/analysis-templates/analysis-templates.tsx
index 6f9cc5b2..d75a48e3 100644
--- a/ui/src/features/settings/analysis-templates/analysis-templates.tsx
+++ b/ui/src/features/settings/analysis-templates/analysis-templates.tsx
@@ -1,7 +1,7 @@
 import { useMutation, useQuery } from '@connectrpc/connect-query';
 import { faPencil, faPlus, faTrash } from '@fortawesome/free-solid-svg-icons';
 import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import { Button, Table } from 'antd';
+import { Button, Card, Table } from 'antd';
 import { format } from 'date-fns';
 
 import { useConfirmModal } from '@ui/features/common/confirm-modal/use-confirm-modal';
@@ -34,83 +34,87 @@ export const ClusterAnalysisTemplatesList = () => {
   );
 
   return (
-    <Table<ClusterAnalysisTemplate>
-      dataSource={data?.clusterAnalysisTemplates}
-      pagination={{ hideOnSinglePage: true }}
-      rowKey={(i) => i.metadata?.name || ''}
-      loading={isLoading}
-      expandable={descriptionExpandable()}
-      className='w-full'
+    <Card
+      title='Cluster Analysis Templates'
+      type='inner'
+      className='min-h-full'
+      extra={
+        <Button icon={<FontAwesomeIcon icon={faPlus} />} onClick={() => showCreate()}>
+          Add Template
+        </Button>
+      }
     >
-      <Table.Column<ClusterAnalysisTemplate>
-        title='Creation Date'
-        width={200}
-        render={(_, template) => {
-          const date = timestampDate(template.metadata?.creationTimestamp);
-          return date ? format(date, 'MMM do yyyy HH:mm:ss') : '';
-        }}
-      />
-      <Table.Column<ClusterAnalysisTemplate> title='Name' dataIndex={['metadata', 'name']} />
-      <Table.Column<ClusterAnalysisTemplate>
-        width={260}
-        title={
-          <div className='text-right'>
-            <Button
-              type='primary'
-              className='ml-auto text-xs font-semibold'
-              icon={<FontAwesomeIcon icon={faPlus} />}
-              onClick={() => showCreate()}
-            >
-              ADD TEMPLATE
-            </Button>
-          </div>
-        }
-        render={(_, template) => (
-          <div className='flex gap-2 justify-end'>
-            <Button
-              icon={<FontAwesomeIcon icon={faPencil} />}
-              className='mr-2 ml-auto'
-              onClick={() => {
-                showEdit((p) => (
-                  <EditClusterAnalysisTemplateModal
-                    {...p}
-                    templateName={template.metadata?.name || ''}
-                  />
-                ));
-              }}
-            >
-              Edit
-            </Button>
-            <Button
-              icon={<FontAwesomeIcon icon={faTrash} />}
-              danger
-              loading={isDeleting}
-              onClick={() => {
-                confirm({
-                  title: (
-                    <div className='flex items-center'>
-                      <FontAwesomeIcon icon={faTrash} className='mr-2' />
-                      Delete Cluster Analysis Template
-                    </div>
-                  ),
-                  content: (
-                    <p>
-                      Are you sure you want to delete ClusterAnalysisTemplate{' '}
-                      <b>{template?.metadata?.name}</b>?
-                    </p>
-                  ),
-                  onOk: () => {
-                    deleteTemplate({ name: template?.metadata?.name || '' });
-                  },
-                  hide: () => {}
-                });
-              }}
-            >
-              Delete
-            </Button>
-          </div>
-        )}
-      />
-    </Table>
+      <Table<ClusterAnalysisTemplate>
+        dataSource={data?.clusterAnalysisTemplates}
+        pagination={{ hideOnSinglePage: true }}
+        rowKey={(i) => i.metadata?.name || ''}
+        loading={isLoading}
+        expandable={descriptionExpandable()}
+        className='w-full'
+      >
+        <Table.Column<ClusterAnalysisTemplate>
+          title='Creation Date'
+          width={200}
+          render={(_, template) => {
+            const date = timestampDate(template.metadata?.creationTimestamp);
+            return date ? format(date, 'MMM do yyyy HH:mm:ss') : '';
+          }}
+        />
+        <Table.Column<ClusterAnalysisTemplate> title='Name' dataIndex={['metadata', 'name']} />
+        <Table.Column<ClusterAnalysisTemplate>
+          width={260}
+          render={(_, template) => (
+            <div className='flex gap-2 justify-end'>
+              <Button
+                icon={<FontAwesomeIcon icon={faPencil} />}
+                className='mr-2 ml-auto'
+                onClick={() => {
+                  showEdit((p) => (
+                    <EditClusterAnalysisTemplateModal
+                      {...p}
+                      templateName={template.metadata?.name || ''}
+                    />
+                  ));
+                }}
+                size='small'
+                color='default'
+                variant='filled'
+              >
+                Edit
+              </Button>
+              <Button
+                icon={<FontAwesomeIcon icon={faTrash} />}
+                loading={isDeleting}
+                onClick={() => {
+                  confirm({
+                    title: (
+                      <div className='flex items-center'>
+                        <FontAwesomeIcon icon={faTrash} className='mr-2' />
+                        Delete Cluster Analysis Template
+                      </div>
+                    ),
+                    content: (
+                      <p>
+                        Are you sure you want to delete ClusterAnalysisTemplate{' '}
+                        <b>{template?.metadata?.name}</b>?
+                      </p>
+                    ),
+                    onOk: () => {
+                      deleteTemplate({ name: template?.metadata?.name || '' });
+                    },
+                    hide: () => {}
+                  });
+                }}
+                size='small'
+                color='danger'
+                variant='filled'
+              >
+                Delete
+              </Button>
+            </div>
+          )}
+        />
+      </Table>
+    </Card>
   );
 };
diff --git a/ui/src/features/settings/cluster-promotion-tasks/cluster-promotion-tasks.tsx b/ui/src/features/settings/cluster-promotion-tasks/cluster-promotion-tasks.tsx
index 8acc80da..8f130d2e 100644
--- a/ui/src/features/settings/cluster-promotion-tasks/cluster-promotion-tasks.tsx
+++ b/ui/src/features/settings/cluster-promotion-tasks/cluster-promotion-tasks.tsx
@@ -1,7 +1,7 @@
 import { useMutation, useQuery } from '@connectrpc/connect-query';
 import { faPencil, faPlus, faTrash } from '@fortawesome/free-solid-svg-icons';
 import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import { Button, Flex, Table } from 'antd';
+import { Button, Card, Flex, Table } from 'antd';
 import { format } from 'date-fns';
 
 import { useConfirmModal } from '@ui/features/common/confirm-modal/use-confirm-modal';
@@ -61,7 +61,19 @@ export const ClusterPromotionTasks = () => {
     ));
 
   return (
-    <div className='p-4'>
+    <Card
+      title='Cluster Promotion Tasks'
+      type='inner'
+      className='min-h-full'
+      extra={
+        <Button
+          icon={<FontAwesomeIcon icon={faPlus} />}
+          onClick={() => onAddClusterPromotionTaskModalOpen()}
+        >
+          Add Cluster Promotion Task
+        </Button>
+      }
+    >
       <Table<ClusterPromotionTask>
         dataSource={listClusterPromotionTasksQuery.data?.clusterPromotionTasks}
         loading={listClusterPromotionTasksQuery.isFetching}
@@ -94,30 +106,23 @@ export const ClusterPromotionTasks = () => {
             render: (_, r) => r.metadata?.name
           },
           {
-            title: (
-              <div className='text-right'>
-                <Button
-                  type='primary'
-                  className='ml-auto text-xs font-semibold'
-                  icon={<FontAwesomeIcon icon={faPlus} />}
-                  onClick={onAddClusterPromotionTaskModalOpen}
-                >
-                  ADD CLUSTER PROMOTION TASK
-                </Button>
-              </div>
-            ),
             render: (_, template) => (
               <Flex gap={8} justify='end'>
                 <Button
                   icon={<FontAwesomeIcon icon={faPencil} />}
                   onClick={() => onEditClusterPromotionTaskModalOpen(template)}
+                  size='small'
+                  color='default'
+                  variant='filled'
                 >
                   Edit
                 </Button>
                 <Button
                   icon={<FontAwesomeIcon icon={faTrash} />}
-                  danger
                   onClick={() => onDeleteClusterPromotionTaskModalOpen(template)}
+                  size='small'
+                  color='danger'
+                  variant='filled'
                 >
                   Delete
                 </Button>
@@ -126,6 +131,6 @@ export const ClusterPromotionTasks = () => {
           }
         ]}
       />
-    </div>
+    </Card>
   );
 };
diff --git a/ui/src/features/settings/cluster-promotion-tasks/create-cluster-promotion-task.tsx b/ui/src/features/settings/cluster-promotion-tasks/create-cluster-promotion-task.tsx
index 10d70a69..71570c4d 100644
--- a/ui/src/features/settings/cluster-promotion-tasks/create-cluster-promotion-task.tsx
+++ b/ui/src/features/settings/cluster-promotion-tasks/create-cluster-promotion-task.tsx
@@ -1,5 +1,6 @@
 import { createConnectQueryKey, useMutation } from '@connectrpc/connect-query';
 import { useQueryClient } from '@tanstack/react-query';
+import { Button, Flex, Space } from 'antd';
 import Modal from 'antd/es/modal/Modal';
 import Link from 'antd/es/typography/Link';
 import { useForm } from 'react-hook-form';
@@ -52,21 +53,24 @@ export const CreateClusterPromotionTaskModal = (props: CreateClusterPromotionTas
     <Modal
       open={props.visible}
       onCancel={props.hide}
-      title={
-        <>
-          Create Cluster Promotion Task{' '}
+      title='Create Cluster Promotion Task'
+      width={700}
+      footer={
+        <Flex justify='space-between' align='center'>
           <Link
             href='https://docs.kargo.io/user-guide/reference-docs/promotion-tasks/#defining-a-global-promotion-task'
             target='_blank'
-            className='ml-1 text-xs'
           >
-            documentation
+            Documentation
           </Link>
-        </>
+          <Space>
+            <Button onClick={props.hide}>Cancel</Button>
+            <Button onClick={onSubmit} loading={createResourceMutation.isPending} type='primary'>
+              Create
+            </Button>
+          </Space>
+        </Flex>
       }
-      okText='Create'
-      width={700}
-      onOk={onSubmit}
     >
       <FieldContainer control={clusterPromotionTaskForm.control} name='value'>
         {({ field }) => (
diff --git a/ui/src/features/stage/stage-details.tsx b/ui/src/features/stage/stage-details.tsx
index 893fbea5..f59e60d7 100644
--- a/ui/src/features/stage/stage-details.tsx
+++ b/ui/src/features/stage/stage-details.tsx
@@ -13,6 +13,7 @@ import { useEffect, useMemo, useState } from 'react';
 import { generatePath, useNavigate, useParams } from 'react-router-dom';
 
 import { paths } from '@ui/config/paths';
+import { useExtensionsContext } from '@ui/extensions/extensions-context';
 import { Description } from '@ui/features/common/description';
 import { HealthStatusIcon } from '@ui/features/common/health-status/health-status-icon';
 import { StagePhaseIcon } from '@ui/features/common/stage-phase/stage-phase-icon';
@@ -93,6 +94,8 @@ export const StageDetails = ({ stage }: { stage: Stage }) => {
   const shardKey = stage?.metadata?.labels['kargo.akuity.io/shard'] || '';
   const argocdShard = config?.argocdShards?.[shardKey];
 
+  const { stageTabs } = useExtensionsContext();
+
   return (
     <Drawer
       open={!!stageName}
@@ -180,6 +183,12 @@ export const StageDetails = ({ stage }: { stage: Stage }) => {
                     />
                   )
                 },
+                ...stageTabs.map((data, index) => ({
+                  children: <data.component stage={stage} />,
+                  key: String(data.label + index),
+                  label: data.label,
+                  icon: data.icon
+                })),
                 {
                   key: TabsTypes.SETTINGS,
                   label: 'Settings',
diff --git a/ui/src/gen/api/v1alpha1/generated_pb.ts b/ui/src/gen/api/v1alpha1/generated_pb.ts
index 5a86e428..adebccb7 100644
--- a/ui/src/gen/api/v1alpha1/generated_pb.ts
+++ b/ui/src/gen/api/v1alpha1/generated_pb.ts
@@ -18,7 +18,7 @@ import type { Message } from "@bufbuild/protobuf";
  * Describes the file api/v1alpha1/generated.proto.
  */
 export const file_api_v1alpha1_generated: GenFile = /*@__PURE__*/
-  fileDesc("ChxhcGkvdjFhbHBoYTEvZ2VuZXJhdGVkLnByb3RvEiRnaXRodWIuY29tLmFrdWl0eS5rYXJnby5hcGkudjFhbHBoYTEiMgoTQW5hbHlzaXNSdW5Bcmd1bWVudBIMCgRuYW1lGAEgASgJEg0KBXZhbHVlGAIgASgJIrACChNBbmFseXNpc1J1bk1ldGFkYXRhElUKBmxhYmVscxgBIAMoCzJFLmdpdGh1Yi5jb20uYWt1aXR5LmthcmdvLmFwaS52MWFscGhhMS5BbmFseXNpc1J1bk1ldGFkYXRhLkxhYmVsc0VudHJ5El8KC2Fubm90YXRpb25zGAIgAygLMkouZ2l0aHViLmNvbS5ha3VpdHkua2FyZ28uYXBpLnYxYWxwaGExLkFuYWx5c2lzUnVuTWV0YWRhdGEuQW5ub3RhdGlvbnNFbnRyeRotCgtMYWJlbHNFbnRyeRILCgNrZXkYASABKAkSDQoFdmFsdWUYAiABKAk6AjgBGjIKEEFubm90YXRpb25zRW50cnkSCwoDa2V5GAEgASgJEg0KBXZhbHVlGAIgASgJOgI4ASJGChRBbmFseXNpc1J1blJlZmVyZW5jZRIRCgluYW1lc3BhY2UYASABKAkSDAoEbmFtZRgCIAEoCRINCgVwaGFzZRgDIAEoCSI3ChlBbmFseXNpc1RlbXBsYXRlUmVmZXJlbmNlEgwKBG5hbWUYASABKAkSDAoEa2luZBgCIAEoCSJPCg1BcHByb3ZlZFN0YWdlEj4KCmFwcHJvdmVkQXQYASABKAsyKi5rOHMuaW8uYXBpbWFjaGluZXJ5LnBrZy5hcGlzLm1ldGEudjEuVGltZSI4ChVBcmdvQ0RBcHBIZWFsdGhTdGF0dXMSDgoGc3RhdHVzGAEgASgJEg8KB21lc3NhZ2UYAiABKAki1AEKD0FyZ29DREFwcFN0YXR1cxIRCgluYW1lc3BhY2UYASABKAkSDAoEbmFtZRgCIAEoCRJRCgxoZWFsdGhTdGF0dXMYAyABKAsyOy5naXRodWIuY29tLmFrdWl0eS5rYXJnby5hcGkudjFhbHBoYTEuQXJnb0NEQXBwSGVhbHRoU3RhdHVzEk0KCnN5bmNTdGF0dXMYBCABKAsyOS5naXRodWIuY29tLmFrdWl0eS5rYXJnby5hcGkudjFhbHBoYTEuQXJnb0NEQXBwU3luY1N0YXR1cyJKChNBcmdvQ0RBcHBTeW5jU3RhdHVzEg4KBnN0YXR1cxgBIAEoCRIQCghyZXZpc2lvbhgCIAEoCRIRCglyZXZpc2lvbnMYAyADKAkiNwoFQ2hhcnQSDwoHcmVwb1VSTBgBIAEoCRIMCgRuYW1lGAIgASgJEg8KB3ZlcnNpb24YAyABKAkiYQoUQ2hhcnREaXNjb3ZlcnlSZXN1bHQSDwoHcmVwb1VSTBgBIAEoCRIMCgRuYW1lGAIgASgJEhgKEHNlbXZlckNvbnN0cmFpbnQYAyABKAkSEAoIdmVyc2lvbnMYBCADKAkiZAoRQ2hhcnRTdWJzY3JpcHRpb24SDwoHcmVwb1VSTBgBIAEoCRIMCgRuYW1lGAIgASgJEhgKEHNlbXZlckNvbnN0cmFpbnQYAyABKAkSFgoOZGlzY292ZXJ5TGltaXQYBCABKAUioQEKFENsdXN0ZXJQcm9tb3Rpb25UYXNrEkIKCG1ldGFkYXRhGAEgASgLMjAuazhzLmlvLmFwaW1hY2hpbmVyeS5wa2cuYXBpcy5tZXRhLnYxLk9iamVjdE1ldGESRQoEc3BlYxgCIAEoCzI3LmdpdGh1Yi5jb20uYWt1aXR5LmthcmdvLmFwaS52MWFscGhhMS5Qcm9tb3Rpb25UYXNrU3BlYyKnAQoYQ2x1c3RlclByb21vdGlvblRhc2tMaXN0EkAKCG1ldGFkYXRhGAEgASgLMi4uazhzLmlvLmFwaW1hY2hpbmVyeS5wa2cuYXBpcy5tZXRhLnYxLkxpc3RNZXRhEkkKBWl0ZW1zGAIgAygLMjouZ2l0aHViLmNvbS5ha3VpdHkua2FyZ28uYXBpLnYxYWxwaGExLkNsdXN0ZXJQcm9tb3Rpb25UYXNrIkkKDEN1cnJlbnRTdGFnZRI5CgVzaW5jZRgBIAEoCzIqLms4cy5pby5hcGltYWNoaW5lcnkucGtnLmFwaXMubWV0YS52MS5UaW1lIrYCChNEaXNjb3ZlcmVkQXJ0aWZhY3RzEkAKDGRpc2NvdmVyZWRBdBgEIAEoCzIqLms4cy5pby5hcGltYWNoaW5lcnkucGtnLmFwaXMubWV0YS52MS5UaW1lEkUKA2dpdBgBIAMoCzI4LmdpdGh1Yi5jb20uYWt1aXR5LmthcmdvLmFwaS52MWFscGhhMS5HaXREaXNjb3ZlcnlSZXN1bHQSSgoGaW1hZ2VzGAIgAygLMjouZ2l0aHViLmNvbS5ha3VpdHkua2FyZ28uYXBpLnYxYWxwaGExLkltYWdlRGlzY292ZXJ5UmVzdWx0EkoKBmNoYXJ0cxgDIAMoCzI6LmdpdGh1Yi5jb20uYWt1aXR5LmthcmdvLmFwaS52MWFscGhhMS5DaGFydERpc2NvdmVyeVJlc3VsdCKwAQoQRGlzY292ZXJlZENvbW1pdBIKCgJpZBgBIAEoCRIOCgZicmFuY2gYAiABKAkSCwoDdGFnGAMgASgJEg8KB3N1YmplY3QYBCABKAkSDgoGYXV0aG9yGAUgASgJEhEKCWNvbW1pdHRlchgGIAEoCRI/CgtjcmVhdG9yRGF0ZRgHIAEoCzIqLms4cy5pby5hcGltYWNoaW5lcnkucGtnLmFwaXMubWV0YS52MS5UaW1lIqQCChhEaXNjb3ZlcmVkSW1hZ2VSZWZlcmVuY2USCwoDdGFnGAEgASgJEg4KBmRpZ2VzdBgCIAEoCRJkCgthbm5vdGF0aW9ucxgFIAMoCzJPLmdpdGh1Yi5jb20uYWt1aXR5LmthcmdvLmFwaS52MWFscGhhMS5EaXNjb3ZlcmVkSW1hZ2VSZWZlcmVuY2UuQW5ub3RhdGlvbnNFbnRyeRISCgpnaXRSZXBvVVJMGAMgASgJEj0KCWNyZWF0ZWRBdBgEIAEoCzIqLms4cy5pby5hcGltYWNoaW5lcnkucGtnLmFwaXMubWV0YS52MS5UaW1lGjIKEEFubm90YXRpb25zRW50cnkSCwoDa2V5GAEgASgJEg0KBXZhbHVlGAIgASgJOgI4ASKiAwoHRnJlaWdodBJCCghtZXRhZGF0YRgBIAEoCzIwLms4cy5pby5hcGltYWNoaW5lcnkucGtnLmFwaXMubWV0YS52MS5PYmplY3RNZXRhEg0KBWFsaWFzGAcgASgJEkMKBm9yaWdpbhgJIAEoCzIzLmdpdGh1Yi5jb20uYWt1aXR5LmthcmdvLmFwaS52MWFscGhhMS5GcmVpZ2h0T3JpZ2luEkAKB2NvbW1pdHMYAyADKAsyLy5naXRodWIuY29tLmFrdWl0eS5rYXJnby5hcGkudjFhbHBoYTEuR2l0Q29tbWl0EjsKBmltYWdlcxgEIAMoCzIrLmdpdGh1Yi5jb20uYWt1aXR5LmthcmdvLmFwaS52MWFscGhhMS5JbWFnZRI7CgZjaGFydHMYBSADKAsyKy5naXRodWIuY29tLmFrdWl0eS5rYXJnby5hcGkudjFhbHBoYTEuQ2hhcnQSQwoGc3RhdHVzGAYgASgLMjMuZ2l0aHViLmNvbS5ha3VpdHkua2FyZ28uYXBpLnYxYWxwaGExLkZyZWlnaHRTdGF0dXMirQIKEUZyZWlnaHRDb2xsZWN0aW9uEgoKAmlkGAMgASgJElEKBWl0ZW1zGAEgAygLMkIuZ2l0aHViLmNvbS5ha3VpdHkua2FyZ28uYXBpLnYxYWxwaGExLkZyZWlnaHRDb2xsZWN0aW9uLkl0ZW1zRW50cnkSUwoTdmVyaWZpY2F0aW9uSGlzdG9yeRgCIAMoCzI2LmdpdGh1Yi5jb20uYWt1aXR5LmthcmdvLmFwaS52MWFscGhhMS5WZXJpZmljYXRpb25JbmZvGmQKCkl0ZW1zRW50cnkSCwoDa2V5GAEgASgJEkUKBXZhbHVlGAIgASgLMjYuZ2l0aHViLmNvbS5ha3VpdHkua2FyZ28uYXBpLnYxYWxwaGExLkZyZWlnaHRSZWZlcmVuY2U6AjgBIo0BCgtGcmVpZ2h0TGlzdBJACghtZXRhZGF0YRgBIAEoCzIuLms4cy5pby5hcGltYWNoaW5lcnkucGtnLmFwaXMubWV0YS52MS5MaXN0TWV0YRI8CgVpdGVtcxgCIAMoCzItLmdpdGh1Yi5jb20uYWt1aXR5LmthcmdvLmFwaS52MWFscGhhMS5GcmVpZ2h0IisKDUZyZWlnaHRPcmlnaW4SDAoEa2luZBgBIAEoCRIMCgRuYW1lGAIgASgJIqECChBGcmVpZ2h0UmVmZXJlbmNlEgwKBG5hbWUYASABKAkSQwoGb3JpZ2luGAggASgLMjMuZ2l0aHViLmNvbS5ha3VpdHkua2FyZ28uYXBpLnYxYWxwaGExLkZyZWlnaHRPcmlnaW4SQAoHY29tbWl0cxgCIAMoCzIvLmdpdGh1Yi5jb20uYWt1aXR5LmthcmdvLmFwaS52MWFscGhhMS5HaXRDb21taXQSOwoGaW1hZ2VzGAMgAygLMisuZ2l0aHViLmNvbS5ha3VpdHkua2FyZ28uYXBpLnYxYWxwaGExLkltYWdlEjsKBmNoYXJ0cxgEIAMoCzIrLmdpdGh1Yi5jb20uYWt1aXR5LmthcmdvLmFwaS52MWFscGhhMS5DaGFydCKcAQoORnJlaWdodFJlcXVlc3QSQwoGb3JpZ2luGAEgASgLMjMuZ2l0aHViLmNvbS5ha3VpdHkua2FyZ28uYXBpLnYxYWxwaGExLkZyZWlnaHRPcmlnaW4SRQoHc291cmNlcxgCIAEoCzI0LmdpdGh1Yi5jb20uYWt1aXR5LmthcmdvLmFwaS52MWFscGhhMS5GcmVpZ2h0U291cmNlcyKYAQoORnJlaWdodFNvdXJjZXMSDgoGZGlyZWN0GAEgASgIEg4KBnN0YWdlcxgCIAMoCRJIChByZXF1aXJlZFNvYWtUaW1lGAMgASgLMi4uazhzLmlvLmFwaW1hY2hpbmVyeS5wa2cuYXBpcy5tZXRhLnYxLkR1cmF0aW9uEhwKFGF2YWlsYWJpbGl0eVN0cmF0ZWd5GAQgASgJItcECg1GcmVpZ2h0U3RhdHVzElkKC2N1cnJlbnRseUluGAMgAygLMkQuZ2l0aHViLmNvbS5ha3VpdHkua2FyZ28uYXBpLnYxYWxwaGExLkZyZWlnaHRTdGF0dXMuQ3VycmVudGx5SW5FbnRyeRJXCgp2ZXJpZmllZEluGAEgAygLMkMuZ2l0aHViLmNvbS5ha3VpdHkua2FyZ28uYXBpLnYxYWxwaGExLkZyZWlnaHRTdGF0dXMuVmVyaWZpZWRJbkVudHJ5ElkKC2FwcHJvdmVkRm9yGAIgAygLMkQuZ2l0aHViLmNvbS5ha3VpdHkua2FyZ28uYXBpLnYxYWxwaGExLkZyZWlnaHRTdGF0dXMuQXBwcm92ZWRGb3JFbnRyeRpmChBDdXJyZW50bHlJbkVudHJ5EgsKA2tleRgBIAEoCRJBCgV2YWx1ZRgCIAEoCzIyLmdpdGh1Yi5jb20uYWt1aXR5LmthcmdvLmFwaS52MWFscGhhMS5DdXJyZW50U3RhZ2U6AjgBGmYKD1ZlcmlmaWVkSW5FbnRyeRILCgNrZXkYASABKAkSQgoFdmFsdWUYAiABKAsyMy5naXRodWIuY29tLmFrdWl0eS5rYXJnby5hcGkudjFhbHBoYTEuVmVyaWZpZWRTdGFnZToCOAEaZwoQQXBwcm92ZWRGb3JFbnRyeRILCgNrZXkYASABKAkSQgoFdmFsdWUYAiABKAsyMy5naXRodWIuY29tLmFrdWl0eS5rYXJnby5hcGkudjFhbHBoYTEuQXBwcm92ZWRTdGFnZToCOAEieQoJR2l0Q29tbWl0Eg8KB3JlcG9VUkwYASABKAkSCgoCaWQYAiABKAkSDgoGYnJhbmNoGAMgASgJEgsKA3RhZxgEIAEoCRIPCgdtZXNzYWdlGAYgASgJEg4KBmF1dGhvchgHIAEoCRIRCgljb21taXR0ZXIYCCABKAkibgoSR2l0RGlzY292ZXJ5UmVzdWx0Eg8KB3JlcG9VUkwYASABKAkSRwoHY29tbWl0cxgCIAMoCzI2LmdpdGh1Yi5jb20uYWt1aXR5LmthcmdvLmFwaS52MWFscGhhMS5EaXNjb3ZlcmVkQ29tbWl0Io4CCg9HaXRTdWJzY3JpcHRpb24SDwoHcmVwb1VSTBgBIAEoCRIfChdjb21taXRTZWxlY3Rpb25TdHJhdGVneRgCIAEoCRIOCgZicmFuY2gYAyABKAkSFQoNc3RyaWN0U2VtdmVycxgLIAEoCBIYChBzZW12ZXJDb25zdHJhaW50GAQgASgJEhEKCWFsbG93VGFncxgFIAEoCRISCgppZ25vcmVUYWdzGAYgAygJEh0KFWluc2VjdXJlU2tpcFRMU1ZlcmlmeRgHIAEoCBIUCgxpbmNsdWRlUGF0aHMYCCADKAkSFAoMZXhjbHVkZVBhdGhzGAkgAygJEhYKDmRpc2NvdmVyeUxpbWl0GAogASgFIsgBCgZIZWFsdGgSDgoGc3RhdHVzGAEgASgJEg4KBmlzc3VlcxgCIAMoCRJOCgZjb25maWcYBCABKAsyPi5rOHMuaW8uYXBpZXh0ZW5zaW9uc19hcGlzZXJ2ZXIucGtnLmFwaXMuYXBpZXh0ZW5zaW9ucy52MS5KU09OEk4KBm91dHB1dBgFIAEoCzI+Lms4cy5pby5hcGlleHRlbnNpb25zX2FwaXNlcnZlci5wa2cuYXBpcy5hcGlleHRlbnNpb25zLnYxLkpTT04ibwoPSGVhbHRoQ2hlY2tTdGVwEgwKBHVzZXMYASABKAkSTgoGY29uZmlnGAIgASgLMj4uazhzLmlvLmFwaWV4dGVuc2lvbnNfYXBpc2VydmVyLnBrZy5hcGlzLmFwaWV4dGVuc2lvbnMudjEuSlNPTiLQAQoFSW1hZ2USDwoHcmVwb1VSTBgBIAEoCRISCgpnaXRSZXBvVVJMGAIgASgJEgsKA3RhZxgDIAEoCRIOCgZkaWdlc3QYBCABKAkSUQoLYW5ub3RhdGlvbnMYBSADKAsyPC5naXRodWIuY29tLmFrdWl0eS5rYXJnby5hcGkudjFhbHBoYTEuSW1hZ2UuQW5ub3RhdGlvbnNFbnRyeRoyChBBbm5vdGF0aW9uc0VudHJ5EgsKA2tleRgBIAEoCRINCgV2YWx1ZRgCIAEoCToCOAEijQEKFEltYWdlRGlzY292ZXJ5UmVzdWx0Eg8KB3JlcG9VUkwYASABKAkSEAoIcGxhdGZvcm0YAiABKAkSUgoKcmVmZXJlbmNlcxgDIAMoCzI+LmdpdGh1Yi5jb20uYWt1aXR5LmthcmdvLmFwaS52MWFscGhhMS5EaXNjb3ZlcmVkSW1hZ2VSZWZlcmVuY2Ui+QEKEUltYWdlU3Vic2NyaXB0aW9uEg8KB3JlcG9VUkwYASABKAkSEgoKZ2l0UmVwb1VSTBgCIAEoCRIeChZpbWFnZVNlbGVjdGlvblN0cmF0ZWd5GAMgASgJEhUKDXN0cmljdFNlbXZlcnMYCiABKAgSGAoQc2VtdmVyQ29uc3RyYWludBgEIAEoCRIRCglhbGxvd1RhZ3MYBSABKAkSEgoKaWdub3JlVGFncxgGIAMoCRIQCghwbGF0Zm9ybRgHIAEoCRIdChVpbnNlY3VyZVNraXBUTFNWZXJpZnkYCCABKAgSFgoOZGlzY292ZXJ5TGltaXQYCSABKAUi0wEKB1Byb2plY3QSQgoIbWV0YWRhdGEYASABKAsyMC5rOHMuaW8uYXBpbWFjaGluZXJ5LnBrZy5hcGlzLm1ldGEudjEuT2JqZWN0TWV0YRI/CgRzcGVjGAIgASgLMjEuZ2l0aHViLmNvbS5ha3VpdHkua2FyZ28uYXBpLnYxYWxwaGExLlByb2plY3RTcGVjEkMKBnN0YXR1cxgDIAEoCzIzLmdpdGh1Yi5jb20uYWt1aXR5LmthcmdvLmFwaS52MWFscGhhMS5Qcm9qZWN0U3RhdHVzIo0BCgtQcm9qZWN0TGlzdBJACghtZXRhZGF0YRgBIAEoCzIuLms4cy5pby5hcGltYWNoaW5lcnkucGtnLmFwaXMubWV0YS52MS5MaXN0TWV0YRI8CgVpdGVtcxgCIAMoCzItLmdpdGh1Yi5jb20uYWt1aXR5LmthcmdvLmFwaS52MWFscGhhMS5Qcm9qZWN0Il8KC1Byb2plY3RTcGVjElAKEXByb21vdGlvblBvbGljaWVzGAEgAygLMjUuZ2l0aHViLmNvbS5ha3VpdHkua2FyZ28uYXBpLnYxYWxwaGExLlByb21vdGlvblBvbGljeSJ0Cg1Qcm9qZWN0U3RhdHVzEkMKCmNvbmRpdGlvbnMYAyADKAsyLy5rOHMuaW8uYXBpbWFjaGluZXJ5LnBrZy5hcGlzLm1ldGEudjEuQ29uZGl0aW9uEg0KBXBoYXNlGAEgASgJEg8KB21lc3NhZ2UYAiABKAki2QEKCVByb21vdGlvbhJCCghtZXRhZGF0YRgBIAEoCzIwLms4cy5pby5hcGltYWNoaW5lcnkucGtnLmFwaXMubWV0YS52MS5PYmplY3RNZXRhEkEKBHNwZWMYAiABKAsyMy5naXRodWIuY29tLmFrdWl0eS5rYXJnby5hcGkudjFhbHBoYTEuUHJvbW90aW9uU3BlYxJFCgZzdGF0dXMYAyABKAsyNS5naXRodWIuY29tLmFrdWl0eS5rYXJnby5hcGkudjFhbHBoYTEuUHJvbW90aW9uU3RhdHVzIpEBCg1Qcm9tb3Rpb25MaXN0EkAKCG1ldGFkYXRhGAEgASgLMi4uazhzLmlvLmFwaW1hY2hpbmVyeS5wa2cuYXBpcy5tZXRhLnYxLkxpc3RNZXRhEj4KBWl0ZW1zGAIgAygLMi8uZ2l0aHViLmNvbS5ha3VpdHkua2FyZ28uYXBpLnYxYWxwaGExLlByb21vdGlvbiI+Cg9Qcm9tb3Rpb25Qb2xpY3kSDQoFc3RhZ2UYASABKAkSHAoUYXV0b1Byb21vdGlvbkVuYWJsZWQYAiABKAgi8gEKElByb21vdGlvblJlZmVyZW5jZRIMCgRuYW1lGAEgASgJEkcKB2ZyZWlnaHQYAiABKAsyNi5naXRodWIuY29tLmFrdWl0eS5rYXJnby5hcGkudjFhbHBoYTEuRnJlaWdodFJlZmVyZW5jZRJFCgZzdGF0dXMYAyABKAsyNS5naXRodWIuY29tLmFrdWl0eS5rYXJnby5hcGkudjFhbHBoYTEuUHJvbW90aW9uU3RhdHVzEj4KCmZpbmlzaGVkQXQYBCABKAsyKi5rOHMuaW8uYXBpbWFjaGluZXJ5LnBrZy5hcGlzLm1ldGEudjEuVGltZSK6AQoNUHJvbW90aW9uU3BlYxINCgVzdGFnZRgBIAEoCRIPCgdmcmVpZ2h0GAIgASgJEkUKBHZhcnMYBCADKAsyNy5naXRodWIuY29tLmFrdWl0eS5rYXJnby5hcGkudjFhbHBoYTEuUHJvbW90aW9uVmFyaWFibGUSQgoFc3RlcHMYAyADKAsyMy5naXRodWIuY29tLmFrdWl0eS5rYXJnby5hcGkudjFhbHBoYTEuUHJvbW90aW9uU3RlcCK3BAoPUHJvbW90aW9uU3RhdHVzEhoKEmxhc3RIYW5kbGVkUmVmcmVzaBgEIAEoCRINCgVwaGFzZRgBIAEoCRIPCgdtZXNzYWdlGAIgASgJEkcKB2ZyZWlnaHQYBSABKAsyNi5naXRodWIuY29tLmFrdWl0eS5rYXJnby5hcGkudjFhbHBoYTEuRnJlaWdodFJlZmVyZW5jZRJSChFmcmVpZ2h0Q29sbGVjdGlvbhgHIAEoCzI3LmdpdGh1Yi5jb20uYWt1aXR5LmthcmdvLmFwaS52MWFscGhhMS5GcmVpZ2h0Q29sbGVjdGlvbhJLCgxoZWFsdGhDaGVja3MYCCADKAsyNS5naXRodWIuY29tLmFrdWl0eS5rYXJnby5hcGkudjFhbHBoYTEuSGVhbHRoQ2hlY2tTdGVwEj4KCmZpbmlzaGVkQXQYBiABKAsyKi5rOHMuaW8uYXBpbWFjaGluZXJ5LnBrZy5hcGlzLm1ldGEudjEuVGltZRITCgtjdXJyZW50U3RlcBgJIAEoAxJaChVzdGVwRXhlY3V0aW9uTWV0YWRhdGEYCyADKAsyOy5naXRodWIuY29tLmFrdWl0eS5rYXJnby5hcGkudjFhbHBoYTEuU3RlcEV4ZWN1dGlvbk1ldGFkYXRhEk0KBXN0YXRlGAogASgLMj4uazhzLmlvLmFwaWV4dGVuc2lvbnNfYXBpc2VydmVyLnBrZy5hcGlzLmFwaWV4dGVuc2lvbnMudjEuSlNPTiLhAgoNUHJvbW90aW9uU3RlcBIMCgR1c2VzGAEgASgJEkoKBHRhc2sYBSABKAsyPC5naXRodWIuY29tLmFrdWl0eS5rYXJnby5hcGkudjFhbHBoYTEuUHJvbW90aW9uVGFza1JlZmVyZW5jZRIKCgJhcxgCIAEoCRIKCgJpZhgHIAEoCRJHCgVyZXRyeRgEIAEoCzI4LmdpdGh1Yi5jb20uYWt1aXR5LmthcmdvLmFwaS52MWFscGhhMS5Qcm9tb3Rpb25TdGVwUmV0cnkSRQoEdmFycxgGIAMoCzI3LmdpdGh1Yi5jb20uYWt1aXR5LmthcmdvLmFwaS52MWFscGhhMS5Qcm9tb3Rpb25WYXJpYWJsZRJOCgZjb25maWcYAyABKAsyPi5rOHMuaW8uYXBpZXh0ZW5zaW9uc19hcGlzZXJ2ZXIucGtnLmFwaXMuYXBpZXh0ZW5zaW9ucy52MS5KU09OIm0KElByb21vdGlvblN0ZXBSZXRyeRI/Cgd0aW1lb3V0GAEgASgLMi4uazhzLmlvLmFwaW1hY2hpbmVyeS5wa2cuYXBpcy5tZXRhLnYxLkR1cmF0aW9uEhYKDmVycm9yVGhyZXNob2xkGAIgASgNIpoBCg1Qcm9tb3Rpb25UYXNrEkIKCG1ldGFkYXRhGAEgASgLMjAuazhzLmlvLmFwaW1hY2hpbmVyeS5wa2cuYXBpcy5tZXRhLnYxLk9iamVjdE1ldGESRQoEc3BlYxgCIAEoCzI3LmdpdGh1Yi5jb20uYWt1aXR5LmthcmdvLmFwaS52MWFscGhhMS5Qcm9tb3Rpb25UYXNrU3BlYyKZAQoRUHJvbW90aW9uVGFza0xpc3QSQAoIbWV0YWRhdGEYASABKAsyLi5rOHMuaW8uYXBpbWFjaGluZXJ5LnBrZy5hcGlzLm1ldGEudjEuTGlzdE1ldGESQgoFaXRlbXMYAiADKAsyMy5naXRodWIuY29tLmFrdWl0eS5rYXJnby5hcGkudjFhbHBoYTEuUHJvbW90aW9uVGFzayI0ChZQcm9tb3Rpb25UYXNrUmVmZXJlbmNlEgwKBG5hbWUYASABKAkSDAoEa2luZBgCIAEoCSKeAQoRUHJvbW90aW9uVGFza1NwZWMSRQoEdmFycxgBIAMoCzI3LmdpdGh1Yi5jb20uYWt1aXR5LmthcmdvLmFwaS52MWFscGhhMS5Qcm9tb3Rpb25WYXJpYWJsZRJCCgVzdGVwcxgCIAMoCzIzLmdpdGh1Yi5jb20uYWt1aXR5LmthcmdvLmFwaS52MWFscGhhMS5Qcm9tb3Rpb25TdGVwIl4KEVByb21vdGlvblRlbXBsYXRlEkkKBHNwZWMYASABKAsyOy5naXRodWIuY29tLmFrdWl0eS5rYXJnby5hcGkudjFhbHBoYTEuUHJvbW90aW9uVGVtcGxhdGVTcGVjIqIBChVQcm9tb3Rpb25UZW1wbGF0ZVNwZWMSRQoEdmFycxgCIAMoCzI3LmdpdGh1Yi5jb20uYWt1aXR5LmthcmdvLmFwaS52MWFscGhhMS5Qcm9tb3Rpb25WYXJpYWJsZRJCCgVzdGVwcxgBIAMoCzIzLmdpdGh1Yi5jb20uYWt1aXR5LmthcmdvLmFwaS52MWFscGhhMS5Qcm9tb3Rpb25TdGVwIjAKEVByb21vdGlvblZhcmlhYmxlEgwKBG5hbWUYASABKAkSDQoFdmFsdWUYAiABKAki5gEKEFJlcG9TdWJzY3JpcHRpb24SQgoDZ2l0GAEgASgLMjUuZ2l0aHViLmNvbS5ha3VpdHkua2FyZ28uYXBpLnYxYWxwaGExLkdpdFN1YnNjcmlwdGlvbhJGCgVpbWFnZRgCIAEoCzI3LmdpdGh1Yi5jb20uYWt1aXR5LmthcmdvLmFwaS52MWFscGhhMS5JbWFnZVN1YnNjcmlwdGlvbhJGCgVjaGFydBgDIAEoCzI3LmdpdGh1Yi5jb20uYWt1aXR5LmthcmdvLmFwaS52MWFscGhhMS5DaGFydFN1YnNjcmlwdGlvbiLNAQoFU3RhZ2USQgoIbWV0YWRhdGEYASABKAsyMC5rOHMuaW8uYXBpbWFjaGluZXJ5LnBrZy5hcGlzLm1ldGEudjEuT2JqZWN0TWV0YRI9CgRzcGVjGAIgASgLMi8uZ2l0aHViLmNvbS5ha3VpdHkua2FyZ28uYXBpLnYxYWxwaGExLlN0YWdlU3BlYxJBCgZzdGF0dXMYAyABKAsyMS5naXRodWIuY29tLmFrdWl0eS5rYXJnby5hcGkudjFhbHBoYTEuU3RhZ2VTdGF0dXMiiQEKCVN0YWdlTGlzdBJACghtZXRhZGF0YRgBIAEoCzIuLms4cy5pby5hcGltYWNoaW5lcnkucGtnLmFwaXMubWV0YS52MS5MaXN0TWV0YRI6CgVpdGVtcxgCIAMoCzIrLmdpdGh1Yi5jb20uYWt1aXR5LmthcmdvLmFwaS52MWFscGhhMS5TdGFnZSKIAgoJU3RhZ2VTcGVjEg0KBXNoYXJkGAQgASgJEk4KEHJlcXVlc3RlZEZyZWlnaHQYBSADKAsyNC5naXRodWIuY29tLmFrdWl0eS5rYXJnby5hcGkudjFhbHBoYTEuRnJlaWdodFJlcXVlc3QSUgoRcHJvbW90aW9uVGVtcGxhdGUYBiABKAsyNy5naXRodWIuY29tLmFrdWl0eS5rYXJnby5hcGkudjFhbHBoYTEuUHJvbW90aW9uVGVtcGxhdGUSSAoMdmVyaWZpY2F0aW9uGAMgASgLMjIuZ2l0aHViLmNvbS5ha3VpdHkua2FyZ28uYXBpLnYxYWxwaGExLlZlcmlmaWNhdGlvbiL2AwoLU3RhZ2VTdGF0dXMSQwoKY29uZGl0aW9ucxgNIAMoCzIvLms4cy5pby5hcGltYWNoaW5lcnkucGtnLmFwaXMubWV0YS52MS5Db25kaXRpb24SGgoSbGFzdEhhbmRsZWRSZWZyZXNoGAsgASgJEg0KBXBoYXNlGAEgASgJEk8KDmZyZWlnaHRIaXN0b3J5GAQgAygLMjcuZ2l0aHViLmNvbS5ha3VpdHkua2FyZ28uYXBpLnYxYWxwaGExLkZyZWlnaHRDb2xsZWN0aW9uEhYKDmZyZWlnaHRTdW1tYXJ5GAwgASgJEjwKBmhlYWx0aBgIIAEoCzIsLmdpdGh1Yi5jb20uYWt1aXR5LmthcmdvLmFwaS52MWFscGhhMS5IZWFsdGgSDwoHbWVzc2FnZRgJIAEoCRIaChJvYnNlcnZlZEdlbmVyYXRpb24YBiABKAMSUgoQY3VycmVudFByb21vdGlvbhgHIAEoCzI4LmdpdGh1Yi5jb20uYWt1aXR5LmthcmdvLmFwaS52MWFscGhhMS5Qcm9tb3Rpb25SZWZlcmVuY2USTwoNbGFzdFByb21vdGlvbhgKIAEoCzI4LmdpdGh1Yi5jb20uYWt1aXR5LmthcmdvLmFwaS52MWFscGhhMS5Qcm9tb3Rpb25SZWZlcmVuY2Ui2gEKFVN0ZXBFeGVjdXRpb25NZXRhZGF0YRINCgVhbGlhcxgBIAEoCRI9CglzdGFydGVkQXQYAiABKAsyKi5rOHMuaW8uYXBpbWFjaGluZXJ5LnBrZy5hcGlzLm1ldGEudjEuVGltZRI+CgpmaW5pc2hlZEF0GAMgASgLMiouazhzLmlvLmFwaW1hY2hpbmVyeS5wa2cuYXBpcy5tZXRhLnYxLlRpbWUSEgoKZXJyb3JDb3VudBgEIAEoDRIOCgZzdGF0dXMYBSABKAkSDwoHbWVzc2FnZRgGIAEoCSKLAgoMVmVyaWZpY2F0aW9uEloKEWFuYWx5c2lzVGVtcGxhdGVzGAEgAygLMj8uZ2l0aHViLmNvbS5ha3VpdHkua2FyZ28uYXBpLnYxYWxwaGExLkFuYWx5c2lzVGVtcGxhdGVSZWZlcmVuY2USVgoTYW5hbHlzaXNSdW5NZXRhZGF0YRgCIAEoCzI5LmdpdGh1Yi5jb20uYWt1aXR5LmthcmdvLmFwaS52MWFscGhhMS5BbmFseXNpc1J1bk1ldGFkYXRhEkcKBGFyZ3MYAyADKAsyOS5naXRodWIuY29tLmFrdWl0eS5rYXJnby5hcGkudjFhbHBoYTEuQW5hbHlzaXNSdW5Bcmd1bWVudCKdAgoQVmVyaWZpY2F0aW9uSW5mbxIKCgJpZBgEIAEoCRINCgVhY3RvchgHIAEoCRI9CglzdGFydFRpbWUYBSABKAsyKi5rOHMuaW8uYXBpbWFjaGluZXJ5LnBrZy5hcGlzLm1ldGEudjEuVGltZRINCgVwaGFzZRgBIAEoCRIPCgdtZXNzYWdlGAIgASgJEk8KC2FuYWx5c2lzUnVuGAMgASgLMjouZ2l0aHViLmNvbS5ha3VpdHkua2FyZ28uYXBpLnYxYWxwaGExLkFuYWx5c2lzUnVuUmVmZXJlbmNlEj4KCmZpbmlzaFRpbWUYBiABKAsyKi5rOHMuaW8uYXBpbWFjaGluZXJ5LnBrZy5hcGlzLm1ldGEudjEuVGltZSKUAQoNVmVyaWZpZWRTdGFnZRI+Cgp2ZXJpZmllZEF0GAEgASgLMiouazhzLmlvLmFwaW1hY2hpbmVyeS5wa2cuYXBpcy5tZXRhLnYxLlRpbWUSQwoLbG9uZ2VzdFNvYWsYAiABKAsyLi5rOHMuaW8uYXBpbWFjaGluZXJ5LnBrZy5hcGlzLm1ldGEudjEuRHVyYXRpb24i2QEKCVdhcmVob3VzZRJCCghtZXRhZGF0YRgBIAEoCzIwLms4cy5pby5hcGltYWNoaW5lcnkucGtnLmFwaXMubWV0YS52MS5PYmplY3RNZXRhEkEKBHNwZWMYAiABKAsyMy5naXRodWIuY29tLmFrdWl0eS5rYXJnby5hcGkudjFhbHBoYTEuV2FyZWhvdXNlU3BlYxJFCgZzdGF0dXMYAyABKAsyNS5naXRodWIuY29tLmFrdWl0eS5rYXJnby5hcGkudjFhbHBoYTEuV2FyZWhvdXNlU3RhdHVzIpEBCg1XYXJlaG91c2VMaXN0EkAKCG1ldGFkYXRhGAEgASgLMi4uazhzLmlvLmFwaW1hY2hpbmVyeS5wa2cuYXBpcy5tZXRhLnYxLkxpc3RNZXRhEj4KBWl0ZW1zGAIgAygLMi8uZ2l0aHViLmNvbS5ha3VpdHkua2FyZ28uYXBpLnYxYWxwaGExLldhcmVob3VzZSLOAQoNV2FyZWhvdXNlU3BlYxINCgVzaGFyZBgCIAEoCRJACghpbnRlcnZhbBgEIAEoCzIuLms4cy5pby5hcGltYWNoaW5lcnkucGtnLmFwaXMubWV0YS52MS5EdXJhdGlvbhIdChVmcmVpZ2h0Q3JlYXRpb25Qb2xpY3kYAyABKAkSTQoNc3Vic2NyaXB0aW9ucxgBIAMoCzI2LmdpdGh1Yi5jb20uYWt1aXR5LmthcmdvLmFwaS52MWFscGhhMS5SZXBvU3Vic2NyaXB0aW9uIv0BCg9XYXJlaG91c2VTdGF0dXMSQwoKY29uZGl0aW9ucxgJIAMoCzIvLms4cy5pby5hcGltYWNoaW5lcnkucGtnLmFwaXMubWV0YS52MS5Db25kaXRpb24SGgoSbGFzdEhhbmRsZWRSZWZyZXNoGAYgASgJEhoKEm9ic2VydmVkR2VuZXJhdGlvbhgEIAEoAxIVCg1sYXN0RnJlaWdodElEGAggASgJElYKE2Rpc2NvdmVyZWRBcnRpZmFjdHMYByABKAsyOS5naXRodWIuY29tLmFrdWl0eS5rYXJnby5hcGkudjFhbHBoYTEuRGlzY292ZXJlZEFydGlmYWN0c0KXAgooY29tLmdpdGh1Yi5jb20uYWt1aXR5LmthcmdvLmFwaS52MWFscGhhMUIOR2VuZXJhdGVkUHJvdG9QAVokZ2l0aHViLmNvbS9ha3VpdHkva2FyZ28vYXBpL3YxYWxwaGExogIFR0NBS0GqAiRHaXRodWIuQ29tLkFrdWl0eS5LYXJnby5BcGkuVjFhbHBoYTHKAiRHaXRodWJcQ29tXEFrdWl0eVxLYXJnb1xBcGlcVjFhbHBoYTHiAjBHaXRodWJcQ29tXEFrdWl0eVxLYXJnb1xBcGlcVjFhbHBoYTFcR1BCTWV0YWRhdGHqAilHaXRodWI6OkNvbTo6QWt1aXR5OjpLYXJnbzo6QXBpOjpWMWFscGhhMQ", [file_k8s_io_apiextensions_apiserver_pkg_apis_apiextensions_v1_generated, file_k8s_io_apimachinery_pkg_apis_meta_v1_generated, file_k8s_io_apimachinery_pkg_runtime_generated, file_k8s_io_apimachinery_pkg_runtime_schema_generated]);
+  fileDesc("ChxhcGkvdjFhbHBoYTEvZ2VuZXJhdGVkLnByb3RvEiRnaXRodWIuY29tLmFrdWl0eS5rYXJnby5hcGkudjFhbHBoYTEiMgoTQW5hbHlzaXNSdW5Bcmd1bWVudBIMCgRuYW1lGAEgASgJEg0KBXZhbHVlGAIgASgJIrACChNBbmFseXNpc1J1bk1ldGFkYXRhElUKBmxhYmVscxgBIAMoCzJFLmdpdGh1Yi5jb20uYWt1aXR5LmthcmdvLmFwaS52MWFscGhhMS5BbmFseXNpc1J1bk1ldGFkYXRhLkxhYmVsc0VudHJ5El8KC2Fubm90YXRpb25zGAIgAygLMkouZ2l0aHViLmNvbS5ha3VpdHkua2FyZ28uYXBpLnYxYWxwaGExLkFuYWx5c2lzUnVuTWV0YWRhdGEuQW5ub3RhdGlvbnNFbnRyeRotCgtMYWJlbHNFbnRyeRILCgNrZXkYASABKAkSDQoFdmFsdWUYAiABKAk6AjgBGjIKEEFubm90YXRpb25zRW50cnkSCwoDa2V5GAEgASgJEg0KBXZhbHVlGAIgASgJOgI4ASJGChRBbmFseXNpc1J1blJlZmVyZW5jZRIRCgluYW1lc3BhY2UYASABKAkSDAoEbmFtZRgCIAEoCRINCgVwaGFzZRgDIAEoCSI3ChlBbmFseXNpc1RlbXBsYXRlUmVmZXJlbmNlEgwKBG5hbWUYASABKAkSDAoEa2luZBgCIAEoCSJPCg1BcHByb3ZlZFN0YWdlEj4KCmFwcHJvdmVkQXQYASABKAsyKi5rOHMuaW8uYXBpbWFjaGluZXJ5LnBrZy5hcGlzLm1ldGEudjEuVGltZSI4ChVBcmdvQ0RBcHBIZWFsdGhTdGF0dXMSDgoGc3RhdHVzGAEgASgJEg8KB21lc3NhZ2UYAiABKAki1AEKD0FyZ29DREFwcFN0YXR1cxIRCgluYW1lc3BhY2UYASABKAkSDAoEbmFtZRgCIAEoCRJRCgxoZWFsdGhTdGF0dXMYAyABKAsyOy5naXRodWIuY29tLmFrdWl0eS5rYXJnby5hcGkudjFhbHBoYTEuQXJnb0NEQXBwSGVhbHRoU3RhdHVzEk0KCnN5bmNTdGF0dXMYBCABKAsyOS5naXRodWIuY29tLmFrdWl0eS5rYXJnby5hcGkudjFhbHBoYTEuQXJnb0NEQXBwU3luY1N0YXR1cyJKChNBcmdvQ0RBcHBTeW5jU3RhdHVzEg4KBnN0YXR1cxgBIAEoCRIQCghyZXZpc2lvbhgCIAEoCRIRCglyZXZpc2lvbnMYAyADKAkiNwoFQ2hhcnQSDwoHcmVwb1VSTBgBIAEoCRIMCgRuYW1lGAIgASgJEg8KB3ZlcnNpb24YAyABKAkiYQoUQ2hhcnREaXNjb3ZlcnlSZXN1bHQSDwoHcmVwb1VSTBgBIAEoCRIMCgRuYW1lGAIgASgJEhgKEHNlbXZlckNvbnN0cmFpbnQYAyABKAkSEAoIdmVyc2lvbnMYBCADKAkiZAoRQ2hhcnRTdWJzY3JpcHRpb24SDwoHcmVwb1VSTBgBIAEoCRIMCgRuYW1lGAIgASgJEhgKEHNlbXZlckNvbnN0cmFpbnQYAyABKAkSFgoOZGlzY292ZXJ5TGltaXQYBCABKAUioQEKFENsdXN0ZXJQcm9tb3Rpb25UYXNrEkIKCG1ldGFkYXRhGAEgASgLMjAuazhzLmlvLmFwaW1hY2hpbmVyeS5wa2cuYXBpcy5tZXRhLnYxLk9iamVjdE1ldGESRQoEc3BlYxgCIAEoCzI3LmdpdGh1Yi5jb20uYWt1aXR5LmthcmdvLmFwaS52MWFscGhhMS5Qcm9tb3Rpb25UYXNrU3BlYyKnAQoYQ2x1c3RlclByb21vdGlvblRhc2tMaXN0EkAKCG1ldGFkYXRhGAEgASgLMi4uazhzLmlvLmFwaW1hY2hpbmVyeS5wa2cuYXBpcy5tZXRhLnYxLkxpc3RNZXRhEkkKBWl0ZW1zGAIgAygLMjouZ2l0aHViLmNvbS5ha3VpdHkua2FyZ28uYXBpLnYxYWxwaGExLkNsdXN0ZXJQcm9tb3Rpb25UYXNrIkkKDEN1cnJlbnRTdGFnZRI5CgVzaW5jZRgBIAEoCzIqLms4cy5pby5hcGltYWNoaW5lcnkucGtnLmFwaXMubWV0YS52MS5UaW1lIrYCChNEaXNjb3ZlcmVkQXJ0aWZhY3RzEkAKDGRpc2NvdmVyZWRBdBgEIAEoCzIqLms4cy5pby5hcGltYWNoaW5lcnkucGtnLmFwaXMubWV0YS52MS5UaW1lEkUKA2dpdBgBIAMoCzI4LmdpdGh1Yi5jb20uYWt1aXR5LmthcmdvLmFwaS52MWFscGhhMS5HaXREaXNjb3ZlcnlSZXN1bHQSSgoGaW1hZ2VzGAIgAygLMjouZ2l0aHViLmNvbS5ha3VpdHkua2FyZ28uYXBpLnYxYWxwaGExLkltYWdlRGlzY292ZXJ5UmVzdWx0EkoKBmNoYXJ0cxgDIAMoCzI6LmdpdGh1Yi5jb20uYWt1aXR5LmthcmdvLmFwaS52MWFscGhhMS5DaGFydERpc2NvdmVyeVJlc3VsdCKwAQoQRGlzY292ZXJlZENvbW1pdBIKCgJpZBgBIAEoCRIOCgZicmFuY2gYAiABKAkSCwoDdGFnGAMgASgJEg8KB3N1YmplY3QYBCABKAkSDgoGYXV0aG9yGAUgASgJEhEKCWNvbW1pdHRlchgGIAEoCRI/CgtjcmVhdG9yRGF0ZRgHIAEoCzIqLms4cy5pby5hcGltYWNoaW5lcnkucGtnLmFwaXMubWV0YS52MS5UaW1lIqQCChhEaXNjb3ZlcmVkSW1hZ2VSZWZlcmVuY2USCwoDdGFnGAEgASgJEg4KBmRpZ2VzdBgCIAEoCRJkCgthbm5vdGF0aW9ucxgFIAMoCzJPLmdpdGh1Yi5jb20uYWt1aXR5LmthcmdvLmFwaS52MWFscGhhMS5EaXNjb3ZlcmVkSW1hZ2VSZWZlcmVuY2UuQW5ub3RhdGlvbnNFbnRyeRISCgpnaXRSZXBvVVJMGAMgASgJEj0KCWNyZWF0ZWRBdBgEIAEoCzIqLms4cy5pby5hcGltYWNoaW5lcnkucGtnLmFwaXMubWV0YS52MS5UaW1lGjIKEEFubm90YXRpb25zRW50cnkSCwoDa2V5GAEgASgJEg0KBXZhbHVlGAIgASgJOgI4ASIxChJFeHByZXNzaW9uVmFyaWFibGUSDAoEbmFtZRgBIAEoCRINCgV2YWx1ZRgCIAEoCSKiAwoHRnJlaWdodBJCCghtZXRhZGF0YRgBIAEoCzIwLms4cy5pby5hcGltYWNoaW5lcnkucGtnLmFwaXMubWV0YS52MS5PYmplY3RNZXRhEg0KBWFsaWFzGAcgASgJEkMKBm9yaWdpbhgJIAEoCzIzLmdpdGh1Yi5jb20uYWt1aXR5LmthcmdvLmFwaS52MWFscGhhMS5GcmVpZ2h0T3JpZ2luEkAKB2NvbW1pdHMYAyADKAsyLy5naXRodWIuY29tLmFrdWl0eS5rYXJnby5hcGkudjFhbHBoYTEuR2l0Q29tbWl0EjsKBmltYWdlcxgEIAMoCzIrLmdpdGh1Yi5jb20uYWt1aXR5LmthcmdvLmFwaS52MWFscGhhMS5JbWFnZRI7CgZjaGFydHMYBSADKAsyKy5naXRodWIuY29tLmFrdWl0eS5rYXJnby5hcGkudjFhbHBoYTEuQ2hhcnQSQwoGc3RhdHVzGAYgASgLMjMuZ2l0aHViLmNvbS5ha3VpdHkua2FyZ28uYXBpLnYxYWxwaGExLkZyZWlnaHRTdGF0dXMirQIKEUZyZWlnaHRDb2xsZWN0aW9uEgoKAmlkGAMgASgJElEKBWl0ZW1zGAEgAygLMkIuZ2l0aHViLmNvbS5ha3VpdHkua2FyZ28uYXBpLnYxYWxwaGExLkZyZWlnaHRDb2xsZWN0aW9uLkl0ZW1zRW50cnkSUwoTdmVyaWZpY2F0aW9uSGlzdG9yeRgCIAMoCzI2LmdpdGh1Yi5jb20uYWt1aXR5LmthcmdvLmFwaS52MWFscGhhMS5WZXJpZmljYXRpb25JbmZvGmQKCkl0ZW1zRW50cnkSCwoDa2V5GAEgASgJEkUKBXZhbHVlGAIgASgLMjYuZ2l0aHViLmNvbS5ha3VpdHkua2FyZ28uYXBpLnYxYWxwaGExLkZyZWlnaHRSZWZlcmVuY2U6AjgBIo0BCgtGcmVpZ2h0TGlzdBJACghtZXRhZGF0YRgBIAEoCzIuLms4cy5pby5hcGltYWNoaW5lcnkucGtnLmFwaXMubWV0YS52MS5MaXN0TWV0YRI8CgVpdGVtcxgCIAMoCzItLmdpdGh1Yi5jb20uYWt1aXR5LmthcmdvLmFwaS52MWFscGhhMS5GcmVpZ2h0IisKDUZyZWlnaHRPcmlnaW4SDAoEa2luZBgBIAEoCRIMCgRuYW1lGAIgASgJIqECChBGcmVpZ2h0UmVmZXJlbmNlEgwKBG5hbWUYASABKAkSQwoGb3JpZ2luGAggASgLMjMuZ2l0aHViLmNvbS5ha3VpdHkua2FyZ28uYXBpLnYxYWxwaGExLkZyZWlnaHRPcmlnaW4SQAoHY29tbWl0cxgCIAMoCzIvLmdpdGh1Yi5jb20uYWt1aXR5LmthcmdvLmFwaS52MWFscGhhMS5HaXRDb21taXQSOwoGaW1hZ2VzGAMgAygLMisuZ2l0aHViLmNvbS5ha3VpdHkua2FyZ28uYXBpLnYxYWxwaGExLkltYWdlEjsKBmNoYXJ0cxgEIAMoCzIrLmdpdGh1Yi5jb20uYWt1aXR5LmthcmdvLmFwaS52MWFscGhhMS5DaGFydCKcAQoORnJlaWdodFJlcXVlc3QSQwoGb3JpZ2luGAEgASgLMjMuZ2l0aHViLmNvbS5ha3VpdHkua2FyZ28uYXBpLnYxYWxwaGExLkZyZWlnaHRPcmlnaW4SRQoHc291cmNlcxgCIAEoCzI0LmdpdGh1Yi5jb20uYWt1aXR5LmthcmdvLmFwaS52MWFscGhhMS5GcmVpZ2h0U291cmNlcyKYAQoORnJlaWdodFNvdXJjZXMSDgoGZGlyZWN0GAEgASgIEg4KBnN0YWdlcxgCIAMoCRJIChByZXF1aXJlZFNvYWtUaW1lGAMgASgLMi4uazhzLmlvLmFwaW1hY2hpbmVyeS5wa2cuYXBpcy5tZXRhLnYxLkR1cmF0aW9uEhwKFGF2YWlsYWJpbGl0eVN0cmF0ZWd5GAQgASgJItcECg1GcmVpZ2h0U3RhdHVzElkKC2N1cnJlbnRseUluGAMgAygLMkQuZ2l0aHViLmNvbS5ha3VpdHkua2FyZ28uYXBpLnYxYWxwaGExLkZyZWlnaHRTdGF0dXMuQ3VycmVudGx5SW5FbnRyeRJXCgp2ZXJpZmllZEluGAEgAygLMkMuZ2l0aHViLmNvbS5ha3VpdHkua2FyZ28uYXBpLnYxYWxwaGExLkZyZWlnaHRTdGF0dXMuVmVyaWZpZWRJbkVudHJ5ElkKC2FwcHJvdmVkRm9yGAIgAygLMkQuZ2l0aHViLmNvbS5ha3VpdHkua2FyZ28uYXBpLnYxYWxwaGExLkZyZWlnaHRTdGF0dXMuQXBwcm92ZWRGb3JFbnRyeRpmChBDdXJyZW50bHlJbkVudHJ5EgsKA2tleRgBIAEoCRJBCgV2YWx1ZRgCIAEoCzIyLmdpdGh1Yi5jb20uYWt1aXR5LmthcmdvLmFwaS52MWFscGhhMS5DdXJyZW50U3RhZ2U6AjgBGmYKD1ZlcmlmaWVkSW5FbnRyeRILCgNrZXkYASABKAkSQgoFdmFsdWUYAiABKAsyMy5naXRodWIuY29tLmFrdWl0eS5rYXJnby5hcGkudjFhbHBoYTEuVmVyaWZpZWRTdGFnZToCOAEaZwoQQXBwcm92ZWRGb3JFbnRyeRILCgNrZXkYASABKAkSQgoFdmFsdWUYAiABKAsyMy5naXRodWIuY29tLmFrdWl0eS5rYXJnby5hcGkudjFhbHBoYTEuQXBwcm92ZWRTdGFnZToCOAEieQoJR2l0Q29tbWl0Eg8KB3JlcG9VUkwYASABKAkSCgoCaWQYAiABKAkSDgoGYnJhbmNoGAMgASgJEgsKA3RhZxgEIAEoCRIPCgdtZXNzYWdlGAYgASgJEg4KBmF1dGhvchgHIAEoCRIRCgljb21taXR0ZXIYCCABKAkibgoSR2l0RGlzY292ZXJ5UmVzdWx0Eg8KB3JlcG9VUkwYASABKAkSRwoHY29tbWl0cxgCIAMoCzI2LmdpdGh1Yi5jb20uYWt1aXR5LmthcmdvLmFwaS52MWFscGhhMS5EaXNjb3ZlcmVkQ29tbWl0Io4CCg9HaXRTdWJzY3JpcHRpb24SDwoHcmVwb1VSTBgBIAEoCRIfChdjb21taXRTZWxlY3Rpb25TdHJhdGVneRgCIAEoCRIOCgZicmFuY2gYAyABKAkSFQoNc3RyaWN0U2VtdmVycxgLIAEoCBIYChBzZW12ZXJDb25zdHJhaW50GAQgASgJEhEKCWFsbG93VGFncxgFIAEoCRISCgppZ25vcmVUYWdzGAYgAygJEh0KFWluc2VjdXJlU2tpcFRMU1ZlcmlmeRgHIAEoCBIUCgxpbmNsdWRlUGF0aHMYCCADKAkSFAoMZXhjbHVkZVBhdGhzGAkgAygJEhYKDmRpc2NvdmVyeUxpbWl0GAogASgFIsgBCgZIZWFsdGgSDgoGc3RhdHVzGAEgASgJEg4KBmlzc3VlcxgCIAMoCRJOCgZjb25maWcYBCABKAsyPi5rOHMuaW8uYXBpZXh0ZW5zaW9uc19hcGlzZXJ2ZXIucGtnLmFwaXMuYXBpZXh0ZW5zaW9ucy52MS5KU09OEk4KBm91dHB1dBgFIAEoCzI+Lms4cy5pby5hcGlleHRlbnNpb25zX2FwaXNlcnZlci5wa2cuYXBpcy5hcGlleHRlbnNpb25zLnYxLkpTT04ibwoPSGVhbHRoQ2hlY2tTdGVwEgwKBHVzZXMYASABKAkSTgoGY29uZmlnGAIgASgLMj4uazhzLmlvLmFwaWV4dGVuc2lvbnNfYXBpc2VydmVyLnBrZy5hcGlzLmFwaWV4dGVuc2lvbnMudjEuSlNPTiLQAQoFSW1hZ2USDwoHcmVwb1VSTBgBIAEoCRISCgpnaXRSZXBvVVJMGAIgASgJEgsKA3RhZxgDIAEoCRIOCgZkaWdlc3QYBCABKAkSUQoLYW5ub3RhdGlvbnMYBSADKAsyPC5naXRodWIuY29tLmFrdWl0eS5rYXJnby5hcGkudjFhbHBoYTEuSW1hZ2UuQW5ub3RhdGlvbnNFbnRyeRoyChBBbm5vdGF0aW9uc0VudHJ5EgsKA2tleRgBIAEoCRINCgV2YWx1ZRgCIAEoCToCOAEijQEKFEltYWdlRGlzY292ZXJ5UmVzdWx0Eg8KB3JlcG9VUkwYASABKAkSEAoIcGxhdGZvcm0YAiABKAkSUgoKcmVmZXJlbmNlcxgDIAMoCzI+LmdpdGh1Yi5jb20uYWt1aXR5LmthcmdvLmFwaS52MWFscGhhMS5EaXNjb3ZlcmVkSW1hZ2VSZWZlcmVuY2Ui+QEKEUltYWdlU3Vic2NyaXB0aW9uEg8KB3JlcG9VUkwYASABKAkSEgoKZ2l0UmVwb1VSTBgCIAEoCRIeChZpbWFnZVNlbGVjdGlvblN0cmF0ZWd5GAMgASgJEhUKDXN0cmljdFNlbXZlcnMYCiABKAgSGAoQc2VtdmVyQ29uc3RyYWludBgEIAEoCRIRCglhbGxvd1RhZ3MYBSABKAkSEgoKaWdub3JlVGFncxgGIAMoCRIQCghwbGF0Zm9ybRgHIAEoCRIdChVpbnNlY3VyZVNraXBUTFNWZXJpZnkYCCABKAgSFgoOZGlzY292ZXJ5TGltaXQYCSABKAUi0wEKB1Byb2plY3QSQgoIbWV0YWRhdGEYASABKAsyMC5rOHMuaW8uYXBpbWFjaGluZXJ5LnBrZy5hcGlzLm1ldGEudjEuT2JqZWN0TWV0YRI/CgRzcGVjGAIgASgLMjEuZ2l0aHViLmNvbS5ha3VpdHkua2FyZ28uYXBpLnYxYWxwaGExLlByb2plY3RTcGVjEkMKBnN0YXR1cxgDIAEoCzIzLmdpdGh1Yi5jb20uYWt1aXR5LmthcmdvLmFwaS52MWFscGhhMS5Qcm9qZWN0U3RhdHVzIo0BCgtQcm9qZWN0TGlzdBJACghtZXRhZGF0YRgBIAEoCzIuLms4cy5pby5hcGltYWNoaW5lcnkucGtnLmFwaXMubWV0YS52MS5MaXN0TWV0YRI8CgVpdGVtcxgCIAMoCzItLmdpdGh1Yi5jb20uYWt1aXR5LmthcmdvLmFwaS52MWFscGhhMS5Qcm9qZWN0Il8KC1Byb2plY3RTcGVjElAKEXByb21vdGlvblBvbGljaWVzGAEgAygLMjUuZ2l0aHViLmNvbS5ha3VpdHkua2FyZ28uYXBpLnYxYWxwaGExLlByb21vdGlvblBvbGljeSJ0Cg1Qcm9qZWN0U3RhdHVzEkMKCmNvbmRpdGlvbnMYAyADKAsyLy5rOHMuaW8uYXBpbWFjaGluZXJ5LnBrZy5hcGlzLm1ldGEudjEuQ29uZGl0aW9uEg0KBXBoYXNlGAEgASgJEg8KB21lc3NhZ2UYAiABKAki2QEKCVByb21vdGlvbhJCCghtZXRhZGF0YRgBIAEoCzIwLms4cy5pby5hcGltYWNoaW5lcnkucGtnLmFwaXMubWV0YS52MS5PYmplY3RNZXRhEkEKBHNwZWMYAiABKAsyMy5naXRodWIuY29tLmFrdWl0eS5rYXJnby5hcGkudjFhbHBoYTEuUHJvbW90aW9uU3BlYxJFCgZzdGF0dXMYAyABKAsyNS5naXRodWIuY29tLmFrdWl0eS5rYXJnby5hcGkudjFhbHBoYTEuUHJvbW90aW9uU3RhdHVzIpEBCg1Qcm9tb3Rpb25MaXN0EkAKCG1ldGFkYXRhGAEgASgLMi4uazhzLmlvLmFwaW1hY2hpbmVyeS5wa2cuYXBpcy5tZXRhLnYxLkxpc3RNZXRhEj4KBWl0ZW1zGAIgAygLMi8uZ2l0aHViLmNvbS5ha3VpdHkua2FyZ28uYXBpLnYxYWxwaGExLlByb21vdGlvbiI+Cg9Qcm9tb3Rpb25Qb2xpY3kSDQoFc3RhZ2UYASABKAkSHAoUYXV0b1Byb21vdGlvbkVuYWJsZWQYAiABKAgi8gEKElByb21vdGlvblJlZmVyZW5jZRIMCgRuYW1lGAEgASgJEkcKB2ZyZWlnaHQYAiABKAsyNi5naXRodWIuY29tLmFrdWl0eS5rYXJnby5hcGkudjFhbHBoYTEuRnJlaWdodFJlZmVyZW5jZRJFCgZzdGF0dXMYAyABKAsyNS5naXRodWIuY29tLmFrdWl0eS5rYXJnby5hcGkudjFhbHBoYTEuUHJvbW90aW9uU3RhdHVzEj4KCmZpbmlzaGVkQXQYBCABKAsyKi5rOHMuaW8uYXBpbWFjaGluZXJ5LnBrZy5hcGlzLm1ldGEudjEuVGltZSK7AQoNUHJvbW90aW9uU3BlYxINCgVzdGFnZRgBIAEoCRIPCgdmcmVpZ2h0GAIgASgJEkYKBHZhcnMYBCADKAsyOC5naXRodWIuY29tLmFrdWl0eS5rYXJnby5hcGkudjFhbHBoYTEuRXhwcmVzc2lvblZhcmlhYmxlEkIKBXN0ZXBzGAMgAygLMjMuZ2l0aHViLmNvbS5ha3VpdHkua2FyZ28uYXBpLnYxYWxwaGExLlByb21vdGlvblN0ZXAitwQKD1Byb21vdGlvblN0YXR1cxIaChJsYXN0SGFuZGxlZFJlZnJlc2gYBCABKAkSDQoFcGhhc2UYASABKAkSDwoHbWVzc2FnZRgCIAEoCRJHCgdmcmVpZ2h0GAUgASgLMjYuZ2l0aHViLmNvbS5ha3VpdHkua2FyZ28uYXBpLnYxYWxwaGExLkZyZWlnaHRSZWZlcmVuY2USUgoRZnJlaWdodENvbGxlY3Rpb24YByABKAsyNy5naXRodWIuY29tLmFrdWl0eS5rYXJnby5hcGkudjFhbHBoYTEuRnJlaWdodENvbGxlY3Rpb24SSwoMaGVhbHRoQ2hlY2tzGAggAygLMjUuZ2l0aHViLmNvbS5ha3VpdHkua2FyZ28uYXBpLnYxYWxwaGExLkhlYWx0aENoZWNrU3RlcBI+CgpmaW5pc2hlZEF0GAYgASgLMiouazhzLmlvLmFwaW1hY2hpbmVyeS5wa2cuYXBpcy5tZXRhLnYxLlRpbWUSEwoLY3VycmVudFN0ZXAYCSABKAMSWgoVc3RlcEV4ZWN1dGlvbk1ldGFkYXRhGAsgAygLMjsuZ2l0aHViLmNvbS5ha3VpdHkua2FyZ28uYXBpLnYxYWxwaGExLlN0ZXBFeGVjdXRpb25NZXRhZGF0YRJNCgVzdGF0ZRgKIAEoCzI+Lms4cy5pby5hcGlleHRlbnNpb25zX2FwaXNlcnZlci5wa2cuYXBpcy5hcGlleHRlbnNpb25zLnYxLkpTT04i4gIKDVByb21vdGlvblN0ZXASDAoEdXNlcxgBIAEoCRJKCgR0YXNrGAUgASgLMjwuZ2l0aHViLmNvbS5ha3VpdHkua2FyZ28uYXBpLnYxYWxwaGExLlByb21vdGlvblRhc2tSZWZlcmVuY2USCgoCYXMYAiABKAkSCgoCaWYYByABKAkSRwoFcmV0cnkYBCABKAsyOC5naXRodWIuY29tLmFrdWl0eS5rYXJnby5hcGkudjFhbHBoYTEuUHJvbW90aW9uU3RlcFJldHJ5EkYKBHZhcnMYBiADKAsyOC5naXRodWIuY29tLmFrdWl0eS5rYXJnby5hcGkudjFhbHBoYTEuRXhwcmVzc2lvblZhcmlhYmxlEk4KBmNvbmZpZxgDIAEoCzI+Lms4cy5pby5hcGlleHRlbnNpb25zX2FwaXNlcnZlci5wa2cuYXBpcy5hcGlleHRlbnNpb25zLnYxLkpTT04ibQoSUHJvbW90aW9uU3RlcFJldHJ5Ej8KB3RpbWVvdXQYASABKAsyLi5rOHMuaW8uYXBpbWFjaGluZXJ5LnBrZy5hcGlzLm1ldGEudjEuRHVyYXRpb24SFgoOZXJyb3JUaHJlc2hvbGQYAiABKA0imgEKDVByb21vdGlvblRhc2sSQgoIbWV0YWRhdGEYASABKAsyMC5rOHMuaW8uYXBpbWFjaGluZXJ5LnBrZy5hcGlzLm1ldGEudjEuT2JqZWN0TWV0YRJFCgRzcGVjGAIgASgLMjcuZ2l0aHViLmNvbS5ha3VpdHkua2FyZ28uYXBpLnYxYWxwaGExLlByb21vdGlvblRhc2tTcGVjIpkBChFQcm9tb3Rpb25UYXNrTGlzdBJACghtZXRhZGF0YRgBIAEoCzIuLms4cy5pby5hcGltYWNoaW5lcnkucGtnLmFwaXMubWV0YS52MS5MaXN0TWV0YRJCCgVpdGVtcxgCIAMoCzIzLmdpdGh1Yi5jb20uYWt1aXR5LmthcmdvLmFwaS52MWFscGhhMS5Qcm9tb3Rpb25UYXNrIjQKFlByb21vdGlvblRhc2tSZWZlcmVuY2USDAoEbmFtZRgBIAEoCRIMCgRraW5kGAIgASgJIp8BChFQcm9tb3Rpb25UYXNrU3BlYxJGCgR2YXJzGAEgAygLMjguZ2l0aHViLmNvbS5ha3VpdHkua2FyZ28uYXBpLnYxYWxwaGExLkV4cHJlc3Npb25WYXJpYWJsZRJCCgVzdGVwcxgCIAMoCzIzLmdpdGh1Yi5jb20uYWt1aXR5LmthcmdvLmFwaS52MWFscGhhMS5Qcm9tb3Rpb25TdGVwIl4KEVByb21vdGlvblRlbXBsYXRlEkkKBHNwZWMYASABKAsyOy5naXRodWIuY29tLmFrdWl0eS5rYXJnby5hcGkudjFhbHBoYTEuUHJvbW90aW9uVGVtcGxhdGVTcGVjIqMBChVQcm9tb3Rpb25UZW1wbGF0ZVNwZWMSRgoEdmFycxgCIAMoCzI4LmdpdGh1Yi5jb20uYWt1aXR5LmthcmdvLmFwaS52MWFscGhhMS5FeHByZXNzaW9uVmFyaWFibGUSQgoFc3RlcHMYASADKAsyMy5naXRodWIuY29tLmFrdWl0eS5rYXJnby5hcGkudjFhbHBoYTEuUHJvbW90aW9uU3RlcCLmAQoQUmVwb1N1YnNjcmlwdGlvbhJCCgNnaXQYASABKAsyNS5naXRodWIuY29tLmFrdWl0eS5rYXJnby5hcGkudjFhbHBoYTEuR2l0U3Vic2NyaXB0aW9uEkYKBWltYWdlGAIgASgLMjcuZ2l0aHViLmNvbS5ha3VpdHkua2FyZ28uYXBpLnYxYWxwaGExLkltYWdlU3Vic2NyaXB0aW9uEkYKBWNoYXJ0GAMgASgLMjcuZ2l0aHViLmNvbS5ha3VpdHkua2FyZ28uYXBpLnYxYWxwaGExLkNoYXJ0U3Vic2NyaXB0aW9uIs0BCgVTdGFnZRJCCghtZXRhZGF0YRgBIAEoCzIwLms4cy5pby5hcGltYWNoaW5lcnkucGtnLmFwaXMubWV0YS52MS5PYmplY3RNZXRhEj0KBHNwZWMYAiABKAsyLy5naXRodWIuY29tLmFrdWl0eS5rYXJnby5hcGkudjFhbHBoYTEuU3RhZ2VTcGVjEkEKBnN0YXR1cxgDIAEoCzIxLmdpdGh1Yi5jb20uYWt1aXR5LmthcmdvLmFwaS52MWFscGhhMS5TdGFnZVN0YXR1cyKJAQoJU3RhZ2VMaXN0EkAKCG1ldGFkYXRhGAEgASgLMi4uazhzLmlvLmFwaW1hY2hpbmVyeS5wa2cuYXBpcy5tZXRhLnYxLkxpc3RNZXRhEjoKBWl0ZW1zGAIgAygLMisuZ2l0aHViLmNvbS5ha3VpdHkua2FyZ28uYXBpLnYxYWxwaGExLlN0YWdlItACCglTdGFnZVNwZWMSDQoFc2hhcmQYBCABKAkSRgoEdmFycxgHIAMoCzI4LmdpdGh1Yi5jb20uYWt1aXR5LmthcmdvLmFwaS52MWFscGhhMS5FeHByZXNzaW9uVmFyaWFibGUSTgoQcmVxdWVzdGVkRnJlaWdodBgFIAMoCzI0LmdpdGh1Yi5jb20uYWt1aXR5LmthcmdvLmFwaS52MWFscGhhMS5GcmVpZ2h0UmVxdWVzdBJSChFwcm9tb3Rpb25UZW1wbGF0ZRgGIAEoCzI3LmdpdGh1Yi5jb20uYWt1aXR5LmthcmdvLmFwaS52MWFscGhhMS5Qcm9tb3Rpb25UZW1wbGF0ZRJICgx2ZXJpZmljYXRpb24YAyABKAsyMi5naXRodWIuY29tLmFrdWl0eS5rYXJnby5hcGkudjFhbHBoYTEuVmVyaWZpY2F0aW9uIvYDCgtTdGFnZVN0YXR1cxJDCgpjb25kaXRpb25zGA0gAygLMi8uazhzLmlvLmFwaW1hY2hpbmVyeS5wa2cuYXBpcy5tZXRhLnYxLkNvbmRpdGlvbhIaChJsYXN0SGFuZGxlZFJlZnJlc2gYCyABKAkSDQoFcGhhc2UYASABKAkSTwoOZnJlaWdodEhpc3RvcnkYBCADKAsyNy5naXRodWIuY29tLmFrdWl0eS5rYXJnby5hcGkudjFhbHBoYTEuRnJlaWdodENvbGxlY3Rpb24SFgoOZnJlaWdodFN1bW1hcnkYDCABKAkSPAoGaGVhbHRoGAggASgLMiwuZ2l0aHViLmNvbS5ha3VpdHkua2FyZ28uYXBpLnYxYWxwaGExLkhlYWx0aBIPCgdtZXNzYWdlGAkgASgJEhoKEm9ic2VydmVkR2VuZXJhdGlvbhgGIAEoAxJSChBjdXJyZW50UHJvbW90aW9uGAcgASgLMjguZ2l0aHViLmNvbS5ha3VpdHkua2FyZ28uYXBpLnYxYWxwaGExLlByb21vdGlvblJlZmVyZW5jZRJPCg1sYXN0UHJvbW90aW9uGAogASgLMjguZ2l0aHViLmNvbS5ha3VpdHkua2FyZ28uYXBpLnYxYWxwaGExLlByb21vdGlvblJlZmVyZW5jZSLaAQoVU3RlcEV4ZWN1dGlvbk1ldGFkYXRhEg0KBWFsaWFzGAEgASgJEj0KCXN0YXJ0ZWRBdBgCIAEoCzIqLms4cy5pby5hcGltYWNoaW5lcnkucGtnLmFwaXMubWV0YS52MS5UaW1lEj4KCmZpbmlzaGVkQXQYAyABKAsyKi5rOHMuaW8uYXBpbWFjaGluZXJ5LnBrZy5hcGlzLm1ldGEudjEuVGltZRISCgplcnJvckNvdW50GAQgASgNEg4KBnN0YXR1cxgFIAEoCRIPCgdtZXNzYWdlGAYgASgJIosCCgxWZXJpZmljYXRpb24SWgoRYW5hbHlzaXNUZW1wbGF0ZXMYASADKAsyPy5naXRodWIuY29tLmFrdWl0eS5rYXJnby5hcGkudjFhbHBoYTEuQW5hbHlzaXNUZW1wbGF0ZVJlZmVyZW5jZRJWChNhbmFseXNpc1J1bk1ldGFkYXRhGAIgASgLMjkuZ2l0aHViLmNvbS5ha3VpdHkua2FyZ28uYXBpLnYxYWxwaGExLkFuYWx5c2lzUnVuTWV0YWRhdGESRwoEYXJncxgDIAMoCzI5LmdpdGh1Yi5jb20uYWt1aXR5LmthcmdvLmFwaS52MWFscGhhMS5BbmFseXNpc1J1bkFyZ3VtZW50Ip0CChBWZXJpZmljYXRpb25JbmZvEgoKAmlkGAQgASgJEg0KBWFjdG9yGAcgASgJEj0KCXN0YXJ0VGltZRgFIAEoCzIqLms4cy5pby5hcGltYWNoaW5lcnkucGtnLmFwaXMubWV0YS52MS5UaW1lEg0KBXBoYXNlGAEgASgJEg8KB21lc3NhZ2UYAiABKAkSTwoLYW5hbHlzaXNSdW4YAyABKAsyOi5naXRodWIuY29tLmFrdWl0eS5rYXJnby5hcGkudjFhbHBoYTEuQW5hbHlzaXNSdW5SZWZlcmVuY2USPgoKZmluaXNoVGltZRgGIAEoCzIqLms4cy5pby5hcGltYWNoaW5lcnkucGtnLmFwaXMubWV0YS52MS5UaW1lIpQBCg1WZXJpZmllZFN0YWdlEj4KCnZlcmlmaWVkQXQYASABKAsyKi5rOHMuaW8uYXBpbWFjaGluZXJ5LnBrZy5hcGlzLm1ldGEudjEuVGltZRJDCgtsb25nZXN0U29haxgCIAEoCzIuLms4cy5pby5hcGltYWNoaW5lcnkucGtnLmFwaXMubWV0YS52MS5EdXJhdGlvbiLZAQoJV2FyZWhvdXNlEkIKCG1ldGFkYXRhGAEgASgLMjAuazhzLmlvLmFwaW1hY2hpbmVyeS5wa2cuYXBpcy5tZXRhLnYxLk9iamVjdE1ldGESQQoEc3BlYxgCIAEoCzIzLmdpdGh1Yi5jb20uYWt1aXR5LmthcmdvLmFwaS52MWFscGhhMS5XYXJlaG91c2VTcGVjEkUKBnN0YXR1cxgDIAEoCzI1LmdpdGh1Yi5jb20uYWt1aXR5LmthcmdvLmFwaS52MWFscGhhMS5XYXJlaG91c2VTdGF0dXMikQEKDVdhcmVob3VzZUxpc3QSQAoIbWV0YWRhdGEYASABKAsyLi5rOHMuaW8uYXBpbWFjaGluZXJ5LnBrZy5hcGlzLm1ldGEudjEuTGlzdE1ldGESPgoFaXRlbXMYAiADKAsyLy5naXRodWIuY29tLmFrdWl0eS5rYXJnby5hcGkudjFhbHBoYTEuV2FyZWhvdXNlIs4BCg1XYXJlaG91c2VTcGVjEg0KBXNoYXJkGAIgASgJEkAKCGludGVydmFsGAQgASgLMi4uazhzLmlvLmFwaW1hY2hpbmVyeS5wa2cuYXBpcy5tZXRhLnYxLkR1cmF0aW9uEh0KFWZyZWlnaHRDcmVhdGlvblBvbGljeRgDIAEoCRJNCg1zdWJzY3JpcHRpb25zGAEgAygLMjYuZ2l0aHViLmNvbS5ha3VpdHkua2FyZ28uYXBpLnYxYWxwaGExLlJlcG9TdWJzY3JpcHRpb24i/QEKD1dhcmVob3VzZVN0YXR1cxJDCgpjb25kaXRpb25zGAkgAygLMi8uazhzLmlvLmFwaW1hY2hpbmVyeS5wa2cuYXBpcy5tZXRhLnYxLkNvbmRpdGlvbhIaChJsYXN0SGFuZGxlZFJlZnJlc2gYBiABKAkSGgoSb2JzZXJ2ZWRHZW5lcmF0aW9uGAQgASgDEhUKDWxhc3RGcmVpZ2h0SUQYCCABKAkSVgoTZGlzY292ZXJlZEFydGlmYWN0cxgHIAEoCzI5LmdpdGh1Yi5jb20uYWt1aXR5LmthcmdvLmFwaS52MWFscGhhMS5EaXNjb3ZlcmVkQXJ0aWZhY3RzQpcCCihjb20uZ2l0aHViLmNvbS5ha3VpdHkua2FyZ28uYXBpLnYxYWxwaGExQg5HZW5lcmF0ZWRQcm90b1ABWiRnaXRodWIuY29tL2FrdWl0eS9rYXJnby9hcGkvdjFhbHBoYTGiAgVHQ0FLQaoCJEdpdGh1Yi5Db20uQWt1aXR5LkthcmdvLkFwaS5WMWFscGhhMcoCJEdpdGh1YlxDb21cQWt1aXR5XEthcmdvXEFwaVxWMWFscGhhMeICMEdpdGh1YlxDb21cQWt1aXR5XEthcmdvXEFwaVxWMWFscGhhMVxHUEJNZXRhZGF0YeoCKUdpdGh1Yjo6Q29tOjpBa3VpdHk6OkthcmdvOjpBcGk6OlYxYWxwaGEx", [file_k8s_io_apiextensions_apiserver_pkg_apis_apiextensions_v1_generated, file_k8s_io_apimachinery_pkg_apis_meta_v1_generated, file_k8s_io_apimachinery_pkg_runtime_generated, file_k8s_io_apimachinery_pkg_runtime_schema_generated]);
 
 /**
  * AnalysisRunArgument represents an argument to be added to an AnalysisRun.
@@ -691,6 +691,44 @@ export type DiscoveredImageReference = Message<"github.com.akuity.kargo.api.v1al
 export const DiscoveredImageReferenceSchema: GenMessage<DiscoveredImageReference> = /*@__PURE__*/
   messageDesc(file_api_v1alpha1_generated, 16);
 
+/**
+ * ExpressionVariable describes a single variable that may be referenced by
+ * expressions in the context of a ClusterPromotionTask, PromotionTask,
+ * Promotion, AnalysisRun arguments, or other objects that support expressions.
+ *
+ * It is used to pass information to the expression evaluation engine, and to
+ * allow for dynamic evaluation of expressions based on the variable values.
+ *
+ * @generated from message github.com.akuity.kargo.api.v1alpha1.ExpressionVariable
+ */
+export type ExpressionVariable = Message<"github.com.akuity.kargo.api.v1alpha1.ExpressionVariable"> & {
+  /**
+   * Name is the name of the variable.
+   *
+   * +kubebuilder:validation:MinLength=1
+   * +kubebuilder:validation:Pattern=^[a-zA-Z_]\w*$
+   *
+   * @generated from field: optional string name = 1;
+   */
+  name: string;
+
+  /**
+   * Value is the value of the variable. It is allowed to utilize expressions
+   * in the value.
+   * See https://docs.kargo.io/user-guide/reference-docs/expressions for details.
+   *
+   * @generated from field: optional string value = 2;
+   */
+  value: string;
+};
+
+/**
+ * Describes the message github.com.akuity.kargo.api.v1alpha1.ExpressionVariable.
+ * Use `create(ExpressionVariableSchema)` to create a new message.
+ */
+export const ExpressionVariableSchema: GenMessage<ExpressionVariable> = /*@__PURE__*/
+  messageDesc(file_api_v1alpha1_generated, 17);
+
 /**
  * Freight represents a collection of versioned artifacts.
  *
@@ -760,7 +798,7 @@ export type Freight = Message<"github.com.akuity.kargo.api.v1alpha1.Freight"> &
  * Use `create(FreightSchema)` to create a new message.
  */
 export const FreightSchema: GenMessage<Freight> = /*@__PURE__*/
-  messageDesc(file_api_v1alpha1_generated, 17);
+  messageDesc(file_api_v1alpha1_generated, 18);
 
 /**
  * FreightCollection is a collection of FreightReferences, each of which
@@ -800,7 +838,7 @@ export type FreightCollection = Message<"github.com.akuity.kargo.api.v1alpha1.Fr
  * Use `create(FreightCollectionSchema)` to create a new message.
  */
 export const FreightCollectionSchema: GenMessage<FreightCollection> = /*@__PURE__*/
-  messageDesc(file_api_v1alpha1_generated, 18);
+  messageDesc(file_api_v1alpha1_generated, 19);
 
 /**
  * FreightList is a list of Freight resources.
@@ -824,7 +862,7 @@ export type FreightList = Message<"github.com.akuity.kargo.api.v1alpha1.FreightL
  * Use `create(FreightListSchema)` to create a new message.
  */
 export const FreightListSchema: GenMessage<FreightList> = /*@__PURE__*/
-  messageDesc(file_api_v1alpha1_generated, 19);
+  messageDesc(file_api_v1alpha1_generated, 20);
 
 /**
  * FreightOrigin describes a kind of Freight in terms of where it may have
@@ -861,7 +899,7 @@ export type FreightOrigin = Message<"github.com.akuity.kargo.api.v1alpha1.Freigh
  * Use `create(FreightOriginSchema)` to create a new message.
  */
 export const FreightOriginSchema: GenMessage<FreightOrigin> = /*@__PURE__*/
-  messageDesc(file_api_v1alpha1_generated, 20);
+  messageDesc(file_api_v1alpha1_generated, 21);
 
 /**
  * FreightReference is a simplified representation of a piece of Freight -- not
@@ -913,7 +951,7 @@ export type FreightReference = Message<"github.com.akuity.kargo.api.v1alpha1.Fre
  * Use `create(FreightReferenceSchema)` to create a new message.
  */
 export const FreightReferenceSchema: GenMessage<FreightReference> = /*@__PURE__*/
-  messageDesc(file_api_v1alpha1_generated, 21);
+  messageDesc(file_api_v1alpha1_generated, 22);
 
 /**
  * FreightRequest expresses a Stage's need for Freight having originated from a
@@ -946,7 +984,7 @@ export type FreightRequest = Message<"github.com.akuity.kargo.api.v1alpha1.Freig
  * Use `create(FreightRequestSchema)` to create a new message.
  */
 export const FreightRequestSchema: GenMessage<FreightRequest> = /*@__PURE__*/
-  messageDesc(file_api_v1alpha1_generated, 22);
+  messageDesc(file_api_v1alpha1_generated, 23);
 
 /**
  * @generated from message github.com.akuity.kargo.api.v1alpha1.FreightSources
@@ -1006,7 +1044,7 @@ export type FreightSources = Message<"github.com.akuity.kargo.api.v1alpha1.Freig
  * Use `create(FreightSourcesSchema)` to create a new message.
  */
 export const FreightSourcesSchema: GenMessage<FreightSources> = /*@__PURE__*/
-  messageDesc(file_api_v1alpha1_generated, 23);
+  messageDesc(file_api_v1alpha1_generated, 24);
 
 /**
  * FreightStatus describes a piece of Freight's most recently observed state.
@@ -1045,7 +1083,7 @@ export type FreightStatus = Message<"github.com.akuity.kargo.api.v1alpha1.Freigh
  * Use `create(FreightStatusSchema)` to create a new message.
  */
 export const FreightStatusSchema: GenMessage<FreightStatus> = /*@__PURE__*/
-  messageDesc(file_api_v1alpha1_generated, 24);
+  messageDesc(file_api_v1alpha1_generated, 25);
 
 /**
  * GitCommit describes a specific commit from a specific Git repository.
@@ -1111,7 +1149,7 @@ export type GitCommit = Message<"github.com.akuity.kargo.api.v1alpha1.GitCommit"
  * Use `create(GitCommitSchema)` to create a new message.
  */
 export const GitCommitSchema: GenMessage<GitCommit> = /*@__PURE__*/
-  messageDesc(file_api_v1alpha1_generated, 25);
+  messageDesc(file_api_v1alpha1_generated, 26);
 
 /**
  * GitDiscoveryResult represents the result of a Git discovery operation for a
@@ -1147,7 +1185,7 @@ export type GitDiscoveryResult = Message<"github.com.akuity.kargo.api.v1alpha1.G
  * Use `create(GitDiscoveryResultSchema)` to create a new message.
  */
 export const GitDiscoveryResultSchema: GenMessage<GitDiscoveryResult> = /*@__PURE__*/
-  messageDesc(file_api_v1alpha1_generated, 26);
+  messageDesc(file_api_v1alpha1_generated, 27);
 
 /**
  * GitSubscription defines a subscription to a Git repository.
@@ -1318,7 +1356,7 @@ export type GitSubscription = Message<"github.com.akuity.kargo.api.v1alpha1.GitS
  * Use `create(GitSubscriptionSchema)` to create a new message.
  */
 export const GitSubscriptionSchema: GenMessage<GitSubscription> = /*@__PURE__*/
-  messageDesc(file_api_v1alpha1_generated, 27);
+  messageDesc(file_api_v1alpha1_generated, 28);
 
 /**
  * Health describes the health of a Stage.
@@ -1362,7 +1400,7 @@ export type Health = Message<"github.com.akuity.kargo.api.v1alpha1.Health"> & {
  * Use `create(HealthSchema)` to create a new message.
  */
 export const HealthSchema: GenMessage<Health> = /*@__PURE__*/
-  messageDesc(file_api_v1alpha1_generated, 28);
+  messageDesc(file_api_v1alpha1_generated, 29);
 
 /**
  * HealthCheckStep describes a health check directive which can be executed by
@@ -1393,7 +1431,7 @@ export type HealthCheckStep = Message<"github.com.akuity.kargo.api.v1alpha1.Heal
  * Use `create(HealthCheckStepSchema)` to create a new message.
  */
 export const HealthCheckStepSchema: GenMessage<HealthCheckStep> = /*@__PURE__*/
-  messageDesc(file_api_v1alpha1_generated, 29);
+  messageDesc(file_api_v1alpha1_generated, 30);
 
 /**
  * Image describes a specific version of a container image.
@@ -1446,7 +1484,7 @@ export type Image = Message<"github.com.akuity.kargo.api.v1alpha1.Image"> & {
  * Use `create(ImageSchema)` to create a new message.
  */
 export const ImageSchema: GenMessage<Image> = /*@__PURE__*/
-  messageDesc(file_api_v1alpha1_generated, 30);
+  messageDesc(file_api_v1alpha1_generated, 31);
 
 /**
  * ImageDiscoveryResult represents the result of an image discovery operation
@@ -1492,7 +1530,7 @@ export type ImageDiscoveryResult = Message<"github.com.akuity.kargo.api.v1alpha1
  * Use `create(ImageDiscoveryResultSchema)` to create a new message.
  */
 export const ImageDiscoveryResultSchema: GenMessage<ImageDiscoveryResult> = /*@__PURE__*/
-  messageDesc(file_api_v1alpha1_generated, 31);
+  messageDesc(file_api_v1alpha1_generated, 32);
 
 /**
  * ImageSubscription defines a subscription to an image repository.
@@ -1639,7 +1677,7 @@ export type ImageSubscription = Message<"github.com.akuity.kargo.api.v1alpha1.Im
  * Use `create(ImageSubscriptionSchema)` to create a new message.
  */
 export const ImageSubscriptionSchema: GenMessage<ImageSubscription> = /*@__PURE__*/
-  messageDesc(file_api_v1alpha1_generated, 32);
+  messageDesc(file_api_v1alpha1_generated, 33);
 
 /**
  * Project is a resource type that reconciles to a specially labeled namespace
@@ -1673,7 +1711,7 @@ export type Project = Message<"github.com.akuity.kargo.api.v1alpha1.Project"> &
  * Use `create(ProjectSchema)` to create a new message.
  */
 export const ProjectSchema: GenMessage<Project> = /*@__PURE__*/
-  messageDesc(file_api_v1alpha1_generated, 33);
+  messageDesc(file_api_v1alpha1_generated, 34);
 
 /**
  * ProjectList is a list of Project resources.
@@ -1697,7 +1735,7 @@ export type ProjectList = Message<"github.com.akuity.kargo.api.v1alpha1.ProjectL
  * Use `create(ProjectListSchema)` to create a new message.
  */
 export const ProjectListSchema: GenMessage<ProjectList> = /*@__PURE__*/
-  messageDesc(file_api_v1alpha1_generated, 34);
+  messageDesc(file_api_v1alpha1_generated, 35);
 
 /**
  * ProjectSpec describes a Project.
@@ -1719,7 +1757,7 @@ export type ProjectSpec = Message<"github.com.akuity.kargo.api.v1alpha1.ProjectS
  * Use `create(ProjectSpecSchema)` to create a new message.
  */
 export const ProjectSpecSchema: GenMessage<ProjectSpec> = /*@__PURE__*/
-  messageDesc(file_api_v1alpha1_generated, 35);
+  messageDesc(file_api_v1alpha1_generated, 36);
 
 /**
  * ProjectStatus describes a Project's current status.
@@ -1765,7 +1803,7 @@ export type ProjectStatus = Message<"github.com.akuity.kargo.api.v1alpha1.Projec
  * Use `create(ProjectStatusSchema)` to create a new message.
  */
 export const ProjectStatusSchema: GenMessage<ProjectStatus> = /*@__PURE__*/
-  messageDesc(file_api_v1alpha1_generated, 36);
+  messageDesc(file_api_v1alpha1_generated, 37);
 
 /**
  * Promotion represents a request to transition a particular Stage into a
@@ -1803,7 +1841,7 @@ export type Promotion = Message<"github.com.akuity.kargo.api.v1alpha1.Promotion"
  * Use `create(PromotionSchema)` to create a new message.
  */
 export const PromotionSchema: GenMessage<Promotion> = /*@__PURE__*/
-  messageDesc(file_api_v1alpha1_generated, 37);
+  messageDesc(file_api_v1alpha1_generated, 38);
 
 /**
  * PromotionList contains a list of Promotion
@@ -1827,7 +1865,7 @@ export type PromotionList = Message<"github.com.akuity.kargo.api.v1alpha1.Promot
  * Use `create(PromotionListSchema)` to create a new message.
  */
 export const PromotionListSchema: GenMessage<PromotionList> = /*@__PURE__*/
-  messageDesc(file_api_v1alpha1_generated, 38);
+  messageDesc(file_api_v1alpha1_generated, 39);
 
 /**
  * PromotionPolicy defines policies governing the promotion of Freight to a
@@ -1863,7 +1901,7 @@ export type PromotionPolicy = Message<"github.com.akuity.kargo.api.v1alpha1.Prom
  * Use `create(PromotionPolicySchema)` to create a new message.
  */
 export const PromotionPolicySchema: GenMessage<PromotionPolicy> = /*@__PURE__*/
-  messageDesc(file_api_v1alpha1_generated, 39);
+  messageDesc(file_api_v1alpha1_generated, 40);
 
 /**
  * PromotionReference contains the relevant information about a Promotion
@@ -1906,7 +1944,7 @@ export type PromotionReference = Message<"github.com.akuity.kargo.api.v1alpha1.P
  * Use `create(PromotionReferenceSchema)` to create a new message.
  */
 export const PromotionReferenceSchema: GenMessage<PromotionReference> = /*@__PURE__*/
-  messageDesc(file_api_v1alpha1_generated, 40);
+  messageDesc(file_api_v1alpha1_generated, 41);
 
 /**
  * PromotionSpec describes the desired transition of a specific Stage into a
@@ -1946,9 +1984,9 @@ export type PromotionSpec = Message<"github.com.akuity.kargo.api.v1alpha1.Promot
    * Vars is a list of variables that can be referenced by expressions in
    * promotion steps.
    *
-   * @generated from field: repeated github.com.akuity.kargo.api.v1alpha1.PromotionVariable vars = 4;
+   * @generated from field: repeated github.com.akuity.kargo.api.v1alpha1.ExpressionVariable vars = 4;
    */
-  vars: PromotionVariable[];
+  vars: ExpressionVariable[];
 
   /**
    * Steps specifies the directives to be executed as part of this Promotion.
@@ -1969,7 +2007,7 @@ export type PromotionSpec = Message<"github.com.akuity.kargo.api.v1alpha1.Promot
  * Use `create(PromotionSpecSchema)` to create a new message.
  */
 export const PromotionSpecSchema: GenMessage<PromotionSpec> = /*@__PURE__*/
-  messageDesc(file_api_v1alpha1_generated, 41);
+  messageDesc(file_api_v1alpha1_generated, 42);
 
 /**
  * PromotionStatus describes the current state of the transition represented by
@@ -2067,7 +2105,7 @@ export type PromotionStatus = Message<"github.com.akuity.kargo.api.v1alpha1.Prom
  * Use `create(PromotionStatusSchema)` to create a new message.
  */
 export const PromotionStatusSchema: GenMessage<PromotionStatus> = /*@__PURE__*/
-  messageDesc(file_api_v1alpha1_generated, 42);
+  messageDesc(file_api_v1alpha1_generated, 43);
 
 /**
  * PromotionStep describes a directive to be executed as part of a Promotion.
@@ -2122,9 +2160,9 @@ export type PromotionStep = Message<"github.com.akuity.kargo.api.v1alpha1.Promot
    * the step's Config. The values override the values specified in the
    * PromotionSpec.
    *
-   * @generated from field: repeated github.com.akuity.kargo.api.v1alpha1.PromotionVariable vars = 6;
+   * @generated from field: repeated github.com.akuity.kargo.api.v1alpha1.ExpressionVariable vars = 6;
    */
-  vars: PromotionVariable[];
+  vars: ExpressionVariable[];
 
   /**
    * Config is opaque configuration for the PromotionStep that is understood
@@ -2142,7 +2180,7 @@ export type PromotionStep = Message<"github.com.akuity.kargo.api.v1alpha1.Promot
  * Use `create(PromotionStepSchema)` to create a new message.
  */
 export const PromotionStepSchema: GenMessage<PromotionStep> = /*@__PURE__*/
-  messageDesc(file_api_v1alpha1_generated, 43);
+  messageDesc(file_api_v1alpha1_generated, 44);
 
 /**
  * PromotionStepRetry describes the retry policy for a PromotionStep.
@@ -2201,7 +2239,7 @@ export type PromotionStepRetry = Message<"github.com.akuity.kargo.api.v1alpha1.P
  * Use `create(PromotionStepRetrySchema)` to create a new message.
  */
 export const PromotionStepRetrySchema: GenMessage<PromotionStepRetry> = /*@__PURE__*/
-  messageDesc(file_api_v1alpha1_generated, 44);
+  messageDesc(file_api_v1alpha1_generated, 45);
 
 /**
  * @generated from message github.com.akuity.kargo.api.v1alpha1.PromotionTask
@@ -2228,7 +2266,7 @@ export type PromotionTask = Message<"github.com.akuity.kargo.api.v1alpha1.Promot
  * Use `create(PromotionTaskSchema)` to create a new message.
  */
 export const PromotionTaskSchema: GenMessage<PromotionTask> = /*@__PURE__*/
-  messageDesc(file_api_v1alpha1_generated, 45);
+  messageDesc(file_api_v1alpha1_generated, 46);
 
 /**
  * PromotionTaskList contains a list of PromotionTasks.
@@ -2252,7 +2290,7 @@ export type PromotionTaskList = Message<"github.com.akuity.kargo.api.v1alpha1.Pr
  * Use `create(PromotionTaskListSchema)` to create a new message.
  */
 export const PromotionTaskListSchema: GenMessage<PromotionTaskList> = /*@__PURE__*/
-  messageDesc(file_api_v1alpha1_generated, 46);
+  messageDesc(file_api_v1alpha1_generated, 47);
 
 /**
  * PromotionTaskReference describes a reference to a PromotionTask.
@@ -2289,7 +2327,7 @@ export type PromotionTaskReference = Message<"github.com.akuity.kargo.api.v1alph
  * Use `create(PromotionTaskReferenceSchema)` to create a new message.
  */
 export const PromotionTaskReferenceSchema: GenMessage<PromotionTaskReference> = /*@__PURE__*/
-  messageDesc(file_api_v1alpha1_generated, 47);
+  messageDesc(file_api_v1alpha1_generated, 48);
 
 /**
  * @generated from message github.com.akuity.kargo.api.v1alpha1.PromotionTaskSpec
@@ -2300,9 +2338,9 @@ export type PromotionTaskSpec = Message<"github.com.akuity.kargo.api.v1alpha1.Pr
    * values of these variables are the default values that can be
    * overridden by the step referencing the task.
    *
-   * @generated from field: repeated github.com.akuity.kargo.api.v1alpha1.PromotionVariable vars = 1;
+   * @generated from field: repeated github.com.akuity.kargo.api.v1alpha1.ExpressionVariable vars = 1;
    */
-  vars: PromotionVariable[];
+  vars: ExpressionVariable[];
 
   /**
    * Steps specifies the directives to be executed as part of this
@@ -2323,7 +2361,7 @@ export type PromotionTaskSpec = Message<"github.com.akuity.kargo.api.v1alpha1.Pr
  * Use `create(PromotionTaskSpecSchema)` to create a new message.
  */
 export const PromotionTaskSpecSchema: GenMessage<PromotionTaskSpec> = /*@__PURE__*/
-  messageDesc(file_api_v1alpha1_generated, 48);
+  messageDesc(file_api_v1alpha1_generated, 49);
 
 /**
  * PromotionTemplate defines a template for a Promotion that can be used to
@@ -2343,7 +2381,7 @@ export type PromotionTemplate = Message<"github.com.akuity.kargo.api.v1alpha1.Pr
  * Use `create(PromotionTemplateSchema)` to create a new message.
  */
 export const PromotionTemplateSchema: GenMessage<PromotionTemplate> = /*@__PURE__*/
-  messageDesc(file_api_v1alpha1_generated, 49);
+  messageDesc(file_api_v1alpha1_generated, 50);
 
 /**
  * PromotionTemplateSpec describes the (partial) specification of a Promotion
@@ -2357,9 +2395,9 @@ export type PromotionTemplateSpec = Message<"github.com.akuity.kargo.api.v1alpha
    * Vars is a list of variables that can be referenced by expressions in
    * promotion steps.
    *
-   * @generated from field: repeated github.com.akuity.kargo.api.v1alpha1.PromotionVariable vars = 2;
+   * @generated from field: repeated github.com.akuity.kargo.api.v1alpha1.ExpressionVariable vars = 2;
    */
-  vars: PromotionVariable[];
+  vars: ExpressionVariable[];
 
   /**
    * Steps specifies the directives to be executed as part of a Promotion.
@@ -2379,40 +2417,6 @@ export type PromotionTemplateSpec = Message<"github.com.akuity.kargo.api.v1alpha
  * Use `create(PromotionTemplateSpecSchema)` to create a new message.
  */
 export const PromotionTemplateSpecSchema: GenMessage<PromotionTemplateSpec> = /*@__PURE__*/
-  messageDesc(file_api_v1alpha1_generated, 50);
-
-/**
- * PromotionVariable describes a single variable that may be referenced by
- * expressions in promotion steps.
- *
- * @generated from message github.com.akuity.kargo.api.v1alpha1.PromotionVariable
- */
-export type PromotionVariable = Message<"github.com.akuity.kargo.api.v1alpha1.PromotionVariable"> & {
-  /**
-   * Name is the name of the variable.
-   *
-   * +kubebuilder:validation:MinLength=1
-   * +kubebuilder:validation:Pattern=^[a-zA-Z_]\w*$
-   *
-   * @generated from field: optional string name = 1;
-   */
-  name: string;
-
-  /**
-   * Value is the value of the variable. It is allowed to utilize expressions
-   * in the value.
-   * See https://docs.kargo.io/user-guide/reference-docs/expressions for details.
-   *
-   * @generated from field: optional string value = 2;
-   */
-  value: string;
-};
-
-/**
- * Describes the message github.com.akuity.kargo.api.v1alpha1.PromotionVariable.
- * Use `create(PromotionVariableSchema)` to create a new message.
- */
-export const PromotionVariableSchema: GenMessage<PromotionVariable> = /*@__PURE__*/
   messageDesc(file_api_v1alpha1_generated, 51);
 
 /**
@@ -2529,6 +2533,15 @@ export type StageSpec = Message<"github.com.akuity.kargo.api.v1alpha1.StageSpec"
    */
   shard: string;
 
+  /**
+   * Vars is a list of variables that can be referenced anywhere in the
+   * StageSpec that supports expressions. For example, the PromotionTemplate
+   * and arguments of the Verification.
+   *
+   * @generated from field: repeated github.com.akuity.kargo.api.v1alpha1.ExpressionVariable vars = 7;
+   */
+  vars: ExpressionVariable[];
+
   /**
    * RequestedFreight expresses the Stage's need for certain pieces of Freight,
    * each having originated from a particular Warehouse. This list must be
diff --git a/ui/src/gen/directives/helm-template-config.json b/ui/src/gen/directives/helm-template-config.json
index c5f99095..a85e35b7 100644
--- a/ui/src/gen/directives/helm-template-config.json
+++ b/ui/src/gen/directives/helm-template-config.json
@@ -1,6 +1,23 @@
 {
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "title": "HelmTemplateConfig",
+ "definitions": {
+  "setValues": {
+   "type": "object",
+   "additionalProperties": false,
+   "properties": {
+    "key": {
+     "type": "string",
+     "description": "The key whose value should be set. For nested values, use dots to delimit key parts. e.g. `image.tag`.",
+     "minLength": 1
+    },
+    "value": {
+     "type": "string",
+     "description": "The new value for the key."
+    }
+   }
+  }
+ },
  "type": "object",
  "additionalProperties": false,
  "properties": {
@@ -36,6 +53,25 @@
     "minLength": 1
    }
   },
+  "setValues": {
+   "type": "array",
+   "description": "Allows for amending chart configuration inline as one would with the `helm template` command's `--set` flag.",
+   "items": {
+    "type": "object",
+    "additionalProperties": false,
+    "properties": {
+     "key": {
+      "type": "string",
+      "description": "The key whose value should be set. For nested values, use dots to delimit key parts. e.g. `image.tag`.",
+      "minLength": 1
+     },
+     "value": {
+      "type": "string",
+      "description": "The new value for the key."
+     }
+    }
+   }
+  },
   "includeCRDs": {
    "type": "boolean",
    "description": "Whether to include CRDs in the rendered manifests.",
diff --git a/ui/src/gen/schema/clusterpromotiontasks.kargo.akuity.io_v1alpha1.json b/ui/src/gen/schema/clusterpromotiontasks.kargo.akuity.io_v1alpha1.json
index 33108dcf..d53e31b1 100644
--- a/ui/src/gen/schema/clusterpromotiontasks.kargo.akuity.io_v1alpha1.json
+++ b/ui/src/gen/schema/clusterpromotiontasks.kargo.akuity.io_v1alpha1.json
@@ -81,7 +81,7 @@
               "vars": {
                 "description": "Vars is a list of variables that can be referenced by expressions in\nthe step's Config. The values override the values specified in the\nPromotionSpec.",
                 "items": {
-                  "description": "PromotionVariable describes a single variable that may be referenced by\nexpressions in promotion steps.",
+                  "description": "ExpressionVariable describes a single variable that may be referenced by\nexpressions in the context of a ClusterPromotionTask, PromotionTask,\nPromotion, AnalysisRun arguments, or other objects that support expressions.\n\nIt is used to pass information to the expression evaluation engine, and to\nallow for dynamic evaluation of expressions based on the variable values.",
                   "properties": {
                     "name": {
                       "description": "Name is the name of the variable.",
@@ -116,7 +116,7 @@
         "vars": {
           "description": "Vars specifies the variables available to the PromotionTask. The\nvalues of these variables are the default values that can be\noverridden by the step referencing the task.",
           "items": {
-            "description": "PromotionVariable describes a single variable that may be referenced by\nexpressions in promotion steps.",
+            "description": "ExpressionVariable describes a single variable that may be referenced by\nexpressions in the context of a ClusterPromotionTask, PromotionTask,\nPromotion, AnalysisRun arguments, or other objects that support expressions.\n\nIt is used to pass information to the expression evaluation engine, and to\nallow for dynamic evaluation of expressions based on the variable values.",
             "properties": {
               "name": {
                 "description": "Name is the name of the variable.",
diff --git a/ui/src/gen/schema/promotions.kargo.akuity.io_v1alpha1.json b/ui/src/gen/schema/promotions.kargo.akuity.io_v1alpha1.json
index 3321b59d..32005620 100644
--- a/ui/src/gen/schema/promotions.kargo.akuity.io_v1alpha1.json
+++ b/ui/src/gen/schema/promotions.kargo.akuity.io_v1alpha1.json
@@ -96,7 +96,7 @@
               "vars": {
                 "description": "Vars is a list of variables that can be referenced by expressions in\nthe step's Config. The values override the values specified in the\nPromotionSpec.",
                 "items": {
-                  "description": "PromotionVariable describes a single variable that may be referenced by\nexpressions in promotion steps.",
+                  "description": "ExpressionVariable describes a single variable that may be referenced by\nexpressions in the context of a ClusterPromotionTask, PromotionTask,\nPromotion, AnalysisRun arguments, or other objects that support expressions.\n\nIt is used to pass information to the expression evaluation engine, and to\nallow for dynamic evaluation of expressions based on the variable values.",
                   "properties": {
                     "name": {
                       "description": "Name is the name of the variable.",
@@ -131,7 +131,7 @@
         "vars": {
           "description": "Vars is a list of variables that can be referenced by expressions in\npromotion steps.",
           "items": {
-            "description": "PromotionVariable describes a single variable that may be referenced by\nexpressions in promotion steps.",
+            "description": "ExpressionVariable describes a single variable that may be referenced by\nexpressions in the context of a ClusterPromotionTask, PromotionTask,\nPromotion, AnalysisRun arguments, or other objects that support expressions.\n\nIt is used to pass information to the expression evaluation engine, and to\nallow for dynamic evaluation of expressions based on the variable values.",
             "properties": {
               "name": {
                 "description": "Name is the name of the variable.",
diff --git a/ui/src/gen/schema/promotiontasks.kargo.akuity.io_v1alpha1.json b/ui/src/gen/schema/promotiontasks.kargo.akuity.io_v1alpha1.json
index 396f2e15..cfc7be8d 100644
--- a/ui/src/gen/schema/promotiontasks.kargo.akuity.io_v1alpha1.json
+++ b/ui/src/gen/schema/promotiontasks.kargo.akuity.io_v1alpha1.json
@@ -81,7 +81,7 @@
               "vars": {
                 "description": "Vars is a list of variables that can be referenced by expressions in\nthe step's Config. The values override the values specified in the\nPromotionSpec.",
                 "items": {
-                  "description": "PromotionVariable describes a single variable that may be referenced by\nexpressions in promotion steps.",
+                  "description": "ExpressionVariable describes a single variable that may be referenced by\nexpressions in the context of a ClusterPromotionTask, PromotionTask,\nPromotion, AnalysisRun arguments, or other objects that support expressions.\n\nIt is used to pass information to the expression evaluation engine, and to\nallow for dynamic evaluation of expressions based on the variable values.",
                   "properties": {
                     "name": {
                       "description": "Name is the name of the variable.",
@@ -116,7 +116,7 @@
         "vars": {
           "description": "Vars specifies the variables available to the PromotionTask. The\nvalues of these variables are the default values that can be\noverridden by the step referencing the task.",
           "items": {
-            "description": "PromotionVariable describes a single variable that may be referenced by\nexpressions in promotion steps.",
+            "description": "ExpressionVariable describes a single variable that may be referenced by\nexpressions in the context of a ClusterPromotionTask, PromotionTask,\nPromotion, AnalysisRun arguments, or other objects that support expressions.\n\nIt is used to pass information to the expression evaluation engine, and to\nallow for dynamic evaluation of expressions based on the variable values.",
             "properties": {
               "name": {
                 "description": "Name is the name of the variable.",
diff --git a/ui/src/gen/schema/stages.kargo.akuity.io_v1alpha1.json b/ui/src/gen/schema/stages.kargo.akuity.io_v1alpha1.json
index c2cb4eb0..0548d430 100644
--- a/ui/src/gen/schema/stages.kargo.akuity.io_v1alpha1.json
+++ b/ui/src/gen/schema/stages.kargo.akuity.io_v1alpha1.json
@@ -88,7 +88,7 @@
                       "vars": {
                         "description": "Vars is a list of variables that can be referenced by expressions in\nthe step's Config. The values override the values specified in the\nPromotionSpec.",
                         "items": {
-                          "description": "PromotionVariable describes a single variable that may be referenced by\nexpressions in promotion steps.",
+                          "description": "ExpressionVariable describes a single variable that may be referenced by\nexpressions in the context of a ClusterPromotionTask, PromotionTask,\nPromotion, AnalysisRun arguments, or other objects that support expressions.\n\nIt is used to pass information to the expression evaluation engine, and to\nallow for dynamic evaluation of expressions based on the variable values.",
                           "properties": {
                             "name": {
                               "description": "Name is the name of the variable.",
@@ -123,7 +123,7 @@
                 "vars": {
                   "description": "Vars is a list of variables that can be referenced by expressions in\npromotion steps.",
                   "items": {
-                    "description": "PromotionVariable describes a single variable that may be referenced by\nexpressions in promotion steps.",
+                    "description": "ExpressionVariable describes a single variable that may be referenced by\nexpressions in the context of a ClusterPromotionTask, PromotionTask,\nPromotion, AnalysisRun arguments, or other objects that support expressions.\n\nIt is used to pass information to the expression evaluation engine, and to\nallow for dynamic evaluation of expressions based on the variable values.",
                     "properties": {
                       "name": {
                         "description": "Name is the name of the variable.",
@@ -223,6 +223,29 @@
           "description": "Shard is the name of the shard that this Stage belongs to. This is an\noptional field. If not specified, the Stage will belong to the default\nshard. A defaulting webhook will sync the value of the\nkargo.akuity.io/shard label with the value of this field. When this field\nis empty, the webhook will ensure that label is absent.",
           "type": "string"
         },
+        "vars": {
+          "description": "Vars is a list of variables that can be referenced anywhere in the\nStageSpec that supports expressions. For example, the PromotionTemplate\nand arguments of the Verification.",
+          "items": {
+            "description": "ExpressionVariable describes a single variable that may be referenced by\nexpressions in the context of a ClusterPromotionTask, PromotionTask,\nPromotion, AnalysisRun arguments, or other objects that support expressions.\n\nIt is used to pass information to the expression evaluation engine, and to\nallow for dynamic evaluation of expressions based on the variable values.",
+            "properties": {
+              "name": {
+                "description": "Name is the name of the variable.",
+                "minLength": 1,
+                "pattern": "^[a-zA-Z_]\\w*$",
+                "type": "string"
+              },
+              "value": {
+                "description": "Value is the value of the variable. It is allowed to utilize expressions\nin the value.\nSee https://docs.kargo.io/user-guide/reference-docs/expressions for details.",
+                "type": "string"
+              }
+            },
+            "required": [
+              "name"
+            ],
+            "type": "object"
+          },
+          "type": "array"
+        },
         "verification": {
           "description": "Verification describes how to verify a Stage's current Freight is fit for\npromotion downstream.",
           "properties": {
diff --git a/ui/src/pages/settings.tsx b/ui/src/pages/settings.tsx
index e2b31769..d058be7c 100644
--- a/ui/src/pages/settings.tsx
+++ b/ui/src/pages/settings.tsx
@@ -1,75 +1,65 @@
 import { faBarChart, faTasks } from '@fortawesome/free-solid-svg-icons';
 import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import { Layout, Menu, MenuProps } from 'antd';
-import { useNavigate, generatePath } from 'react-router-dom';
+import { Breadcrumb, Flex, Menu, Typography } from 'antd';
+import { NavLink, Routes, Route, Navigate, useLocation } from 'react-router-dom';
 
-import { paths } from '@ui/config/paths';
-import { PageTitle } from '@ui/features/common';
+import { BaseHeader } from '@ui/features/common/layout/base-header';
 import { ClusterAnalysisTemplatesList } from '@ui/features/settings/analysis-templates/analysis-templates';
 import { ClusterPromotionTasks } from '@ui/features/settings/cluster-promotion-tasks/cluster-promotion-tasks';
 
-const { Sider, Content } = Layout;
-
-type MenuItem = Required<MenuProps>['items'][number];
-
-export const Settings = ({ section = 'verification' }: { section?: string }) => {
-  const navigate = useNavigate();
-
-  const items: MenuItem[] = [
-    {
-      key: 'verification',
-      label: 'Verification',
-      icon: <FontAwesomeIcon icon={faBarChart} />,
-      onClick: () => {
-        navigate(generatePath(paths.settingsAnalysisTemplates));
-      }
-    },
-    {
-      key: 'cluster-promotion-tasks',
-      label: 'ClusterPromotionTasks',
-      icon: <FontAwesomeIcon icon={faTasks} />,
-      onClick: () => {
-        navigate(generatePath(paths.settingsClusterPromotionTasks));
-      }
-    }
-  ];
+const settingsViews = {
+  verification: {
+    label: 'Verificaion',
+    icon: faBarChart,
+    path: 'analysis-templates',
+    component: ClusterAnalysisTemplatesList
+  },
+  clusterPromotionTasks: {
+    label: 'ClusterPromotionTasks',
+    icon: faTasks,
+    path: 'cluster-promotion-tasks',
+    component: ClusterPromotionTasks
+  }
+};
 
-  const renderSection = (section: string) => {
-    switch (section) {
-      case 'verification':
-        return <ClusterAnalysisTemplatesList />;
-      case 'cluster-promotion-tasks':
-        return <ClusterPromotionTasks />;
-    }
-  };
+const defaultView = settingsViews.verification;
 
-  const getSectionTitle = (section: string) => {
-    switch (section) {
-      case 'verification':
-        return 'Cluster Analysis Templates';
-      case 'cluster-promotion-tasks':
-        return 'Cluster Promotion Tasks';
-    }
-  };
+export const Settings = () => {
+  const location = useLocation();
 
   return (
-    <Layout className='min-h-screen'>
-      <Sider
-        width={250}
-        className='border-r border-gray-300 shadow-sm'
-        style={{ background: 'white' }}
-      >
-        <div className='p-4'>
-          <PageTitle title='Settings' />
-        </div>
-        <Menu mode='vertical' defaultSelectedKeys={[section]} items={items} />
-      </Sider>
-      <Layout>
-        <Content className='p-6 bg-gray-100'>
-          <div className='text-2xl font-semibold mb-4'>{getSectionTitle(section)}</div>
-          {renderSection(section)}
-        </Content>
-      </Layout>
-    </Layout>
+    <>
+      <BaseHeader>
+        <Breadcrumb separator='>' items={[{ title: 'Settings' }]} />
+      </BaseHeader>
+      <div className='py-4 px-6'>
+        <Typography.Title level={3}>Settings</Typography.Title>
+        <Flex gap={24} className='mt-2'>
+          <div style={{ width: 240 }}>
+            <Menu
+              className='-ml-2 -mt-1'
+              style={{ border: 0, background: 'transparent' }}
+              selectedKeys={Object.values(settingsViews)
+                .map((i) => i.path)
+                .filter((i) => location.pathname.endsWith(i))}
+              items={Object.values(settingsViews).map((i) => ({
+                label: <NavLink to={`../${i.path}`}>{i.label}</NavLink>,
+                icon: <FontAwesomeIcon icon={i.icon} />,
+                key: i.path
+              }))}
+            />
+          </div>
+          <div className='flex-1 overflow-hidden' style={{ maxWidth: '920px', minHeight: '700px' }}>
+            <Routes>
+              <Route index element={<Navigate to={defaultView.path} replace={true} />} />
+              {Object.values(settingsViews).map((t) => (
+                <Route key={t.path} path={t.path} element={<t.component />} />
+              ))}
+              <Route path='*' element={<Navigate to='../' replace={true} />} />
+            </Routes>
+          </div>
+        </Flex>
+      </div>
+    </>
   );
 };



Test patch:
diff --git a/internal/api/events_test.go b/internal/api/events_test.go
new file mode 100644
index 00000000..68895b64
--- /dev/null
+++ b/internal/api/events_test.go
@@ -0,0 +1,54 @@
+package api
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/require"
+
+	kargoapi "github.com/akuity/kargo/api/v1alpha1"
+	"github.com/akuity/kargo/internal/server/user"
+)
+
+func TestFormatEventUserActor(t *testing.T) {
+	for _, test := range []struct {
+		name     string
+		user     user.Info
+		expected string
+	}{
+		{
+			name:     "admin",
+			user:     user.Info{IsAdmin: true},
+			expected: kargoapi.EventActorAdmin,
+		},
+		{
+			name: "sub",
+			user: user.Info{
+				Claims: map[string]any{
+					"sub": "subject",
+				},
+			},
+			expected: kargoapi.EventActorSubjectPrefix + "subject",
+		},
+		{
+			name: "email",
+			user: user.Info{
+				Claims: map[string]any{
+					"email": "email@inbox.com",
+				},
+			},
+			expected: kargoapi.EventActorEmailPrefix + "email@inbox.com",
+		},
+		{
+			name: "oidc-username",
+			user: user.Info{
+				Username: "oidc-username",
+			},
+			expected: formatOIDCUsername("oidc-username"),
+		},
+	} {
+		t.Run(test.name, func(t *testing.T) {
+			result := FormatEventUserActor(test.user)
+			require.Equal(t, test.expected, result)
+		})
+	}
+}
diff --git a/internal/controller/freight/finder_test.go b/internal/controller/freight/finder_test.go
index 2df033e5..4ee9a259 100644
--- a/internal/controller/freight/finder_test.go
+++ b/internal/controller/freight/finder_test.go
@@ -59,8 +59,10 @@ func TestFindCommit(t *testing.T) {
 					Commits: []kargoapi.GitCommit{testCommit2},
 				},
 			},
-			assertions: func(t *testing.T, _ *kargoapi.GitCommit, err error) {
-				require.ErrorContains(t, err, "not found in referenced Freight")
+			assertions: func(t *testing.T, commit *kargoapi.GitCommit, err error) {
+				require.NoError(t, err)
+				require.Nil(t, commit)
+
 			},
 		},
 		{
@@ -127,8 +129,9 @@ func TestFindCommit(t *testing.T) {
 					RequestedFreight: []kargoapi.FreightRequest{{Origin: testOrigin1}},
 				},
 			},
-			assertions: func(t *testing.T, _ *kargoapi.GitCommit, err error) {
-				require.ErrorContains(t, err, "not found in referenced Freight")
+			assertions: func(t *testing.T, commit *kargoapi.GitCommit, err error) {
+				require.NoError(t, err)
+				require.Nil(t, commit)
 			},
 		},
 		{
@@ -293,8 +296,9 @@ func TestFindImage(t *testing.T) {
 					Images: []kargoapi.Image{testImage2},
 				},
 			},
-			assertions: func(t *testing.T, _ *kargoapi.Image, err error) {
-				require.ErrorContains(t, err, "not found in referenced Freight")
+			assertions: func(t *testing.T, image *kargoapi.Image, err error) {
+				require.NoError(t, err)
+				require.Nil(t, image)
 			},
 		},
 		{
@@ -359,8 +363,9 @@ func TestFindImage(t *testing.T) {
 					RequestedFreight: []kargoapi.FreightRequest{{Origin: testOrigin1}},
 				},
 			},
-			assertions: func(t *testing.T, _ *kargoapi.Image, err error) {
-				require.ErrorContains(t, err, "not found in referenced Freight")
+			assertions: func(t *testing.T, image *kargoapi.Image, err error) {
+				require.NoError(t, err)
+				require.Nil(t, image)
 			},
 		},
 		{
@@ -656,8 +661,9 @@ func TestFindChart(t *testing.T) {
 					Charts: []kargoapi.Chart{testChart2},
 				},
 			},
-			assertions: func(t *testing.T, _ *kargoapi.Chart, err error) {
-				require.ErrorContains(t, err, "not found in referenced Freight")
+			assertions: func(t *testing.T, chart *kargoapi.Chart, err error) {
+				require.NoError(t, err)
+				require.Nil(t, chart)
 			},
 		},
 		{
diff --git a/internal/controller/promotions/promotions_test.go b/internal/controller/promotions/promotions_test.go
index 09aded66..b2fc49e7 100644
--- a/internal/controller/promotions/promotions_test.go
+++ b/internal/controller/promotions/promotions_test.go
@@ -531,6 +531,118 @@ func Test_parseCreateActorAnnotation(t *testing.T) {
 	}
 }
 
+func Test_calculateRequeueInterval(t *testing.T) {
+	defaultTimeout := 5 * time.Minute
+	for _, test := range []struct {
+		name       string
+		promo      *kargoapi.Promotion
+		setup      func(*testing.T)
+		assertions func(*testing.T, time.Duration)
+	}{
+		{
+			name: "should return default requeue interval",
+			promo: &kargoapi.Promotion{
+				ObjectMeta: metav1.ObjectMeta{
+					CreationTimestamp: now,
+					Name:              "fake-name",
+					Namespace:         "fake-namespace",
+				},
+				Spec: kargoapi.PromotionSpec{
+					Stage: "fake-stage",
+					Steps: []kargoapi.PromotionStep{
+						{
+							Uses: "slow-step",
+							Retry: &kargoapi.PromotionStepRetry{
+								Timeout: &metav1.Duration{
+									// exceeds default threshold
+									Duration: 10 * time.Minute,
+								},
+							},
+						},
+					},
+				},
+				Status: kargoapi.PromotionStatus{
+					Phase:       kargoapi.PromotionPhasePending,
+					CurrentStep: 0,
+					StepExecutionMetadata: []kargoapi.StepExecutionMetadata{
+						{StartedAt: &now},
+					},
+				},
+			},
+			setup: func(tt *testing.T) {
+				tt.Helper()
+				var (
+					// exceeds than the 5 minute default
+					stepRunnerTimeout = 10 * time.Minute
+					numRetries        = uint32(1)
+					runner            = promotion.NewMockRetryableStepRunner(
+						"slow-step",
+						&stepRunnerTimeout,
+						numRetries,
+					)
+				)
+				promotion.RegisterStepRunner(runner)
+			},
+			assertions: func(tt *testing.T, d time.Duration) {
+				require.Equal(tt, d, defaultTimeout)
+			},
+		},
+		{
+			name: "requeue interval should return requeue interval less than the default",
+			promo: &kargoapi.Promotion{
+				ObjectMeta: metav1.ObjectMeta{
+					CreationTimestamp: now,
+					Name:              "fake-name",
+					Namespace:         "fake-namespace",
+				},
+				Spec: kargoapi.PromotionSpec{
+					Stage: "fake-stage",
+					Steps: []kargoapi.PromotionStep{
+						{
+							Uses: "fast-step",
+							Retry: &kargoapi.PromotionStepRetry{
+								Timeout: &metav1.Duration{
+									// faster than default threshold
+									Duration: 3 * time.Minute,
+								},
+							},
+						},
+					},
+				},
+				Status: kargoapi.PromotionStatus{
+					Phase:       kargoapi.PromotionPhasePending,
+					CurrentStep: 0,
+					StepExecutionMetadata: []kargoapi.StepExecutionMetadata{
+						{StartedAt: &now},
+					},
+				},
+			},
+			setup: func(tt *testing.T) {
+				tt.Helper()
+				var (
+					// lower than the 5 minute default
+					stepRunnerTimeout = 3 * time.Minute
+					numRetries        = uint32(1)
+					runner            = promotion.NewMockRetryableStepRunner(
+						"fast-step",
+						&stepRunnerTimeout,
+						numRetries,
+					)
+				)
+				promotion.RegisterStepRunner(runner)
+			},
+			assertions: func(tt *testing.T, d time.Duration) {
+				require.Less(tt, d, defaultTimeout)
+			},
+		},
+	} {
+		t.Run(test.name, func(t *testing.T) {
+			test.setup(t)
+			test.assertions(t, calculateRequeueInterval(test.promo))
+		})
+	}
+}
+
 // nolint: unparam
 func newPromo(namespace, name, stage string,
 	phase kargoapi.PromotionPhase,
diff --git a/internal/credentials/kubernetes/gar/workload_identity_federation_test.go b/internal/credentials/kubernetes/gar/workload_identity_federation_test.go
index 955b2b67..9a4048c0 100644
--- a/internal/credentials/kubernetes/gar/workload_identity_federation_test.go
+++ b/internal/credentials/kubernetes/gar/workload_identity_federation_test.go
@@ -8,6 +8,7 @@ import (
 
 	"github.com/patrickmn/go-cache"
 	"github.com/stretchr/testify/assert"
+	"golang.org/x/oauth2"
 
 	"github.com/akuity/kargo/internal/credentials"
 )
@@ -98,13 +99,20 @@ func TestWorkloadIdentityFederationProvider_GetCredentials(t *testing.T) {
 	)
 
 	testCases := []struct {
-		name       string
-		provider   *WorkloadIdentityFederationProvider
-		setupCache func(c *cache.Cache)
-		project    string
-		credType   credentials.Type
-		repoURL    string
-		assert     func(t *testing.T, c *cache.Cache, creds *credentials.Credentials, err error)
+		name                  string
+		provider              *WorkloadIdentityFederationProvider
+		setupTokenCache       func(c *cache.Cache)
+		setupTokenSourceCache func(c *cache.Cache)
+		project               string
+		credType              credentials.Type
+		repoURL               string
+		assert                func(
+			t *testing.T,
+			tokenCache *cache.Cache,
+			tokenSourceCache *cache.Cache,
+			creds *credentials.Credentials,
+			err error,
+		)
 	}{
 		{
 			name: "returns nil when not supported",
@@ -114,24 +122,24 @@ func TestWorkloadIdentityFederationProvider_GetCredentials(t *testing.T) {
 			project:  fakeProject,
 			credType: credentials.TypeGit, // Not supported
 			repoURL:  fakeGCRRepoURL,
-			assert: func(t *testing.T, _ *cache.Cache, creds *credentials.Credentials, err error) {
+			assert: func(t *testing.T, _, _ *cache.Cache, creds *credentials.Credentials, err error) {
 				assert.NoError(t, err)
 				assert.Nil(t, creds)
 			},
 		},
 		{
-			name: "cache hit",
+			name: "token cache hit",
 			provider: &WorkloadIdentityFederationProvider{
 				projectID:  fakeProjectID,
 				tokenCache: cache.New(10*time.Hour, time.Hour),
 			},
-			setupCache: func(c *cache.Cache) {
+			setupTokenCache: func(c *cache.Cache) {
 				c.Set(tokenCacheKey(fakeProject), fakeToken, cache.DefaultExpiration)
 			},
 			project:  fakeProject,
 			credType: credentials.TypeImage,
 			repoURL:  fakeGCRRepoURL,
-			assert: func(t *testing.T, _ *cache.Cache, creds *credentials.Credentials, err error) {
+			assert: func(t *testing.T, _, _ *cache.Cache, creds *credentials.Credentials, err error) {
 				assert.NoError(t, err)
 				assert.NotNil(t, creds)
 				assert.Equal(t, accessTokenUsername, creds.Username)
@@ -139,25 +147,49 @@ func TestWorkloadIdentityFederationProvider_GetCredentials(t *testing.T) {
 			},
 		},
 		{
-			name: "cache miss, successful token fetch",
+			name: "token cache miss, token source cache hit",
 			provider: &WorkloadIdentityFederationProvider{
-				projectID:  fakeProjectID,
-				tokenCache: cache.New(10*time.Hour, time.Hour),
+				projectID:        fakeProjectID,
+				tokenCache:       cache.New(10*time.Hour, time.Hour),
+				tokenSourceCache: cache.New(10*time.Hour, time.Hour),
 				getAccessTokenFn: func(_ context.Context, _ string) (string, error) {
 					return fakeToken, nil
 				},
 			},
+			setupTokenSourceCache: func(c *cache.Cache) {
+				c.Set(tokenCacheKey(fakeProject), newFakeTokenSource(fakeToken), cache.DefaultExpiration)
+			},
 			project:  fakeProject,
 			credType: credentials.TypeImage,
 			repoURL:  fakeGCRRepoURL,
-			assert: func(t *testing.T, c *cache.Cache, creds *credentials.Credentials, err error) {
+			assert: func(t *testing.T, _, _ *cache.Cache, creds *credentials.Credentials, err error) {
+				assert.NoError(t, err)
+				assert.NotNil(t, creds)
+				assert.Equal(t, accessTokenUsername, creds.Username)
+				assert.Equal(t, fakeToken, creds.Password)
+			},
+		},
+		{
+			name: "miss in both caches, successful token fetch",
+			provider: &WorkloadIdentityFederationProvider{
+				projectID:        fakeProjectID,
+				tokenCache:       cache.New(10*time.Hour, time.Hour),
+				tokenSourceCache: cache.New(10*time.Hour, time.Hour),
+				getAccessTokenFn: func(_ context.Context, _ string) (string, error) {
+					return fakeToken, nil
+				},
+			},
+			project:  fakeProject,
+			credType: credentials.TypeImage,
+			repoURL:  fakeGCRRepoURL,
+			assert: func(t *testing.T, tokenCache, _ *cache.Cache, creds *credentials.Credentials, err error) {
 				assert.NoError(t, err)
 				assert.NotNil(t, creds)
 				assert.Equal(t, accessTokenUsername, creds.Username)
 				assert.Equal(t, fakeToken, creds.Password)
 
 				// Verify the token was cached
-				token, found := c.Get(tokenCacheKey(fakeProject))
+				token, found := tokenCache.Get(tokenCacheKey(fakeProject))
 				assert.True(t, found)
 				assert.Equal(t, fakeToken, token)
 			},
@@ -165,8 +197,9 @@ func TestWorkloadIdentityFederationProvider_GetCredentials(t *testing.T) {
 		{
 			name: "error in getAccessToken",
 			provider: &WorkloadIdentityFederationProvider{
-				projectID:  fakeProjectID,
-				tokenCache: cache.New(10*time.Hour, time.Hour),
+				projectID:        fakeProjectID,
+				tokenCache:       cache.New(10*time.Hour, time.Hour),
+				tokenSourceCache: cache.New(10*time.Hour, time.Hour),
 				getAccessTokenFn: func(_ context.Context, _ string) (string, error) {
 					return "", fmt.Errorf("token fetch error")
 				},
@@ -174,7 +207,7 @@ func TestWorkloadIdentityFederationProvider_GetCredentials(t *testing.T) {
 			project:  fakeProject,
 			credType: credentials.TypeImage,
 			repoURL:  fakeGCRRepoURL,
-			assert: func(t *testing.T, _ *cache.Cache, creds *credentials.Credentials, err error) {
+			assert: func(t *testing.T, _, _ *cache.Cache, creds *credentials.Credentials, err error) {
 				assert.ErrorContains(t, err, "token fetch error")
 				assert.Nil(t, creds)
 			},
@@ -182,30 +215,60 @@ func TestWorkloadIdentityFederationProvider_GetCredentials(t *testing.T) {
 		{
 			name: "empty token from getAccessToken",
 			provider: &WorkloadIdentityFederationProvider{
-				projectID:  fakeProjectID,
-				tokenCache: cache.New(10*time.Hour, time.Hour),
+				projectID:        fakeProjectID,
+				tokenCache:       cache.New(10*time.Hour, time.Hour),
+				tokenSourceCache: cache.New(10*time.Hour, time.Hour),
 				getAccessTokenFn: func(_ context.Context, _ string) (string, error) {
 					return "", nil
 				},
+				tokenSource: newFakeTokenSource(fakeToken),
 			},
 			project:  fakeProject,
 			credType: credentials.TypeImage,
 			repoURL:  fakeGCRRepoURL,
-			assert: func(t *testing.T, _ *cache.Cache, creds *credentials.Credentials, err error) {
+			assert: func(t *testing.T, _, tokenSourceCache *cache.Cache, creds *credentials.Credentials, err error) {
 				assert.NoError(t, err)
-				assert.Nil(t, creds)
+				assert.NotNil(t, creds)
+				assert.Equal(t, accessTokenUsername, creds.Username)
+				assert.Equal(t, fakeToken, creds.Password)
+
+				// Verify the token source was cached
+				tokenSource, found := tokenSourceCache.Get(tokenCacheKey(fakeProject))
+				assert.True(t, found)
+				ts, ok := tokenSource.(oauth2.TokenSource)
+				assert.True(t, ok)
+				token, err := ts.Token()
+				assert.NoError(t, err)
+				assert.Equal(t, fakeToken, token.AccessToken)
 			},
 		},
 	}
 
 	for _, tt := range testCases {
 		t.Run(tt.name, func(t *testing.T) {
-			if tt.setupCache != nil {
-				tt.setupCache(tt.provider.tokenCache)
+			if tt.setupTokenCache != nil {
+				tt.setupTokenCache(tt.provider.tokenCache)
+			}
+			if tt.setupTokenSourceCache != nil {
+				tt.setupTokenSourceCache(tt.provider.tokenSourceCache)
 			}
-
 			creds, err := tt.provider.GetCredentials(context.Background(), tt.project, tt.credType, tt.repoURL, nil)
-			tt.assert(t, tt.provider.tokenCache, creds, err)
+			tt.assert(t, tt.provider.tokenCache, tt.provider.tokenSourceCache, creds, err)
 		})
 	}
 }
+
+type fakeTokenSource struct {
+	// The token to be returned by the Token() method
+	token string
+}
+
+func newFakeTokenSource(token string) oauth2.TokenSource {
+	return &fakeTokenSource{token: token}
+}
+
+func (f *fakeTokenSource) Token() (*oauth2.Token, error) {
+	return &oauth2.Token{
+		AccessToken: f.token,
+	}, nil
+}
diff --git a/internal/expressions/function/functions_test.go b/internal/expressions/function/functions_test.go
index 73b4f9a2..cc399f7a 100644
--- a/internal/expressions/function/functions_test.go
+++ b/internal/expressions/function/functions_test.go
@@ -5,6 +5,7 @@ import (
 	"testing"
 
 	"github.com/stretchr/testify/assert"
+	corev1 "k8s.io/api/core/v1"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/apimachinery/pkg/runtime"
 	"sigs.k8s.io/controller-runtime/pkg/client"
@@ -637,3 +638,175 @@ func Test_getChart(t *testing.T) {
 		})
 	}
 }
+
+func Test_getConfigMap(t *testing.T) {
+	const testProject = "fake-project"
+	const testConfigMap = "fake-configmap"
+
+	testData := map[string]string{
+		"foo": "bar",
+	}
+
+	scheme := runtime.NewScheme()
+	assert.NoError(t, corev1.AddToScheme(scheme))
+
+	tests := []struct {
+		name       string
+		objects    []client.Object
+		args       []any
+		assertions func(t *testing.T, result any, err error)
+	}{
+		{
+			name: "no arguments",
+			args: []any{},
+			assertions: func(t *testing.T, result any, err error) {
+				assert.ErrorContains(t, err, "expected 1 argument")
+				assert.Nil(t, result)
+			},
+		},
+		{
+			name: "too many arguments",
+			args: []any{testConfigMap, "extra"},
+			assertions: func(t *testing.T, result any, err error) {
+				assert.ErrorContains(t, err, "expected 1 argument")
+				assert.Nil(t, result)
+			},
+		},
+		{
+			name: "invalid argument type",
+			args: []any{123},
+			assertions: func(t *testing.T, result any, err error) {
+				assert.ErrorContains(t, err, "argument must be string")
+				assert.Nil(t, result)
+			},
+		},
+		{
+			name: "ConfigMap not found",
+			args: []any{testConfigMap},
+			assertions: func(t *testing.T, result any, err error) {
+				assert.NoError(t, err)
+				assert.NotNil(t, result)
+				assert.IsType(t, map[string]string{}, result)
+				assert.Empty(t, result)
+			},
+		},
+		{
+			name: "success",
+			objects: []client.Object{
+				&corev1.ConfigMap{
+					ObjectMeta: metav1.ObjectMeta{
+						Namespace: testProject,
+						Name:      testConfigMap,
+					},
+					Data: testData,
+				},
+			},
+			args: []any{testConfigMap},
+			assertions: func(t *testing.T, result any, err error) {
+				assert.NoError(t, err)
+				assert.Equal(t, testData, result)
+			},
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			ctx := context.Background()
+
+			c := fake.NewClientBuilder().
+				WithScheme(scheme).
+				WithObjects(tt.objects...).
+				Build()
+
+			fn := getConfigMap(ctx, c, testProject)
+
+			result, err := fn(tt.args...)
+			tt.assertions(t, result, err)
+		})
+	}
+}
+
+func Test_getSecret(t *testing.T) {
+	const testProject = "fake-project"
+	const testSecret = "fake-secret"
+
+	scheme := runtime.NewScheme()
+	assert.NoError(t, corev1.AddToScheme(scheme))
+
+	tests := []struct {
+		name       string
+		objects    []client.Object
+		args       []any
+		assertions func(t *testing.T, result any, err error)
+	}{
+		{
+			name: "no arguments",
+			args: []any{},
+			assertions: func(t *testing.T, result any, err error) {
+				assert.ErrorContains(t, err, "expected 1 argument")
+				assert.Nil(t, result)
+			},
+		},
+		{
+			name: "too many arguments",
+			args: []any{testSecret, "extra"},
+			assertions: func(t *testing.T, result any, err error) {
+				assert.ErrorContains(t, err, "expected 1 argument")
+				assert.Nil(t, result)
+			},
+		},
+		{
+			name: "invalid argument type",
+			args: []any{123},
+			assertions: func(t *testing.T, result any, err error) {
+				assert.ErrorContains(t, err, "argument must be string")
+				assert.Nil(t, result)
+			},
+		},
+		{
+			name: "Secret not found",
+			args: []any{testSecret},
+			assertions: func(t *testing.T, result any, err error) {
+				assert.NoError(t, err)
+				assert.NotNil(t, result)
+				assert.IsType(t, map[string]string{}, result)
+				assert.Empty(t, result)
+			},
+		},
+		{
+			name: "success",
+			objects: []client.Object{
+				&corev1.Secret{
+					ObjectMeta: metav1.ObjectMeta{
+						Namespace: testProject,
+						Name:      testSecret,
+					},
+					Data: map[string][]byte{
+						"foo": []byte("bar"),
+					},
+				},
+			},
+			args: []any{testSecret},
+			assertions: func(t *testing.T, result any, err error) {
+				assert.NoError(t, err)
+				assert.Equal(t, map[string]string{"foo": "bar"}, result)
+			},
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			ctx := context.Background()
+
+			c := fake.NewClientBuilder().
+				WithScheme(scheme).
+				WithObjects(tt.objects...).
+				Build()
+
+			fn := getSecret(ctx, c, testProject)
+
+			result, err := fn(tt.args...)
+			tt.assertions(t, result, err)
+		})
+	}
+}
diff --git a/internal/indexer/indexer_test.go b/internal/indexer/indexer_test.go
index 7e4ae63b..9d552681 100644
--- a/internal/indexer/indexer_test.go
+++ b/internal/indexer/indexer_test.go
@@ -9,8 +9,8 @@ import (
 	corev1 "k8s.io/api/core/v1"
 	apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
-	"k8s.io/apimachinery/pkg/runtime"
 	"sigs.k8s.io/controller-runtime/pkg/client"
+	"sigs.k8s.io/controller-runtime/pkg/client/fake"
 
 	rbacapi "github.com/akuity/kargo/api/rbac/v1alpha1"
 	kargoapi "github.com/akuity/kargo/api/v1alpha1"
@@ -329,7 +329,7 @@ func TestRunningPromotionsByArgoCDApplications(t *testing.T) {
 				},
 				Spec: kargoapi.PromotionSpec{
 					Stage: "fake-stage",
-					Vars: []kargoapi.PromotionVariable{
+					Vars: []kargoapi.ExpressionVariable{
 						{
 							Name:  "app",
 							Value: "fake-app-from-var",
@@ -361,7 +361,7 @@ func TestRunningPromotionsByArgoCDApplications(t *testing.T) {
 						},
 						{
 							Uses: "argocd-update",
-							Vars: []kargoapi.PromotionVariable{
+							Vars: []kargoapi.ExpressionVariable{
 								{
 									Name:  "app",
 									Value: "fake-app-from-step-var",
@@ -381,7 +381,7 @@ func TestRunningPromotionsByArgoCDApplications(t *testing.T) {
 						},
 						{
 							Uses: "argocd-update",
-							Vars: []kargoapi.PromotionVariable{
+							Vars: []kargoapi.ExpressionVariable{
 								{
 									Name:  "input",
 									Value: "${{ outputs.composition.name }}",
@@ -449,13 +449,12 @@ func TestRunningPromotionsByArgoCDApplications(t *testing.T) {
 	}
 	for _, testCase := range testCases {
 		t.Run(testCase.name, func(t *testing.T) {
-			scheme := runtime.NewScheme()
-			require.NoError(t, kargoapi.AddToScheme(scheme))
 			require.Equal(
 				t,
 				testCase.expected,
 				RunningPromotionsByArgoCDApplications(
 					context.TODO(),
+					fake.NewClientBuilder().Build(),
 					testCase.shardName,
 				)(testCase.obj),
 			)
diff --git a/internal/kargo/promotion_builder_test.go b/internal/kargo/promotion_builder_test.go
index 8f1a5947..666c6be0 100644
--- a/internal/kargo/promotion_builder_test.go
+++ b/internal/kargo/promotion_builder_test.go
@@ -82,9 +82,12 @@ func TestPromotionBuilder_Build(t *testing.T) {
 					Namespace: "test-project",
 				},
 				Spec: kargoapi.StageSpec{
+					Vars: []kargoapi.ExpressionVariable{
+						{Name: "key0", Value: "value0"},
+					},
 					PromotionTemplate: &kargoapi.PromotionTemplate{
 						Spec: kargoapi.PromotionTemplateSpec{
-							Vars: []kargoapi.PromotionVariable{
+							Vars: []kargoapi.ExpressionVariable{
 								{Name: "key1", Value: "value1"},
 							},
 							Steps: []kargoapi.PromotionStep{
@@ -108,7 +111,11 @@ func TestPromotionBuilder_Build(t *testing.T) {
 				assert.Equal(t, "abc123", promotion.Spec.Freight)
 
 				// Check vars
-				assert.Equal(t, []kargoapi.PromotionVariable{
+				assert.Equal(t, []kargoapi.ExpressionVariable{
+					{
+						Name:  "key0",
+						Value: "value0",
+					},
 					{
 						Name:  "key1",
 						Value: "value1",
@@ -141,7 +148,7 @@ func TestPromotionBuilder_Build(t *testing.T) {
 									Task: &kargoapi.PromotionTaskReference{
 										Name: "test-task",
 									},
-									Vars: []kargoapi.PromotionVariable{
+									Vars: []kargoapi.ExpressionVariable{
 										{Name: "input1", Value: "value1"},
 									},
 								},
@@ -252,7 +259,7 @@ func TestPromotionBuilder_InflateSteps(t *testing.T) {
 							Task: &kargoapi.PromotionTaskReference{
 								Name: "test-task",
 							},
-							Vars: []kargoapi.PromotionVariable{
+							Vars: []kargoapi.ExpressionVariable{
 								{Name: "input1", Value: "value1"},
 							},
 						},
@@ -266,7 +273,7 @@ func TestPromotionBuilder_InflateSteps(t *testing.T) {
 						Namespace: "test-project",
 					},
 					Spec: kargoapi.PromotionTaskSpec{
-						Vars: []kargoapi.PromotionVariable{
+						Vars: []kargoapi.ExpressionVariable{
 							{Name: "input1"},
 						},
 						Steps: []kargoapi.PromotionStep{
@@ -289,7 +296,7 @@ func TestPromotionBuilder_InflateSteps(t *testing.T) {
 				// Check inflated task step
 				assert.Equal(t, "task-step::sub-step", steps[1].As)
 				assert.Equal(t, "other-fake-step", steps[1].Uses)
-				assert.ElementsMatch(t, []kargoapi.PromotionVariable{
+				assert.ElementsMatch(t, []kargoapi.ExpressionVariable{
 					{Name: "input1", Value: "value1"},
 				}, steps[1].Vars)
 			},
@@ -308,7 +315,7 @@ func TestPromotionBuilder_InflateSteps(t *testing.T) {
 							Task: &kargoapi.PromotionTaskReference{
 								Name: "test-task-1",
 							},
-							Vars: []kargoapi.PromotionVariable{
+							Vars: []kargoapi.ExpressionVariable{
 								{Name: "input1", Value: "value1"},
 							},
 						},
@@ -318,7 +325,7 @@ func TestPromotionBuilder_InflateSteps(t *testing.T) {
 								Kind: "ClusterPromotionTask",
 								Name: "test-task-2",
 							},
-							Vars: []kargoapi.PromotionVariable{
+							Vars: []kargoapi.ExpressionVariable{
 								{Name: "input2", Value: "value2"},
 							},
 						},
@@ -332,7 +339,7 @@ func TestPromotionBuilder_InflateSteps(t *testing.T) {
 						Namespace: "test-project",
 					},
 					Spec: kargoapi.PromotionTaskSpec{
-						Vars: []kargoapi.PromotionVariable{
+						Vars: []kargoapi.ExpressionVariable{
 							{Name: "input1"},
 						},
 						Steps: []kargoapi.PromotionStep{
@@ -348,7 +355,7 @@ func TestPromotionBuilder_InflateSteps(t *testing.T) {
 						Name: "test-task-2",
 					},
 					Spec: kargoapi.PromotionTaskSpec{
-						Vars: []kargoapi.PromotionVariable{
+						Vars: []kargoapi.ExpressionVariable{
 							{Name: "input2"},
 						},
 						Steps: []kargoapi.PromotionStep{
@@ -366,13 +373,13 @@ func TestPromotionBuilder_InflateSteps(t *testing.T) {
 
 				assert.Equal(t, "task1::step1", steps[0].As)
 				assert.Equal(t, "fake-step", steps[0].Uses)
-				assert.ElementsMatch(t, []kargoapi.PromotionVariable{
+				assert.ElementsMatch(t, []kargoapi.ExpressionVariable{
 					{Name: "input1", Value: "value1"},
 				}, steps[0].Vars)
 
 				assert.Equal(t, "task2::step2", steps[1].As)
 				assert.Equal(t, "other-fake-step", steps[1].Uses)
-				assert.ElementsMatch(t, []kargoapi.PromotionVariable{
+				assert.ElementsMatch(t, []kargoapi.ExpressionVariable{
 					{Name: "input2", Value: "value2"},
 				}, steps[1].Vars)
 			},
@@ -402,7 +409,7 @@ func TestPromotionBuilder_inflateTaskSteps(t *testing.T) {
 		name       string
 		project    string
 		taskAlias  string
-		promoVars  []kargoapi.PromotionVariable
+		promoVars  []kargoapi.ExpressionVariable
 		taskStep   kargoapi.PromotionStep
 		objects    []client.Object
 		assertions func(*testing.T, []kargoapi.PromotionStep, error)
@@ -439,7 +446,7 @@ func TestPromotionBuilder_inflateTaskSteps(t *testing.T) {
 						Namespace: "test-project",
 					},
 					Spec: kargoapi.PromotionTaskSpec{
-						Vars: []kargoapi.PromotionVariable{
+						Vars: []kargoapi.ExpressionVariable{
 							{Name: "input1"},
 						},
 					},
@@ -454,14 +461,14 @@ func TestPromotionBuilder_inflateTaskSteps(t *testing.T) {
 			name:      "successful task step inflation",
 			project:   "test-project",
 			taskAlias: "task-1",
-			promoVars: []kargoapi.PromotionVariable{
+			promoVars: []kargoapi.ExpressionVariable{
 				{Name: "input3", Value: "value1"},
 			},
 			taskStep: kargoapi.PromotionStep{
 				Task: &kargoapi.PromotionTaskReference{
 					Name: "test-task",
 				},
-				Vars: []kargoapi.PromotionVariable{
+				Vars: []kargoapi.ExpressionVariable{
 					{Name: "input1", Value: "value1"},
 					{Name: "input2", Value: "value2"},
 				},
@@ -473,7 +480,7 @@ func TestPromotionBuilder_inflateTaskSteps(t *testing.T) {
 						Namespace: "test-project",
 					},
 					Spec: kargoapi.PromotionTaskSpec{
-						Vars: []kargoapi.PromotionVariable{
+						Vars: []kargoapi.ExpressionVariable{
 							{Name: "input1"},
 							{Name: "input2", Value: "default2"},
 							{Name: "input3"},
@@ -486,7 +493,7 @@ func TestPromotionBuilder_inflateTaskSteps(t *testing.T) {
 							{
 								As:   "step2",
 								Uses: "other-fake-step",
-								Vars: []kargoapi.PromotionVariable{
+								Vars: []kargoapi.ExpressionVariable{
 									{Name: "input4", Value: "value4"},
 								},
 							},
@@ -500,7 +507,7 @@ func TestPromotionBuilder_inflateTaskSteps(t *testing.T) {
 
 				assert.Equal(t, "task-1::step1", steps[0].As)
 				assert.Equal(t, "fake-step", steps[0].Uses)
-				assert.ElementsMatch(t, []kargoapi.PromotionVariable{
+				assert.ElementsMatch(t, []kargoapi.ExpressionVariable{
 					{Name: "input2", Value: "default2"},
 					{Name: "input1", Value: "value1"},
 					{Name: "input2", Value: "value2"},
@@ -508,7 +515,7 @@ func TestPromotionBuilder_inflateTaskSteps(t *testing.T) {
 
 				assert.Equal(t, "task-1::step2", steps[1].As)
 				assert.Equal(t, "other-fake-step", steps[1].Uses)
-				assert.ElementsMatch(t, []kargoapi.PromotionVariable{
+				assert.ElementsMatch(t, []kargoapi.ExpressionVariable{
 					{Name: "input2", Value: "default2"},
 					{Name: "input1", Value: "value1"},
 					{Name: "input2", Value: "value2"},
@@ -524,7 +531,7 @@ func TestPromotionBuilder_inflateTaskSteps(t *testing.T) {
 				Task: &kargoapi.PromotionTaskReference{
 					Name: "test-task",
 				},
-				Vars: []kargoapi.PromotionVariable{
+				Vars: []kargoapi.ExpressionVariable{
 					{Name: "input1", Value: "value1"},
 				},
 			},
@@ -535,7 +542,7 @@ func TestPromotionBuilder_inflateTaskSteps(t *testing.T) {
 						Namespace: "test-project",
 					},
 					Spec: kargoapi.PromotionTaskSpec{
-						Vars: []kargoapi.PromotionVariable{
+						Vars: []kargoapi.ExpressionVariable{
 							{Name: "input1"},
 						},
 						Steps: []kargoapi.PromotionStep{
@@ -566,7 +573,7 @@ func TestPromotionBuilder_inflateTaskSteps(t *testing.T) {
 					Kind: "ClusterPromotionTask",
 					Name: "test-cluster-task",
 				},
-				Vars: []kargoapi.PromotionVariable{
+				Vars: []kargoapi.ExpressionVariable{
 					{Name: "input1", Value: "value1"},
 				},
 			},
@@ -576,7 +583,7 @@ func TestPromotionBuilder_inflateTaskSteps(t *testing.T) {
 						Name: "test-cluster-task",
 					},
 					Spec: kargoapi.PromotionTaskSpec{
-						Vars: []kargoapi.PromotionVariable{
+						Vars: []kargoapi.ExpressionVariable{
 							{Name: "input1"},
 						},
 						Steps: []kargoapi.PromotionStep{
@@ -703,7 +710,7 @@ func TestPromotionBuilder_getTaskSpec(t *testing.T) {
 						Namespace: "test-project",
 					},
 					Spec: kargoapi.PromotionTaskSpec{
-						Vars: []kargoapi.PromotionVariable{
+						Vars: []kargoapi.ExpressionVariable{
 							{Name: "input1", Value: "value1"},
 						},
 					},
@@ -731,7 +738,7 @@ func TestPromotionBuilder_getTaskSpec(t *testing.T) {
 						Name: "test-cluster-task",
 					},
 					Spec: kargoapi.PromotionTaskSpec{
-						Vars: []kargoapi.PromotionVariable{
+						Vars: []kargoapi.ExpressionVariable{
 							{Name: "input1", Value: "value1"},
 						},
 					},
@@ -760,7 +767,7 @@ func TestPromotionBuilder_getTaskSpec(t *testing.T) {
 						Namespace: "test-project",
 					},
 					Spec: kargoapi.PromotionTaskSpec{
-						Vars: []kargoapi.PromotionVariable{
+						Vars: []kargoapi.ExpressionVariable{
 							{Name: "input1", Value: "value1"},
 						},
 					},
@@ -937,66 +944,66 @@ func Test_generatePromotionTaskStepName(t *testing.T) {
 func Test_promotionTaskVarsToStepVars(t *testing.T) {
 	tests := []struct {
 		name       string
-		taskVars   []kargoapi.PromotionVariable
-		promoVars  []kargoapi.PromotionVariable
-		stepVars   []kargoapi.PromotionVariable
-		assertions func(t *testing.T, result []kargoapi.PromotionVariable, err error)
+		taskVars   []kargoapi.ExpressionVariable
+		promoVars  []kargoapi.ExpressionVariable
+		stepVars   []kargoapi.ExpressionVariable
+		assertions func(t *testing.T, result []kargoapi.ExpressionVariable, err error)
 	}{
 		{
 			name:     "nil inputs returns nil map and no error",
 			taskVars: nil,
 			stepVars: nil,
-			assertions: func(t *testing.T, result []kargoapi.PromotionVariable, err error) {
+			assertions: func(t *testing.T, result []kargoapi.ExpressionVariable, err error) {
 				require.NoError(t, err)
 				assert.Nil(t, result)
 			},
 		},
 		{
 			name:     "empty inputs returns nil map and no error",
-			taskVars: []kargoapi.PromotionVariable{},
+			taskVars: []kargoapi.ExpressionVariable{},
 			stepVars: nil,
-			assertions: func(t *testing.T, result []kargoapi.PromotionVariable, err error) {
+			assertions: func(t *testing.T, result []kargoapi.ExpressionVariable, err error) {
 				require.NoError(t, err)
 				assert.Nil(t, result)
 			},
 		},
 		{
 			name: "missing required variable returns error",
-			taskVars: []kargoapi.PromotionVariable{
+			taskVars: []kargoapi.ExpressionVariable{
 				{Name: "input1"},
 			},
-			stepVars: []kargoapi.PromotionVariable{
+			stepVars: []kargoapi.ExpressionVariable{
 				{Name: "input1", Value: ""},
 			},
-			assertions: func(t *testing.T, result []kargoapi.PromotionVariable, err error) {
+			assertions: func(t *testing.T, result []kargoapi.ExpressionVariable, err error) {
 				assert.ErrorContains(t, err, "missing value for variable \"input1\"")
 				assert.Nil(t, result)
 			},
 		},
 		{
 			name: "default value used when config value not provided",
-			taskVars: []kargoapi.PromotionVariable{
+			taskVars: []kargoapi.ExpressionVariable{
 				{Name: "input1", Value: "default1"},
 			},
 			stepVars: nil,
-			assertions: func(t *testing.T, result []kargoapi.PromotionVariable, err error) {
+			assertions: func(t *testing.T, result []kargoapi.ExpressionVariable, err error) {
 				require.NoError(t, err)
-				assert.ElementsMatch(t, []kargoapi.PromotionVariable{
+				assert.ElementsMatch(t, []kargoapi.ExpressionVariable{
 					{Name: "input1", Value: "default1"},
 				}, result)
 			},
 		},
 		{
 			name: "step value appends task default value",
-			taskVars: []kargoapi.PromotionVariable{
+			taskVars: []kargoapi.ExpressionVariable{
 				{Name: "input1", Value: "default1"},
 			},
-			stepVars: []kargoapi.PromotionVariable{
+			stepVars: []kargoapi.ExpressionVariable{
 				{Name: "input1", Value: "override1"},
 			},
-			assertions: func(t *testing.T, result []kargoapi.PromotionVariable, err error) {
+			assertions: func(t *testing.T, result []kargoapi.ExpressionVariable, err error) {
 				require.NoError(t, err)
-				assert.ElementsMatch(t, []kargoapi.PromotionVariable{
+				assert.ElementsMatch(t, []kargoapi.ExpressionVariable{
 					{Name: "input1", Value: "default1"},
 					{Name: "input1", Value: "override1"},
 				}, result)
@@ -1004,14 +1011,14 @@ func Test_promotionTaskVarsToStepVars(t *testing.T) {
 		},
 		{
 			name: "promotion variable overrides default value",
-			taskVars: []kargoapi.PromotionVariable{
+			taskVars: []kargoapi.ExpressionVariable{
 				{Name: "input1", Value: "default1"},
 			},
-			promoVars: []kargoapi.PromotionVariable{
+			promoVars: []kargoapi.ExpressionVariable{
 				{Name: "input1", Value: "override1"},
 			},
 			stepVars: nil,
-			assertions: func(t *testing.T, result []kargoapi.PromotionVariable, err error) {
+			assertions: func(t *testing.T, result []kargoapi.ExpressionVariable, err error) {
 				require.NoError(t, err)
 				// Variable is set by engine at runtime
 				assert.Empty(t, result)
@@ -1019,18 +1026,18 @@ func Test_promotionTaskVarsToStepVars(t *testing.T) {
 		},
 		{
 			name: "multiple inputs processed correctly",
-			taskVars: []kargoapi.PromotionVariable{
+			taskVars: []kargoapi.ExpressionVariable{
 				{Name: "input1", Value: "default1"},
 				{Name: "input2", Value: "default2"},
 				{Name: "input3"},
 			},
-			stepVars: []kargoapi.PromotionVariable{
+			stepVars: []kargoapi.ExpressionVariable{
 				{Name: "input1", Value: "override1"},
 				{Name: "input3", Value: "value3"},
 			},
-			assertions: func(t *testing.T, result []kargoapi.PromotionVariable, err error) {
+			assertions: func(t *testing.T, result []kargoapi.ExpressionVariable, err error) {
 				require.NoError(t, err)
-				assert.ElementsMatch(t, []kargoapi.PromotionVariable{
+				assert.ElementsMatch(t, []kargoapi.ExpressionVariable{
 					{Name: "input1", Value: "default1"},
 					{Name: "input2", Value: "default2"},
 					{Name: "input1", Value: "override1"},
diff --git a/internal/promotion/mock_step_runner_test.go b/internal/promotion/mock_step_runner.go
similarity index 63%
rename from internal/promotion/mock_step_runner_test.go
rename to internal/promotion/mock_step_runner.go
index ae8cd745..6edcfd0f 100644
--- a/internal/promotion/mock_step_runner_test.go
+++ b/internal/promotion/mock_step_runner.go
@@ -2,6 +2,7 @@ package promotion
 
 import (
 	"context"
+	"time"
 
 	"github.com/akuity/kargo/pkg/promotion"
 )
@@ -37,3 +38,29 @@ func (m *mockStepRunner) Run(
 	}
 	return m.runResult, m.runErr
 }
+
+type MockRetryableStepRunner struct {
+	*mockStepRunner
+	defaultTimeout        *time.Duration
+	defaultErrorThreshold uint32
+}
+
+func NewMockRetryableStepRunner(
+	name string,
+	defaultTimeout *time.Duration,
+	defaultErrThreshold uint32,
+) MockRetryableStepRunner {
+	return MockRetryableStepRunner{
+		mockStepRunner:        &mockStepRunner{name: name},
+		defaultTimeout:        defaultTimeout,
+		defaultErrorThreshold: defaultErrThreshold,
+	}
+}
+
+func (m MockRetryableStepRunner) DefaultTimeout() *time.Duration {
+	return m.defaultTimeout
+}
+
+func (m MockRetryableStepRunner) DefaultErrorThreshold() uint32 {
+	return m.defaultErrorThreshold
+}
diff --git a/internal/promotion/promotion_test.go b/internal/promotion/promotion_test.go
index fbf669a0..e32c7b65 100644
--- a/internal/promotion/promotion_test.go
+++ b/internal/promotion/promotion_test.go
@@ -17,20 +17,6 @@ import (
 	"github.com/akuity/kargo/pkg/promotion"
 )
 
-type mockRetryableStepRunner struct {
-	*mockStepRunner
-	defaultTimeout        *time.Duration
-	defaultErrorThreshold uint32
-}
-
-func (m mockRetryableStepRunner) DefaultTimeout() *time.Duration {
-	return m.defaultTimeout
-}
-
-func (m mockRetryableStepRunner) DefaultErrorThreshold() uint32 {
-	return m.defaultErrorThreshold
-}
-
 func TestStep_GetTimeout(t *testing.T) {
 	tests := []struct {
 		name       string
@@ -70,7 +56,7 @@ func TestStep_GetTimeout(t *testing.T) {
 					},
 				},
 			},
-			runner: mockRetryableStepRunner{defaultTimeout: ptr.To(time.Duration(3))},
+			runner: MockRetryableStepRunner{defaultTimeout: ptr.To(time.Duration(3))},
 			assertions: func(t *testing.T, result *time.Duration) {
 				assert.Equal(t, ptr.To(time.Duration(5)), result)
 			},
@@ -80,7 +66,7 @@ func TestStep_GetTimeout(t *testing.T) {
 			step: &Step{
 				Retry: &kargoapi.PromotionStepRetry{},
 			},
-			runner: mockRetryableStepRunner{defaultTimeout: ptr.To(time.Duration(3))},
+			runner: MockRetryableStepRunner{defaultTimeout: ptr.To(time.Duration(3))},
 			assertions: func(t *testing.T, result *time.Duration) {
 				assert.Equal(t, ptr.To(time.Duration(3)), result)
 			},
@@ -97,8 +83,8 @@ func TestStep_GetTimeout(t *testing.T) {
 
 func TestStep_GetConfig(t *testing.T) {
 	testScheme := k8sruntime.NewScheme()
-	err := kargoapi.AddToScheme(testScheme)
-	require.NoError(t, err)
+	require.NoError(t, kargoapi.AddToScheme(testScheme))
+
 	testClient := fake.NewClientBuilder().WithScheme(testScheme).WithObjects(
 		&kargoapi.Warehouse{
 			ObjectMeta: v1.ObjectMeta{
@@ -194,7 +180,7 @@ func TestStep_GetConfig(t *testing.T) {
 			name: "test vars with literal values",
 			// Test that vars can be assigned literal values
 			promoCtx: Context{
-				Vars: []kargoapi.PromotionVariable{
+				Vars: []kargoapi.ExpressionVariable{
 					{
 						Name:  "strVar",
 						Value: "foo",
@@ -224,7 +210,7 @@ func TestStep_GetConfig(t *testing.T) {
 			name: "test vars with expressions",
 			// Test using expressions to define the value of vars
 			promoCtx: Context{
-				Vars: []kargoapi.PromotionVariable{
+				Vars: []kargoapi.ExpressionVariable{
 					{
 						Name:  "strVar",
 						Value: "${{ 'f' + 'o' + 'o' }}",
@@ -274,7 +260,7 @@ func TestStep_GetConfig(t *testing.T) {
 			// Test that the warehouse() function can be used to reference freight
 			// origins
 			promoCtx: Context{
-				Vars: []kargoapi.PromotionVariable{{
+				Vars: []kargoapi.ExpressionVariable{{
 					Name:  "warehouseName",
 					Value: "fake-warehouse",
 				}},
@@ -322,7 +308,7 @@ func TestStep_GetConfig(t *testing.T) {
 						},
 					},
 				},
-				Vars: []kargoapi.PromotionVariable{
+				Vars: []kargoapi.ExpressionVariable{
 					{
 						Name:  "warehouseName",
 						Value: "fake-warehouse",
@@ -377,7 +363,7 @@ func TestStep_GetConfig(t *testing.T) {
 						},
 					},
 				},
-				Vars: []kargoapi.PromotionVariable{
+				Vars: []kargoapi.ExpressionVariable{
 					{
 						Name:  "warehouseName",
 						Value: "fake-warehouse",
@@ -439,7 +425,7 @@ func TestStep_GetConfig(t *testing.T) {
 						},
 					},
 				},
-				Vars: []kargoapi.PromotionVariable{
+				Vars: []kargoapi.ExpressionVariable{
 					{
 						Name:  "warehouseName",
 						Value: "fake-warehouse",
@@ -502,6 +488,384 @@ func TestStep_GetConfig(t *testing.T) {
 		})
 	}
 }
+
+func TestStep_GetVars(t *testing.T) {
+	testScheme := k8sruntime.NewScheme()
+	require.NoError(t, kargoapi.AddToScheme(testScheme))
+
+	testClient := fake.NewClientBuilder().WithScheme(testScheme).WithObjects(
+		&kargoapi.Warehouse{
+			ObjectMeta: v1.ObjectMeta{
+				Name:      "fake-warehouse",
+				Namespace: "fake-project",
+			},
+			Spec: kargoapi.WarehouseSpec{
+				Subscriptions: []kargoapi.RepoSubscription{
+					{
+						Git: &kargoapi.GitSubscription{
+							RepoURL: "https://fake-git-repo",
+						},
+					},
+				},
+			},
+		},
+	).Build()
+
+	testCases := []struct {
+		name         string
+		promoCtx     Context
+		promoState   promotion.State
+		step         Step
+		expectedVars map[string]any
+		expectErr    bool
+	}{
+		{
+			name: "global vars with literal values",
+			promoCtx: Context{
+				Project: "fake-project",
+				Vars: []kargoapi.ExpressionVariable{
+					{
+						Name:  "str",
+						Value: "foo",
+					},
+					{
+						Name:  "bool",
+						Value: "true",
+					},
+					{
+						Name:  "num",
+						Value: "42",
+					},
+				},
+			},
+			step: Step{},
+			expectedVars: map[string]any{
+				"str":  "foo",
+				"bool": "true",
+				"num":  "42",
+			},
+		},
+		{
+			name: "global vars with expressions",
+			promoCtx: Context{
+				Project: "fake-project",
+				Vars: []kargoapi.ExpressionVariable{
+					{
+						Name:  "str",
+						Value: "${{ 'f' + 'o' + 'o' }}",
+					},
+					{
+						Name:  "bool",
+						Value: "${{ 1 == 1 }}",
+					},
+					{
+						Name:  "num",
+						Value: "${{ 40 + 2 }}",
+					},
+				},
+			},
+			step: Step{},
+			expectedVars: map[string]any{
+				"str":  "foo",
+				"bool": true,
+				"num":  float64(42),
+			},
+		},
+		{
+			name: "step vars with literal values",
+			promoCtx: Context{
+				Project: "fake-project",
+			},
+			step: Step{
+				Vars: []kargoapi.ExpressionVariable{
+					{
+						Name:  "str",
+						Value: "foo",
+					},
+					{
+						Name:  "bool",
+						Value: "true",
+					},
+					{
+						Name:  "num",
+						Value: "42",
+					},
+				},
+			},
+			expectedVars: map[string]any{
+				"str":  "foo",
+				"bool": "true",
+				"num":  "42",
+			},
+		},
+		{
+			name: "step vars with expressions",
+			promoCtx: Context{
+				Project: "fake-project",
+			},
+			step: Step{
+				Vars: []kargoapi.ExpressionVariable{
+					{
+						Name:  "str",
+						Value: "${{ 'f' + 'o' + 'o' }}",
+					},
+					{
+						Name:  "bool",
+						Value: "${{ 1 == 1 }}",
+					},
+					{
+						Name:  "num",
+						Value: "${{ 40 + 2 }}",
+					},
+				},
+			},
+			expectedVars: map[string]any{
+				"str":  "foo",
+				"bool": true,
+				"num":  float64(42),
+			},
+		},
+		{
+			name: "step vars referencing global vars",
+			promoCtx: Context{
+				Project: "fake-project",
+				Vars: []kargoapi.ExpressionVariable{
+					{
+						Name:  "global",
+						Value: "global-value",
+					},
+				},
+			},
+			step: Step{
+				Vars: []kargoapi.ExpressionVariable{
+					{
+						Name:  "local",
+						Value: "${{ vars.global + '-suffix' }}",
+					},
+				},
+			},
+			expectedVars: map[string]any{
+				"global": "global-value",
+				"local":  "global-value-suffix",
+			},
+		},
+		{
+			name: "step vars referencing other step vars",
+			promoCtx: Context{
+				Project: "fake-project",
+			},
+			step: Step{
+				Vars: []kargoapi.ExpressionVariable{
+					{
+						Name:  "v1",
+						Value: "value1",
+					},
+					{
+						Name:  "v2",
+						Value: "${{ vars.v1 + '-suffix' }}",
+					},
+				},
+			},
+			expectedVars: map[string]any{
+				"v1": "value1",
+				"v2": "value1-suffix",
+			},
+		},
+		{
+			name: "step vars referencing outputs",
+			promoCtx: Context{
+				Project: "fake-project",
+			},
+			promoState: promotion.State{
+				"output1": "output-value",
+			},
+			step: Step{
+				Vars: []kargoapi.ExpressionVariable{
+					{
+						Name:  "fromOutput",
+						Value: "${{ outputs.output1 + '-suffix' }}",
+					},
+				},
+			},
+			expectedVars: map[string]any{
+				"fromOutput": "output-value-suffix",
+			},
+		},
+		{
+			name: "step vars referencing task outputs",
+			promoCtx: Context{
+				Project: "fake-project",
+			},
+			promoState: promotion.State{
+				"task::alias": map[string]any{
+					"foo": "baz",
+				},
+			},
+			step: Step{
+				Alias: "task::other-alias",
+				Vars: []kargoapi.ExpressionVariable{
+					{
+						Name:  "fromTask",
+						Value: "${{ task.outputs.alias.foo + '-suffix' }}",
+					},
+				},
+			},
+			expectedVars: map[string]any{
+				"fromTask": "baz-suffix",
+			},
+		},
+		{
+			name: "invalid expression in global var",
+			promoCtx: Context{
+				Project: "fake-project",
+				Vars: []kargoapi.ExpressionVariable{
+					{
+						Name:  "invalid",
+						Value: "${{ invalid.expression }}",
+					},
+				},
+			},
+			step:      Step{},
+			expectErr: true,
+		},
+		{
+			name: "invalid expression in step var",
+			promoCtx: Context{
+				Project: "fake-project",
+			},
+			step: Step{
+				Vars: []kargoapi.ExpressionVariable{
+					{
+						Name:  "invalid",
+						Value: "${{ invalid.expression }}",
+					},
+				},
+			},
+			expectErr: true,
+		},
+		{
+			name: "warehouse function in vars",
+			promoCtx: Context{
+				Project: "fake-project",
+				FreightRequests: []kargoapi.FreightRequest{{
+					Origin: kargoapi.FreightOrigin{
+						Kind: kargoapi.FreightOriginKindWarehouse,
+						Name: "fake-warehouse",
+					},
+				}},
+			},
+			step: Step{
+				Vars: []kargoapi.ExpressionVariable{
+					{
+						Name:  "wh",
+						Value: "${{ warehouse('fake-warehouse') }}",
+					},
+				},
+			},
+			expectedVars: map[string]any{
+				"wh": map[string]any{
+					"kind": string(kargoapi.FreightOriginKindWarehouse),
+					"name": "fake-warehouse",
+				},
+			},
+		},
+		{
+			name: "context properties in vars",
+			promoCtx: Context{
+				Project:   "fake-project",
+				Stage:     "fake-stage",
+				Promotion: "fake-promotion",
+				Actor:     "fake-creator",
+			},
+			step: Step{
+				Vars: []kargoapi.ExpressionVariable{
+					{
+						Name:  "proj",
+						Value: "${{ ctx.project }}",
+					},
+					{
+						Name:  "stage",
+						Value: "${{ ctx.stage }}",
+					},
+					{
+						Name:  "promo",
+						Value: "${{ ctx.promotion }}",
+					},
+					{
+						Name:  "actor",
+						Value: "${{ ctx.meta.promotion.actor }}",
+					},
+				},
+			},
+			expectedVars: map[string]any{
+				"proj":  "fake-project",
+				"stage": "fake-stage",
+				"promo": "fake-promotion",
+				"actor": "fake-creator",
+			},
+		},
+		{
+			name: "freight functions in vars",
+			promoCtx: Context{
+				Project: "fake-project",
+				FreightRequests: []kargoapi.FreightRequest{{
+					Origin: kargoapi.FreightOrigin{
+						Kind: kargoapi.FreightOriginKindWarehouse,
+						Name: "fake-warehouse",
+					},
+					Sources: kargoapi.FreightSources{
+						Direct: true,
+					},
+				}},
+				Freight: kargoapi.FreightCollection{
+					Freight: map[string]kargoapi.FreightReference{
+						"Warehouse/fake-warehouse": {
+							Origin: kargoapi.FreightOrigin{
+								Kind: kargoapi.FreightOriginKindWarehouse,
+								Name: "fake-warehouse",
+							},
+							Commits: []kargoapi.GitCommit{{
+								RepoURL: "https://fake-git-repo",
+								ID:      "fake-commit-id",
+							}},
+						},
+					},
+				},
+			},
+			step: Step{
+				Vars: []kargoapi.ExpressionVariable{
+					{
+						Name:  "commit",
+						Value: "${{ commitFrom('https://fake-git-repo').ID }}",
+					},
+				},
+			},
+			expectedVars: map[string]any{
+				"commit": "fake-commit-id",
+			},
+		},
+	}
+
+	for _, testCase := range testCases {
+		t.Run(testCase.name, func(t *testing.T) {
+			vars, err := testCase.step.GetVars(
+				context.Background(),
+				testClient,
+				testCase.promoCtx,
+				testCase.promoState,
+			)
+
+			if testCase.expectErr {
+				require.Error(t, err)
+				return
+			}
+
+			require.NoError(t, err)
+			require.Equal(t, testCase.expectedVars, vars)
+		})
+	}
+}
+
 func TestStep_Skip(t *testing.T) {
 	tests := []struct {
 		name       string
@@ -524,7 +888,7 @@ func TestStep_Skip(t *testing.T) {
 				If: "${{ vars.foo == 'bar' }}",
 			},
 			ctx: Context{
-				Vars: []kargoapi.PromotionVariable{
+				Vars: []kargoapi.ExpressionVariable{
 					{
 						Name:  "foo",
 						Value: "bar",
@@ -565,6 +929,33 @@ func TestStep_Skip(t *testing.T) {
 				assert.True(t, b)
 			},
 		},
+		{
+			name: "if condition uses expression function",
+			step: &Step{
+				If: "${{ commitFrom('https://git.example.com', warehouse('fake-warehouse')).ID == 'foo' }}",
+			},
+			ctx: Context{
+				Project: "fake-project",
+				Freight: kargoapi.FreightCollection{
+					Freight: map[string]kargoapi.FreightReference{
+						"Warehouse/fake-warehouse": {
+							Origin: kargoapi.FreightOrigin{
+								Kind: kargoapi.FreightOriginKindWarehouse,
+								Name: "fake-warehouse",
+							},
+							Commits: []kargoapi.GitCommit{{
+								RepoURL: "https://git.example.com",
+								ID:      "foo",
+							}},
+						},
+					},
+				},
+			},
+			assertions: func(t *testing.T, b bool, err error) {
+				assert.NoError(t, err)
+				assert.False(t, b)
+			},
+		},
 		{
 			name: "if condition does not evaluate to a boolean",
 			step: &Step{
@@ -578,7 +969,12 @@ func TestStep_Skip(t *testing.T) {
 	}
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
-			got, err := tt.step.Skip(tt.ctx, tt.state)
+			got, err := tt.step.Skip(
+				context.Background(),
+				fake.NewClientBuilder().Build(),
+				tt.ctx,
+				tt.state,
+			)
 			tt.assertions(t, got, err)
 		})
 	}
diff --git a/internal/promotion/runner/builtin/helm_template_runner_test.go b/internal/promotion/runner/builtin/helm_template_runner_test.go
index 924f2a88..cbd6819a 100644
--- a/internal/promotion/runner/builtin/helm_template_runner_test.go
+++ b/internal/promotion/runner/builtin/helm_template_runner_test.go
@@ -107,6 +107,53 @@ data:
 # Source: test-chart/templates/test.yaml
 apiVersion: v1
 kind: ConfigMap
+metadata:
+  name: test-release-configmap
+  namespace: test-namespace
+data:
+  value1: value1
+  value2: value2
+`, string(content))
+			},
+		},
+		{
+			name: "successful run with set values",
+			files: map[string]string{
+				"values.yaml": "key1: value1",
+				"charts/test-chart/Chart.yaml": `apiVersion: v1
+name: test-chart
+version: 0.1.0`,
+				"charts/test-chart/templates/test.yaml": `---
+apiVersion: v1
+kind: ConfigMap
+metadata:
+  name: {{ .Release.Name }}-configmap
+  namespace: {{ .Release.Namespace }}
+data:
+  value1: {{ .Values.key1 }}
+  value2: {{ .Values.key2 }}
+`,
+			},
+			cfg: builtin.HelmTemplateConfig{
+				Path:        "charts/test-chart",
+				ValuesFiles: []string{"values.yaml"},
+				SetValues:   []builtin.SetValues{{Key: "key2", Value: "value2"}},
+				OutPath:     "output.yaml",
+				ReleaseName: "test-release",
+				Namespace:   "test-namespace",
+			},
+			assertions: func(t *testing.T, workDir string, result promotion.StepResult, err error) {
+				require.NoError(t, err)
+				assert.Equal(t, promotion.StepResult{Status: kargoapi.PromotionPhaseSucceeded}, result)
+
+				outPath := filepath.Join(workDir, "output.yaml")
+				require.FileExists(t, outPath)
+				content, err := os.ReadFile(outPath)
+				require.NoError(t, err)
+				assert.Equal(t, `---
+# Source: test-chart/templates/test.yaml
+apiVersion: v1
+kind: ConfigMap
 metadata:
   name: test-release-configmap
   namespace: test-namespace
@@ -302,19 +349,56 @@ func Test_helmTemplateRunner_composeValues(t *testing.T) {
 	tests := []struct {
 		name           string
 		workDir        string
-		valuesFiles    []string
+		cfg            builtin.HelmTemplateConfig
 		valuesContents map[string]string
 		assertions     func(*testing.T, map[string]any, error)
 	}{
 		{
-			name:        "successful composition",
-			workDir:     t.TempDir(),
-			valuesFiles: []string{"values1.yaml", "values2.yaml"},
+			name:    "successful composition",
+			workDir: t.TempDir(),
+			cfg: builtin.HelmTemplateConfig{
+				ValuesFiles: []string{"values1.yaml", "values2.yaml"},
+				SetValues:   []builtin.SetValues{{Key: "key3", Value: "value3"}},
+			},
 			valuesContents: map[string]string{
 				"values1.yaml": "key1: value1",
 				"values2.yaml": "key2: value2",
 			},
 			assertions: func(t *testing.T, result map[string]any, err error) {
+				assert.NoError(t, err)
+				assert.Equal(t, map[string]any{
+					"key1": "value1",
+					"key2": "value2",
+					"key3": "value3",
+				}, result)
+			},
+		},
+		{
+			name:    "successful composition with no files",
+			workDir: t.TempDir(),
+			cfg: builtin.HelmTemplateConfig{
+				ValuesFiles: []string{},
+				SetValues:   []builtin.SetValues{{Key: "key3", Value: "value3"}},
+			},
+			valuesContents: map[string]string{},
+			assertions: func(t *testing.T, result map[string]any, err error) {
+				assert.NoError(t, err)
+				assert.Equal(t, map[string]any{
+					"key3": "value3",
+				}, result)
+			},
+		},
+		{
+			name:    "successful composition with no set values",
+			workDir: t.TempDir(),
+			cfg: builtin.HelmTemplateConfig{
+				ValuesFiles: []string{"values1.yaml", "values2.yaml"},
+				SetValues:   []builtin.SetValues{},
+			},
+			valuesContents: map[string]string{
+				"values1.yaml": "key1: value1",
+				"values2.yaml": "key2: value2",
+			}, assertions: func(t *testing.T, result map[string]any, err error) {
 				assert.NoError(t, err)
 				assert.Equal(t, map[string]any{
 					"key1": "value1",
@@ -323,9 +407,12 @@ func Test_helmTemplateRunner_composeValues(t *testing.T) {
 			},
 		},
 		{
-			name:        "file not found",
-			workDir:     t.TempDir(),
-			valuesFiles: []string{"non_existent.yaml"},
+			name:    "file not found",
+			workDir: t.TempDir(),
+			cfg: builtin.HelmTemplateConfig{
+				ValuesFiles: []string{"non_existent.yaml"},
+				SetValues:   []builtin.SetValues{},
+			},
 			assertions: func(t *testing.T, result map[string]any, err error) {
 				assert.Error(t, err)
 				assert.Nil(t, result)
@@ -340,7 +427,7 @@ func Test_helmTemplateRunner_composeValues(t *testing.T) {
 			for p, c := range tt.valuesContents {
 				require.NoError(t, os.WriteFile(filepath.Join(tt.workDir, p), []byte(c), 0o600))
 			}
-			result, err := runner.composeValues(tt.workDir, tt.valuesFiles)
+			result, err := runner.composeValues(tt.workDir, tt.cfg)
 			tt.assertions(t, result, err)
 		})
 	}
diff --git a/internal/rollouts/analysis_run_builder_test.go b/internal/rollouts/analysis_run_builder_test.go
index 852536d1..bf7fb3b0 100644
--- a/internal/rollouts/analysis_run_builder_test.go
+++ b/internal/rollouts/analysis_run_builder_test.go
@@ -624,6 +624,60 @@ func TestAnalysisRunBuilder_buildArgs(t *testing.T) {
 				require.ErrorContains(t, err, "evaluate argument \"param1\"")
 			},
 		},
+		{
+			name: "evaluate variable in argument",
+			template: &rolloutsapi.AnalysisTemplate{
+				Spec: rolloutsapi.AnalysisTemplateSpec{
+					Args: []rolloutsapi.Argument{
+						{Name: "param1"},
+					},
+				},
+			},
+			args: []kargoapi.AnalysisRunArgument{
+				{Name: "param1", Value: "${{ vars.test }}"},
+			},
+			exprCfg: &ArgumentEvaluationConfig{
+				Vars: []ArgumentVariable{
+					{Name: "test", Value: "value1"},
+					// Ensure overriding and expression evaluation works.
+					{Name: "test", Value: "${{ vars.test + '-suffix'}}"},
+				},
+			},
+			assertions: func(t *testing.T, args []rolloutsapi.Argument, err error) {
+				require.NoError(t, err)
+				assert.Len(t, args, 1)
+				assert.Equal(t, "value1-suffix", *args[0].Value)
+			},
+		},
+		{
+			name:     "variable evaluation error",
+			template: &rolloutsapi.AnalysisTemplate{},
+			exprCfg: &ArgumentEvaluationConfig{
+				Vars: []ArgumentVariable{
+					{Name: "test", Value: "${{ ctx.test }}"},
+				},
+			},
+			assertions: func(t *testing.T, _ []rolloutsapi.Argument, err error) {
+				require.ErrorContains(t, err, "evaluate variable \"test\"")
+			},
+		},
+		{
+			name:     "existing vars in env",
+			template: &rolloutsapi.AnalysisTemplate{},
+			exprCfg: &ArgumentEvaluationConfig{
+				Env: map[string]any{
+					"vars": map[string]string{
+						"test": "value1",
+					},
+				},
+				Vars: []ArgumentVariable{
+					{Name: "test", Value: "value2"},
+				},
+			},
+			assertions: func(t *testing.T, _ []rolloutsapi.Argument, err error) {
+				require.ErrorContains(t, err, "vars in argument evaluation config env is of unexpected type")
+			},
+		},
 	}
 
 	for _, tt := range tests {
diff --git a/internal/server/option/auth_test.go b/internal/server/option/auth_test.go
index 8958f3ec..130c4898 100644
--- a/internal/server/option/auth_test.go
+++ b/internal/server/option/auth_test.go
@@ -308,7 +308,8 @@ func TestAuthenticate(t *testing.T) {
 			authInterceptor: &authInterceptor{
 				cfg: config.ServerConfig{
 					OIDCConfig: &libOIDC.Config{
-						IssuerURL: testIDPIssuer,
+						IssuerURL:     testIDPIssuer,
+						UsernameClaim: "preferred_username",
 					},
 				},
 				parseUnverifiedJWTFn: func(_ string, claims jwt.Claims) (*jwt.Token, []string, error) {
@@ -322,8 +323,9 @@ func TestAuthenticate(t *testing.T) {
 					string,
 				) (claims, error) {
 					return claims{
-						"sub":   "ironman",
-						"email": "tony@starkindustries.com",
+						"preferred_username": "foo",
+						"sub":                "ironman",
+						"email":              "tony@starkindustries.com",
 						"groups": []string{
 							"avengers",
 							"shield",
@@ -345,6 +347,7 @@ func TestAuthenticate(t *testing.T) {
 				u, ok := user.InfoFromContext(ctx)
 				require.True(t, ok)
 				require.False(t, u.IsAdmin)
+				require.Equal(t, u.Username, "foo")
 				require.Equal(t, "ironman", u.Claims["sub"])
 				require.Equal(t, "tony@starkindustries.com", u.Claims["email"])
 				require.Equal(t, []string{"avengers", "shield"}, u.Claims["groups"])
diff --git a/internal/server/refresh_warehouse_v1alpha1_test.go b/internal/server/refresh_warehouse_v1alpha1_test.go
new file mode 100644
index 00000000..bd975568
--- /dev/null
+++ b/internal/server/refresh_warehouse_v1alpha1_test.go
@@ -0,0 +1,130 @@
+package server
+
+import (
+	"context"
+	"testing"
+	"time"
+
+	"connectrpc.com/connect"
+	"github.com/stretchr/testify/require"
+	corev1 "k8s.io/api/core/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/runtime"
+	"k8s.io/client-go/rest"
+	"sigs.k8s.io/controller-runtime/pkg/client"
+	"sigs.k8s.io/controller-runtime/pkg/client/fake"
+
+	svcv1alpha1 "github.com/akuity/kargo/api/service/v1alpha1"
+	kargoapi "github.com/akuity/kargo/api/v1alpha1"
+	"github.com/akuity/kargo/internal/server/kubernetes"
+	"github.com/akuity/kargo/internal/server/validation"
+)
+
+func TestRefreshWarehouse(t *testing.T) {
+	testSets := map[string]struct {
+		req          *svcv1alpha1.RefreshWarehouseRequest
+		errExpected  bool
+		expectedCode connect.Code
+	}{
+		"empty project": {
+			req: &svcv1alpha1.RefreshWarehouseRequest{
+				Project: "",
+				Name:    "",
+			},
+			errExpected:  true,
+			expectedCode: connect.CodeInvalidArgument,
+		},
+		"empty name": {
+			req: &svcv1alpha1.RefreshWarehouseRequest{
+				Project: "kargo-demo",
+				Name:    "",
+			},
+			errExpected:  true,
+			expectedCode: connect.CodeInvalidArgument,
+		},
+		"non-existing project": {
+			req: &svcv1alpha1.RefreshWarehouseRequest{
+				Project: "kargo-x",
+				Name:    "test",
+			},
+			errExpected:  true,
+			expectedCode: connect.CodeNotFound,
+		},
+		"non-existing warehouse": {
+			req: &svcv1alpha1.RefreshWarehouseRequest{
+				Project: "non-existing-project",
+				Name:    "test",
+			},
+			errExpected:  true,
+			expectedCode: connect.CodeNotFound,
+		},
+		"existing warehouse": {
+			req: &svcv1alpha1.RefreshWarehouseRequest{
+				Project: "kargo-demo",
+				Name:    "test",
+			},
+		},
+	}
+	for name, ts := range testSets {
+		t.Run(name, func(t *testing.T) {
+			t.Parallel()
+
+			ctx := context.Background()
+
+			client, err := kubernetes.NewClient(
+				ctx,
+				&rest.Config{},
+				kubernetes.ClientOptions{
+					SkipAuthorization: true,
+					NewInternalClient: func(
+						_ context.Context,
+						_ *rest.Config,
+						scheme *runtime.Scheme,
+					) (client.Client, error) {
+						return fake.NewClientBuilder().
+							WithScheme(scheme).
+							WithObjects(
+								mustNewObject[corev1.Namespace]("testdata/namespace.yaml"),
+								mustNewObject[kargoapi.Stage]("testdata/stage.yaml"),
+							).
+							Build(), nil
+					},
+				},
+			)
+			require.NoError(t, err)
+
+			if !ts.errExpected {
+				err = client.Create(ctx, &kargoapi.Warehouse{
+					ObjectMeta: metav1.ObjectMeta{
+						Namespace: ts.req.GetProject(),
+						Name:      ts.req.GetName(),
+					},
+					Spec: kargoapi.WarehouseSpec{},
+				})
+				require.NoError(t, err)
+			}
+
+			svr := &server{
+				client: client,
+			}
+			svr.externalValidateProjectFn = validation.ValidateProject
+			res, err := svr.RefreshWarehouse(ctx, connect.NewRequest(ts.req))
+			if ts.errExpected {
+				require.Error(t, err)
+				require.Equal(t, ts.expectedCode, connect.CodeOf(err))
+				return
+			}
+
+			require.NoError(t, err)
+			stage := res.Msg.GetWarehouse()
+			annotation := stage.GetAnnotations()[kargoapi.AnnotationKeyRefresh]
+			refreshTime, err := time.Parse(time.RFC3339, annotation)
+			require.NoError(t, err)
+			// Make sure we set timestamp is close to now
+			// Assume it doesn't take 3 seconds to run this unit test.
+			require.WithinDuration(t, time.Now(), refreshTime, 3*time.Second)
+			require.Equal(t, ts.req.GetProject(), stage.Namespace)
+			require.Equal(t, ts.req.GetName(), stage.Name)
+		})
+	}
+}