Repository: akuity/kargo

Issue:
Apply promotion policy to multiple stages
# Proposed Feature

It would be useful to set a promotion policy based on a set of labels and/or naming patterns.

Today, a promotion policy is set against explicit names:

```yaml
apiVersion: kargo.akuity.io/v1alpha1
kind: Project
metadata:
  name: example
spec:
  promotionPolicies:
  - stage: test
    autoPromotionEnabled: true
  - stage: uat
    autoPromotionEnabled: true
```

Instead, we could possibly use selectors:

```yaml
spec:
  promotionPolicies:
  - stageSelector:
      matchLabels:
        autopromote: "true"
      matchExpressions:
        - { key: foo, operator: In, values: [bar] }
        - { key: env, operator: NotIn, values: [prod] }
```

or globs/regex:

```yaml
spec:
  promotionPolicies:
  - autoPromotionEnabled: true
    name: glob:prod-*
```

# Motivation

Currently, all the Stages need to be referened in the Project if you want auto promotion

# Suggested Implementation

NOTE: labels are mutable on resources, but names cannot be changed. It could mean that there is a use case for globs/regex vs. labels. So we might want both.

Discussion:
@krancour: See note about security considerations [here](https://github.com/akuity/kargo/issues/3535#issuecomment-2740719966).

Does _not_ mean we can't do this. I just don't want to lose track of this concern.@krancour: @jessesuen what do you think about just making this a requirement in #3669? It's hard to imagine we'd tackle these two issues separately anyway.

Solution patch:
diff --git a/api/v1alpha1/generated.pb.go b/api/v1alpha1/generated.pb.go
index 4a85c659..e954b3b9 100644
--- a/api/v1alpha1/generated.pb.go
+++ b/api/v1alpha1/generated.pb.go
@@ -1010,15 +1010,15 @@ func (m *Project) XXX_DiscardUnknown() {
 
 var xxx_messageInfo_Project proto.InternalMessageInfo
 
-func (m *ProjectList) Reset()      { *m = ProjectList{} }
-func (*ProjectList) ProtoMessage() {}
-func (*ProjectList) Descriptor() ([]byte, []int) {
+func (m *ProjectConfig) Reset()      { *m = ProjectConfig{} }
+func (*ProjectConfig) ProtoMessage() {}
+func (*ProjectConfig) Descriptor() ([]byte, []int) {
 	return fileDescriptor_e26b7f7bbc391025, []int{35}
 }
-func (m *ProjectList) XXX_Unmarshal(b []byte) error {
+func (m *ProjectConfig) XXX_Unmarshal(b []byte) error {
 	return m.Unmarshal(b)
 }
-func (m *ProjectList) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
+func (m *ProjectConfig) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
 	b = b[:cap(b)]
 	n, err := m.MarshalToSizedBuffer(b)
 	if err != nil {
@@ -1026,27 +1026,83 @@ func (m *ProjectList) XXX_Marshal(b []byte, deterministic bool) ([]byte, error)
 	}
 	return b[:n], nil
 }
-func (m *ProjectList) XXX_Merge(src proto.Message) {
-	xxx_messageInfo_ProjectList.Merge(m, src)
+func (m *ProjectConfig) XXX_Merge(src proto.Message) {
+	xxx_messageInfo_ProjectConfig.Merge(m, src)
 }
-func (m *ProjectList) XXX_Size() int {
+func (m *ProjectConfig) XXX_Size() int {
 	return m.Size()
 }
-func (m *ProjectList) XXX_DiscardUnknown() {
-	xxx_messageInfo_ProjectList.DiscardUnknown(m)
+func (m *ProjectConfig) XXX_DiscardUnknown() {
+	xxx_messageInfo_ProjectConfig.DiscardUnknown(m)
 }
 
-var xxx_messageInfo_ProjectList proto.InternalMessageInfo
+var xxx_messageInfo_ProjectConfig proto.InternalMessageInfo
 
-func (m *ProjectSpec) Reset()      { *m = ProjectSpec{} }
-func (*ProjectSpec) ProtoMessage() {}
-func (*ProjectSpec) Descriptor() ([]byte, []int) {
+func (m *ProjectConfigList) Reset()      { *m = ProjectConfigList{} }
+func (*ProjectConfigList) ProtoMessage() {}
+func (*ProjectConfigList) Descriptor() ([]byte, []int) {
 	return fileDescriptor_e26b7f7bbc391025, []int{36}
 }
-func (m *ProjectSpec) XXX_Unmarshal(b []byte) error {
+func (m *ProjectConfigList) XXX_Unmarshal(b []byte) error {
+	return m.Unmarshal(b)
+}
+func (m *ProjectConfigList) 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 *ProjectConfigList) XXX_Merge(src proto.Message) {
+	xxx_messageInfo_ProjectConfigList.Merge(m, src)
+}
+func (m *ProjectConfigList) XXX_Size() int {
+	return m.Size()
+}
+func (m *ProjectConfigList) XXX_DiscardUnknown() {
+	xxx_messageInfo_ProjectConfigList.DiscardUnknown(m)
+}
+
+var xxx_messageInfo_ProjectConfigList proto.InternalMessageInfo
+
+func (m *ProjectConfigSpec) Reset()      { *m = ProjectConfigSpec{} }
+func (*ProjectConfigSpec) ProtoMessage() {}
+func (*ProjectConfigSpec) Descriptor() ([]byte, []int) {
+	return fileDescriptor_e26b7f7bbc391025, []int{37}
+}
+func (m *ProjectConfigSpec) XXX_Unmarshal(b []byte) error {
+	return m.Unmarshal(b)
+}
+func (m *ProjectConfigSpec) 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 *ProjectConfigSpec) XXX_Merge(src proto.Message) {
+	xxx_messageInfo_ProjectConfigSpec.Merge(m, src)
+}
+func (m *ProjectConfigSpec) XXX_Size() int {
+	return m.Size()
+}
+func (m *ProjectConfigSpec) XXX_DiscardUnknown() {
+	xxx_messageInfo_ProjectConfigSpec.DiscardUnknown(m)
+}
+
+var xxx_messageInfo_ProjectConfigSpec proto.InternalMessageInfo
+
+func (m *ProjectList) Reset()      { *m = ProjectList{} }
+func (*ProjectList) ProtoMessage() {}
+func (*ProjectList) Descriptor() ([]byte, []int) {
+	return fileDescriptor_e26b7f7bbc391025, []int{38}
+}
+func (m *ProjectList) XXX_Unmarshal(b []byte) error {
 	return m.Unmarshal(b)
 }
-func (m *ProjectSpec) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
+func (m *ProjectList) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
 	b = b[:cap(b)]
 	n, err := m.MarshalToSizedBuffer(b)
 	if err != nil {
@@ -1054,22 +1110,22 @@ func (m *ProjectSpec) XXX_Marshal(b []byte, deterministic bool) ([]byte, error)
 	}
 	return b[:n], nil
 }
-func (m *ProjectSpec) XXX_Merge(src proto.Message) {
-	xxx_messageInfo_ProjectSpec.Merge(m, src)
+func (m *ProjectList) XXX_Merge(src proto.Message) {
+	xxx_messageInfo_ProjectList.Merge(m, src)
 }
-func (m *ProjectSpec) XXX_Size() int {
+func (m *ProjectList) XXX_Size() int {
 	return m.Size()
 }
-func (m *ProjectSpec) XXX_DiscardUnknown() {
-	xxx_messageInfo_ProjectSpec.DiscardUnknown(m)
+func (m *ProjectList) XXX_DiscardUnknown() {
+	xxx_messageInfo_ProjectList.DiscardUnknown(m)
 }
 
-var xxx_messageInfo_ProjectSpec proto.InternalMessageInfo
+var xxx_messageInfo_ProjectList proto.InternalMessageInfo
 
 func (m *ProjectStatus) Reset()      { *m = ProjectStatus{} }
 func (*ProjectStatus) ProtoMessage() {}
 func (*ProjectStatus) Descriptor() ([]byte, []int) {
-	return fileDescriptor_e26b7f7bbc391025, []int{37}
+	return fileDescriptor_e26b7f7bbc391025, []int{39}
 }
 func (m *ProjectStatus) XXX_Unmarshal(b []byte) error {
 	return m.Unmarshal(b)
@@ -1097,7 +1153,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{38}
+	return fileDescriptor_e26b7f7bbc391025, []int{40}
 }
 func (m *Promotion) XXX_Unmarshal(b []byte) error {
 	return m.Unmarshal(b)
@@ -1125,7 +1181,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{39}
+	return fileDescriptor_e26b7f7bbc391025, []int{41}
 }
 func (m *PromotionList) XXX_Unmarshal(b []byte) error {
 	return m.Unmarshal(b)
@@ -1153,7 +1209,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{40}
+	return fileDescriptor_e26b7f7bbc391025, []int{42}
 }
 func (m *PromotionPolicy) XXX_Unmarshal(b []byte) error {
 	return m.Unmarshal(b)
@@ -1181,7 +1237,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{41}
+	return fileDescriptor_e26b7f7bbc391025, []int{43}
 }
 func (m *PromotionReference) XXX_Unmarshal(b []byte) error {
 	return m.Unmarshal(b)
@@ -1209,7 +1265,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{42}
+	return fileDescriptor_e26b7f7bbc391025, []int{44}
 }
 func (m *PromotionSpec) XXX_Unmarshal(b []byte) error {
 	return m.Unmarshal(b)
@@ -1237,7 +1293,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{43}
+	return fileDescriptor_e26b7f7bbc391025, []int{45}
 }
 func (m *PromotionStatus) XXX_Unmarshal(b []byte) error {
 	return m.Unmarshal(b)
@@ -1265,7 +1321,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{44}
+	return fileDescriptor_e26b7f7bbc391025, []int{46}
 }
 func (m *PromotionStep) XXX_Unmarshal(b []byte) error {
 	return m.Unmarshal(b)
@@ -1293,7 +1349,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{45}
+	return fileDescriptor_e26b7f7bbc391025, []int{47}
 }
 func (m *PromotionStepRetry) XXX_Unmarshal(b []byte) error {
 	return m.Unmarshal(b)
@@ -1321,7 +1377,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{46}
+	return fileDescriptor_e26b7f7bbc391025, []int{48}
 }
 func (m *PromotionTask) XXX_Unmarshal(b []byte) error {
 	return m.Unmarshal(b)
@@ -1349,7 +1405,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{47}
+	return fileDescriptor_e26b7f7bbc391025, []int{49}
 }
 func (m *PromotionTaskList) XXX_Unmarshal(b []byte) error {
 	return m.Unmarshal(b)
@@ -1377,7 +1433,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{48}
+	return fileDescriptor_e26b7f7bbc391025, []int{50}
 }
 func (m *PromotionTaskReference) XXX_Unmarshal(b []byte) error {
 	return m.Unmarshal(b)
@@ -1405,7 +1461,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{49}
+	return fileDescriptor_e26b7f7bbc391025, []int{51}
 }
 func (m *PromotionTaskSpec) XXX_Unmarshal(b []byte) error {
 	return m.Unmarshal(b)
@@ -1433,7 +1489,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{50}
+	return fileDescriptor_e26b7f7bbc391025, []int{52}
 }
 func (m *PromotionTemplate) XXX_Unmarshal(b []byte) error {
 	return m.Unmarshal(b)
@@ -1461,7 +1517,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{51}
+	return fileDescriptor_e26b7f7bbc391025, []int{53}
 }
 func (m *PromotionTemplateSpec) XXX_Unmarshal(b []byte) error {
 	return m.Unmarshal(b)
@@ -1489,7 +1545,7 @@ var xxx_messageInfo_PromotionTemplateSpec proto.InternalMessageInfo
 func (m *RepoSubscription) Reset()      { *m = RepoSubscription{} }
 func (*RepoSubscription) ProtoMessage() {}
 func (*RepoSubscription) Descriptor() ([]byte, []int) {
-	return fileDescriptor_e26b7f7bbc391025, []int{52}
+	return fileDescriptor_e26b7f7bbc391025, []int{54}
 }
 func (m *RepoSubscription) XXX_Unmarshal(b []byte) error {
 	return m.Unmarshal(b)
@@ -1517,7 +1573,7 @@ var xxx_messageInfo_RepoSubscription proto.InternalMessageInfo
 func (m *Stage) Reset()      { *m = Stage{} }
 func (*Stage) ProtoMessage() {}
 func (*Stage) Descriptor() ([]byte, []int) {
-	return fileDescriptor_e26b7f7bbc391025, []int{53}
+	return fileDescriptor_e26b7f7bbc391025, []int{55}
 }
 func (m *Stage) XXX_Unmarshal(b []byte) error {
 	return m.Unmarshal(b)
@@ -1545,7 +1601,7 @@ var xxx_messageInfo_Stage proto.InternalMessageInfo
 func (m *StageList) Reset()      { *m = StageList{} }
 func (*StageList) ProtoMessage() {}
 func (*StageList) Descriptor() ([]byte, []int) {
-	return fileDescriptor_e26b7f7bbc391025, []int{54}
+	return fileDescriptor_e26b7f7bbc391025, []int{56}
 }
 func (m *StageList) XXX_Unmarshal(b []byte) error {
 	return m.Unmarshal(b)
@@ -1573,7 +1629,7 @@ var xxx_messageInfo_StageList proto.InternalMessageInfo
 func (m *StageSpec) Reset()      { *m = StageSpec{} }
 func (*StageSpec) ProtoMessage() {}
 func (*StageSpec) Descriptor() ([]byte, []int) {
-	return fileDescriptor_e26b7f7bbc391025, []int{55}
+	return fileDescriptor_e26b7f7bbc391025, []int{57}
 }
 func (m *StageSpec) XXX_Unmarshal(b []byte) error {
 	return m.Unmarshal(b)
@@ -1601,7 +1657,7 @@ var xxx_messageInfo_StageSpec proto.InternalMessageInfo
 func (m *StageStatus) Reset()      { *m = StageStatus{} }
 func (*StageStatus) ProtoMessage() {}
 func (*StageStatus) Descriptor() ([]byte, []int) {
-	return fileDescriptor_e26b7f7bbc391025, []int{56}
+	return fileDescriptor_e26b7f7bbc391025, []int{58}
 }
 func (m *StageStatus) XXX_Unmarshal(b []byte) error {
 	return m.Unmarshal(b)
@@ -1629,7 +1685,7 @@ var xxx_messageInfo_StageStatus proto.InternalMessageInfo
 func (m *StepExecutionMetadata) Reset()      { *m = StepExecutionMetadata{} }
 func (*StepExecutionMetadata) ProtoMessage() {}
 func (*StepExecutionMetadata) Descriptor() ([]byte, []int) {
-	return fileDescriptor_e26b7f7bbc391025, []int{57}
+	return fileDescriptor_e26b7f7bbc391025, []int{59}
 }
 func (m *StepExecutionMetadata) XXX_Unmarshal(b []byte) error {
 	return m.Unmarshal(b)
@@ -1657,7 +1713,7 @@ var xxx_messageInfo_StepExecutionMetadata proto.InternalMessageInfo
 func (m *Verification) Reset()      { *m = Verification{} }
 func (*Verification) ProtoMessage() {}
 func (*Verification) Descriptor() ([]byte, []int) {
-	return fileDescriptor_e26b7f7bbc391025, []int{58}
+	return fileDescriptor_e26b7f7bbc391025, []int{60}
 }
 func (m *Verification) XXX_Unmarshal(b []byte) error {
 	return m.Unmarshal(b)
@@ -1685,7 +1741,7 @@ var xxx_messageInfo_Verification proto.InternalMessageInfo
 func (m *VerificationInfo) Reset()      { *m = VerificationInfo{} }
 func (*VerificationInfo) ProtoMessage() {}
 func (*VerificationInfo) Descriptor() ([]byte, []int) {
-	return fileDescriptor_e26b7f7bbc391025, []int{59}
+	return fileDescriptor_e26b7f7bbc391025, []int{61}
 }
 func (m *VerificationInfo) XXX_Unmarshal(b []byte) error {
 	return m.Unmarshal(b)
@@ -1713,7 +1769,7 @@ var xxx_messageInfo_VerificationInfo proto.InternalMessageInfo
 func (m *VerifiedStage) Reset()      { *m = VerifiedStage{} }
 func (*VerifiedStage) ProtoMessage() {}
 func (*VerifiedStage) Descriptor() ([]byte, []int) {
-	return fileDescriptor_e26b7f7bbc391025, []int{60}
+	return fileDescriptor_e26b7f7bbc391025, []int{62}
 }
 func (m *VerifiedStage) XXX_Unmarshal(b []byte) error {
 	return m.Unmarshal(b)
@@ -1741,7 +1797,7 @@ var xxx_messageInfo_VerifiedStage proto.InternalMessageInfo
 func (m *Warehouse) Reset()      { *m = Warehouse{} }
 func (*Warehouse) ProtoMessage() {}
 func (*Warehouse) Descriptor() ([]byte, []int) {
-	return fileDescriptor_e26b7f7bbc391025, []int{61}
+	return fileDescriptor_e26b7f7bbc391025, []int{63}
 }
 func (m *Warehouse) XXX_Unmarshal(b []byte) error {
 	return m.Unmarshal(b)
@@ -1769,7 +1825,7 @@ var xxx_messageInfo_Warehouse proto.InternalMessageInfo
 func (m *WarehouseList) Reset()      { *m = WarehouseList{} }
 func (*WarehouseList) ProtoMessage() {}
 func (*WarehouseList) Descriptor() ([]byte, []int) {
-	return fileDescriptor_e26b7f7bbc391025, []int{62}
+	return fileDescriptor_e26b7f7bbc391025, []int{64}
 }
 func (m *WarehouseList) XXX_Unmarshal(b []byte) error {
 	return m.Unmarshal(b)
@@ -1797,7 +1853,7 @@ var xxx_messageInfo_WarehouseList proto.InternalMessageInfo
 func (m *WarehouseSpec) Reset()      { *m = WarehouseSpec{} }
 func (*WarehouseSpec) ProtoMessage() {}
 func (*WarehouseSpec) Descriptor() ([]byte, []int) {
-	return fileDescriptor_e26b7f7bbc391025, []int{63}
+	return fileDescriptor_e26b7f7bbc391025, []int{65}
 }
 func (m *WarehouseSpec) XXX_Unmarshal(b []byte) error {
 	return m.Unmarshal(b)
@@ -1825,7 +1881,7 @@ var xxx_messageInfo_WarehouseSpec proto.InternalMessageInfo
 func (m *WarehouseStatus) Reset()      { *m = WarehouseStatus{} }
 func (*WarehouseStatus) ProtoMessage() {}
 func (*WarehouseStatus) Descriptor() ([]byte, []int) {
-	return fileDescriptor_e26b7f7bbc391025, []int{64}
+	return fileDescriptor_e26b7f7bbc391025, []int{66}
 }
 func (m *WarehouseStatus) XXX_Unmarshal(b []byte) error {
 	return m.Unmarshal(b)
@@ -1894,8 +1950,10 @@ func init() {
 	proto.RegisterType((*ImageDiscoveryResult)(nil), "github.com.akuity.kargo.api.v1alpha1.ImageDiscoveryResult")
 	proto.RegisterType((*ImageSubscription)(nil), "github.com.akuity.kargo.api.v1alpha1.ImageSubscription")
 	proto.RegisterType((*Project)(nil), "github.com.akuity.kargo.api.v1alpha1.Project")
+	proto.RegisterType((*ProjectConfig)(nil), "github.com.akuity.kargo.api.v1alpha1.ProjectConfig")
+	proto.RegisterType((*ProjectConfigList)(nil), "github.com.akuity.kargo.api.v1alpha1.ProjectConfigList")
+	proto.RegisterType((*ProjectConfigSpec)(nil), "github.com.akuity.kargo.api.v1alpha1.ProjectConfigSpec")
 	proto.RegisterType((*ProjectList)(nil), "github.com.akuity.kargo.api.v1alpha1.ProjectList")
-	proto.RegisterType((*ProjectSpec)(nil), "github.com.akuity.kargo.api.v1alpha1.ProjectSpec")
 	proto.RegisterType((*ProjectStatus)(nil), "github.com.akuity.kargo.api.v1alpha1.ProjectStatus")
 	proto.RegisterType((*Promotion)(nil), "github.com.akuity.kargo.api.v1alpha1.Promotion")
 	proto.RegisterType((*PromotionList)(nil), "github.com.akuity.kargo.api.v1alpha1.PromotionList")
@@ -1931,268 +1989,270 @@ func init() {
 }
 
 var fileDescriptor_e26b7f7bbc391025 = []byte{
-	// 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,
+	// 4195 bytes of a gzipped FileDescriptorProto
+	0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xdc, 0x5c, 0x4d, 0x6c, 0x23, 0x47,
+	0x76, 0x9e, 0x26, 0x45, 0x4a, 0x7c, 0x94, 0x46, 0x52, 0x8d, 0xc6, 0xab, 0x95, 0x63, 0x69, 0xd2,
+	0x36, 0x0c, 0x3b, 0xb6, 0xa9, 0x8c, 0xc6, 0x63, 0x8f, 0xc7, 0xc6, 0x04, 0x22, 0x35, 0x3f, 0x9a,
+	0xd5, 0x7a, 0x94, 0xa2, 0x3c, 0x8e, 0xc7, 0x36, 0x26, 0x25, 0xb2, 0x44, 0xf6, 0x8a, 0x64, 0xd3,
+	0x5d, 0x45, 0xed, 0x28, 0x09, 0x92, 0xcd, 0x2f, 0x16, 0x09, 0x10, 0x2c, 0x90, 0x0d, 0xbc, 0x87,
+	0x04, 0x09, 0x92, 0xe3, 0x02, 0xb9, 0xe6, 0x90, 0x83, 0x0f, 0xb9, 0x38, 0xc9, 0x26, 0x30, 0x92,
+	0x00, 0x71, 0x80, 0x85, 0xb2, 0xd6, 0x02, 0x39, 0xe7, 0x92, 0xcb, 0x00, 0x01, 0x82, 0xfa, 0xe9,
+	0xee, 0xea, 0x66, 0x73, 0xc4, 0xa6, 0x7e, 0x30, 0xd8, 0x9b, 0x54, 0xaf, 0xea, 0x7b, 0x55, 0xaf,
+	0x5e, 0xbd, 0x7a, 0xef, 0xd5, 0x63, 0xc3, 0xeb, 0x0d, 0x87, 0x37, 0x7b, 0xdb, 0xa5, 0x9a, 0xdb,
+	0x5e, 0x26, 0xbb, 0x3d, 0x87, 0xef, 0x2f, 0xef, 0x12, 0xaf, 0xe1, 0x2e, 0x93, 0xae, 0xb3, 0xbc,
+	0x77, 0x99, 0xb4, 0xba, 0x4d, 0x72, 0x79, 0xb9, 0x41, 0x3b, 0xd4, 0x23, 0x9c, 0xd6, 0x4b, 0x5d,
+	0xcf, 0xe5, 0x2e, 0x7a, 0x21, 0x1c, 0x55, 0x52, 0xa3, 0x4a, 0x72, 0x54, 0x89, 0x74, 0x9d, 0x92,
+	0x3f, 0x6a, 0xe1, 0x35, 0x03, 0xbb, 0xe1, 0x36, 0xdc, 0x65, 0x39, 0x78, 0xbb, 0xb7, 0x23, 0xff,
+	0x93, 0xff, 0xc8, 0xbf, 0x14, 0xe8, 0xc2, 0x9d, 0xdd, 0x6b, 0xac, 0xe4, 0x48, 0xce, 0xf4, 0x11,
+	0xa7, 0x1d, 0xe6, 0xb8, 0x1d, 0xf6, 0x1a, 0xe9, 0x3a, 0x8c, 0x7a, 0x7b, 0xd4, 0x5b, 0xee, 0xee,
+	0x36, 0x04, 0x8d, 0x45, 0x3b, 0x2c, 0xef, 0xf5, 0x4d, 0x6f, 0xe1, 0xf5, 0x10, 0xa9, 0x4d, 0x6a,
+	0x4d, 0xa7, 0x43, 0xbd, 0xfd, 0x70, 0x78, 0x9b, 0x72, 0x92, 0x34, 0x6a, 0x79, 0xd0, 0x28, 0xaf,
+	0xd7, 0xe1, 0x4e, 0x9b, 0xf6, 0x0d, 0x78, 0xe3, 0xa8, 0x01, 0xac, 0xd6, 0xa4, 0x6d, 0x12, 0x1f,
+	0x67, 0x7f, 0x04, 0x17, 0x56, 0x3b, 0xa4, 0xb5, 0xcf, 0x1c, 0x86, 0x7b, 0x9d, 0x55, 0xaf, 0xd1,
+	0x6b, 0xd3, 0x0e, 0x47, 0x97, 0x60, 0xac, 0x43, 0xda, 0x74, 0xde, 0xba, 0x64, 0xbd, 0x54, 0x28,
+	0x4f, 0x7e, 0x7e, 0xb0, 0x74, 0xee, 0xf0, 0x60, 0x69, 0xec, 0x5d, 0xd2, 0xa6, 0x58, 0x52, 0xd0,
+	0xf3, 0x90, 0xdb, 0x23, 0xad, 0x1e, 0x9d, 0xcf, 0xc8, 0x2e, 0x53, 0xba, 0x4b, 0xee, 0xbe, 0x68,
+	0xc4, 0x8a, 0x66, 0xff, 0x6e, 0x36, 0x02, 0xff, 0x4d, 0xca, 0x49, 0x9d, 0x70, 0x82, 0xda, 0x90,
+	0x6f, 0x91, 0x6d, 0xda, 0x62, 0xf3, 0xd6, 0xa5, 0xec, 0x4b, 0xc5, 0x95, 0x9b, 0xa5, 0x61, 0x36,
+	0xb1, 0x94, 0x00, 0x55, 0xda, 0x90, 0x38, 0x37, 0x3b, 0xdc, 0xdb, 0x2f, 0x9f, 0xd7, 0x93, 0xc8,
+	0xab, 0x46, 0xac, 0x99, 0xa0, 0xdf, 0xb6, 0xa0, 0x48, 0x3a, 0x1d, 0x97, 0x13, 0x2e, 0xb6, 0x69,
+	0x3e, 0x23, 0x99, 0xde, 0x1d, 0x9d, 0xe9, 0x6a, 0x08, 0xa6, 0x38, 0x5f, 0xd0, 0x9c, 0x8b, 0x06,
+	0x05, 0x9b, 0x3c, 0x17, 0xde, 0x82, 0xa2, 0x31, 0x55, 0x34, 0x03, 0xd9, 0x5d, 0xba, 0xaf, 0xe4,
+	0x8b, 0xc5, 0x9f, 0x68, 0x2e, 0x22, 0x50, 0x2d, 0xc1, 0xeb, 0x99, 0x6b, 0xd6, 0xc2, 0x0d, 0x98,
+	0x89, 0x33, 0x4c, 0x33, 0xde, 0xfe, 0x63, 0x0b, 0xe6, 0x8c, 0x55, 0x60, 0xba, 0x43, 0x3d, 0xda,
+	0xa9, 0x51, 0xb4, 0x0c, 0x05, 0xb1, 0x97, 0xac, 0x4b, 0x6a, 0xfe, 0x56, 0xcf, 0xea, 0x85, 0x14,
+	0xde, 0xf5, 0x09, 0x38, 0xec, 0x13, 0xa8, 0x45, 0xe6, 0x49, 0x6a, 0xd1, 0x6d, 0x12, 0x46, 0xe7,
+	0xb3, 0x51, 0xb5, 0xd8, 0x14, 0x8d, 0x58, 0xd1, 0xec, 0x87, 0xf0, 0x75, 0x7f, 0x3e, 0x5b, 0xb4,
+	0xdd, 0x6d, 0x11, 0x4e, 0xc3, 0x49, 0x1d, 0xad, 0x7a, 0x97, 0x60, 0x6c, 0xd7, 0xe9, 0xd4, 0xe3,
+	0xb3, 0xf8, 0x86, 0xd3, 0xa9, 0x63, 0x49, 0xb1, 0x77, 0x61, 0x6a, 0xb5, 0xdb, 0xf5, 0xdc, 0x3d,
+	0x5a, 0xaf, 0x72, 0xd2, 0xa0, 0xe8, 0x01, 0x00, 0xd1, 0x0d, 0xab, 0x5c, 0x42, 0x17, 0x57, 0x7e,
+	0xa1, 0xa4, 0xce, 0x4c, 0xc9, 0x3c, 0x33, 0xa5, 0xee, 0x6e, 0x43, 0x34, 0xb0, 0x92, 0x38, 0x9a,
+	0xa5, 0xbd, 0xcb, 0xa5, 0x2d, 0xa7, 0x4d, 0xcb, 0xe7, 0x0f, 0x0f, 0x96, 0x60, 0x35, 0x40, 0xc0,
+	0x06, 0x9a, 0xfd, 0x3b, 0x16, 0x5c, 0x5c, 0xf5, 0x1a, 0x6e, 0x65, 0x6d, 0xb5, 0xdb, 0xbd, 0x43,
+	0x49, 0x8b, 0x37, 0xab, 0x9c, 0xf0, 0x1e, 0x43, 0x37, 0x20, 0xcf, 0xe4, 0x5f, 0x7a, 0x31, 0x2f,
+	0xfa, 0xfa, 0xa9, 0xe8, 0x8f, 0x0f, 0x96, 0xe6, 0x12, 0x06, 0x52, 0xac, 0x47, 0xa1, 0x97, 0x61,
+	0xbc, 0x4d, 0x19, 0x23, 0x0d, 0x5f, 0xe2, 0xd3, 0x1a, 0x60, 0xfc, 0x9b, 0xaa, 0x19, 0xfb, 0x74,
+	0xfb, 0x1f, 0x33, 0x30, 0x1d, 0x60, 0x69, 0xf6, 0xa7, 0xb0, 0xbd, 0x3d, 0x98, 0x6c, 0x1a, 0x2b,
+	0x94, 0xbb, 0x5c, 0x5c, 0x79, 0x7b, 0xc8, 0x93, 0x94, 0x24, 0xa4, 0xf2, 0x9c, 0x66, 0x33, 0x69,
+	0xb6, 0xe2, 0x08, 0x1b, 0xd4, 0x06, 0x60, 0xfb, 0x9d, 0x9a, 0x66, 0x3a, 0x26, 0x99, 0xbe, 0x95,
+	0x92, 0x69, 0x35, 0x00, 0x28, 0x23, 0xcd, 0x12, 0xc2, 0x36, 0x6c, 0x30, 0xb0, 0xff, 0xc6, 0x82,
+	0x0b, 0x09, 0xe3, 0xd0, 0x3b, 0xb1, 0xfd, 0x7c, 0xa1, 0x6f, 0x3f, 0x51, 0xdf, 0xb0, 0x70, 0x37,
+	0x5f, 0x85, 0x09, 0x8f, 0xee, 0x39, 0xe2, 0xa6, 0xd0, 0x12, 0x9e, 0xd1, 0xe3, 0x27, 0xb0, 0x6e,
+	0xc7, 0x41, 0x0f, 0xf4, 0x0a, 0x14, 0xfc, 0xbf, 0x85, 0x98, 0xb3, 0xe2, 0x30, 0x89, 0x8d, 0xf3,
+	0xbb, 0x32, 0x1c, 0xd2, 0xed, 0xdf, 0x82, 0x5c, 0xa5, 0x49, 0x3c, 0x2e, 0x34, 0xc6, 0xa3, 0x5d,
+	0xf7, 0x3d, 0xbc, 0xa1, 0xa7, 0x18, 0x68, 0x0c, 0x56, 0xcd, 0xd8, 0xa7, 0x0f, 0xb1, 0xd9, 0x2f,
+	0xc3, 0xf8, 0x1e, 0xf5, 0xe4, 0x7c, 0xb3, 0x51, 0xb0, 0xfb, 0xaa, 0x19, 0xfb, 0x74, 0xfb, 0xdf,
+	0x2c, 0x98, 0x93, 0x33, 0x58, 0x73, 0x58, 0xcd, 0xdd, 0xa3, 0xde, 0x3e, 0xa6, 0xac, 0xd7, 0x3a,
+	0xe1, 0x09, 0xad, 0xc1, 0x0c, 0xa3, 0xed, 0x3d, 0xea, 0x55, 0xdc, 0x0e, 0xe3, 0x1e, 0x71, 0x3a,
+	0x5c, 0xcf, 0x6c, 0x5e, 0xf7, 0x9e, 0xa9, 0xc6, 0xe8, 0xb8, 0x6f, 0x04, 0x7a, 0x09, 0x26, 0xf4,
+	0xb4, 0x85, 0x2a, 0x09, 0xc1, 0x4e, 0x8a, 0x3d, 0xd0, 0x6b, 0x62, 0x38, 0xa0, 0xda, 0xff, 0x6d,
+	0xc1, 0xac, 0x5c, 0x55, 0xb5, 0xb7, 0xcd, 0x6a, 0x9e, 0xd3, 0x15, 0x06, 0xf8, 0x69, 0x5c, 0xd2,
+	0x0d, 0x38, 0x5f, 0xf7, 0x05, 0xbf, 0xe1, 0xb4, 0x1d, 0x2e, 0xcf, 0x48, 0xae, 0xfc, 0x8c, 0xc6,
+	0x38, 0xbf, 0x16, 0xa1, 0xe2, 0x58, 0x6f, 0xb5, 0x7d, 0xad, 0x1e, 0xe3, 0xd4, 0xdb, 0xf4, 0xdc,
+	0xb6, 0x2b, 0xd6, 0xb9, 0x45, 0xd8, 0x2e, 0xfa, 0x55, 0x98, 0x68, 0xeb, 0x4b, 0x4f, 0x5b, 0xcd,
+	0x5f, 0x1c, 0xce, 0x6a, 0xde, 0xdb, 0xfe, 0x16, 0xad, 0x71, 0x71, 0x61, 0x86, 0xa7, 0x2d, 0x6c,
+	0xc3, 0x01, 0x2a, 0xfa, 0x00, 0xc6, 0x58, 0x97, 0xd6, 0xa4, 0x88, 0x8a, 0x2b, 0x6f, 0x0e, 0x77,
+	0xa8, 0x23, 0x93, 0xac, 0x76, 0x69, 0x2d, 0x94, 0xad, 0xf8, 0x0f, 0x4b, 0x48, 0xfb, 0x3f, 0x2d,
+	0x98, 0x4f, 0x5a, 0xd5, 0x86, 0xc3, 0x38, 0xfa, 0xa8, 0x6f, 0x65, 0xa5, 0xe1, 0x56, 0x26, 0x46,
+	0xcb, 0x75, 0x05, 0xa7, 0xd7, 0x6f, 0x31, 0x56, 0xf5, 0x10, 0x72, 0x0e, 0xa7, 0x6d, 0xdf, 0xd5,
+	0xb8, 0x3e, 0xdc, 0xb2, 0x92, 0x26, 0x1b, 0x5e, 0xa1, 0xeb, 0x02, 0x10, 0x2b, 0x5c, 0xfb, 0x43,
+	0x98, 0xac, 0xf4, 0x3c, 0x8f, 0x76, 0xb8, 0xba, 0xe0, 0xbe, 0x01, 0x39, 0xe6, 0x74, 0xb4, 0x9d,
+	0x4f, 0x77, 0xb7, 0x15, 0x04, 0x78, 0x55, 0x0c, 0xc6, 0x0a, 0xc3, 0xfe, 0xb3, 0x2c, 0x5c, 0xf0,
+	0x35, 0x86, 0xd6, 0x57, 0x3d, 0xee, 0xec, 0x90, 0x1a, 0x67, 0xa8, 0x0e, 0x93, 0xf5, 0xb0, 0x99,
+	0x6b, 0x43, 0x9c, 0x86, 0x57, 0x60, 0xec, 0x0d, 0x78, 0x8e, 0x23, 0xa8, 0xe8, 0x7d, 0xc8, 0x36,
+	0x1c, 0xae, 0x3d, 0xc3, 0x6b, 0xc3, 0x49, 0xee, 0xb6, 0x13, 0xb7, 0x3c, 0xe5, 0xa2, 0x66, 0x95,
+	0xbd, 0xed, 0x70, 0x2c, 0x10, 0xd1, 0x36, 0xe4, 0x9d, 0x36, 0x69, 0xd0, 0x94, 0xbb, 0xb2, 0x2e,
+	0xc6, 0xc4, 0xd1, 0x03, 0x57, 0x53, 0x52, 0x19, 0xd6, 0xc8, 0x82, 0x47, 0x4d, 0x58, 0x0c, 0x65,
+	0xb3, 0x87, 0xdf, 0xf9, 0x04, 0xdb, 0x19, 0xf2, 0x90, 0x54, 0x86, 0x35, 0xb2, 0xfd, 0x65, 0x06,
+	0x66, 0x42, 0xf9, 0x55, 0xdc, 0x76, 0xdb, 0xe1, 0x68, 0x01, 0x32, 0x4e, 0x5d, 0x1b, 0x24, 0xd0,
+	0x03, 0x33, 0xeb, 0x6b, 0x38, 0xe3, 0xd4, 0xd1, 0x8b, 0x90, 0xdf, 0xf6, 0x48, 0xa7, 0xd6, 0xd4,
+	0x86, 0x28, 0x00, 0x2e, 0xcb, 0x56, 0xac, 0xa9, 0xe8, 0x39, 0xc8, 0x72, 0xd2, 0xd0, 0xf6, 0x27,
+	0x90, 0xdf, 0x16, 0x69, 0x60, 0xd1, 0x2e, 0x0c, 0x1f, 0xeb, 0xc9, 0x33, 0x2c, 0x77, 0xde, 0x30,
+	0x7c, 0x55, 0xd5, 0x8c, 0x7d, 0xba, 0xe0, 0x48, 0x7a, 0xbc, 0xe9, 0x7a, 0xf3, 0xb9, 0x28, 0xc7,
+	0x55, 0xd9, 0x8a, 0x35, 0x55, 0xb8, 0x28, 0x35, 0x39, 0x7f, 0x4e, 0xbd, 0xf9, 0x7c, 0xd4, 0x45,
+	0xa9, 0xf8, 0x04, 0x1c, 0xf6, 0x41, 0x1f, 0x43, 0xb1, 0xe6, 0x51, 0xc2, 0x5d, 0x6f, 0x8d, 0x70,
+	0x3a, 0x3f, 0x9e, 0x5a, 0x03, 0xa7, 0x85, 0x97, 0x5e, 0x09, 0x21, 0xb0, 0x89, 0x67, 0xff, 0x6d,
+	0x16, 0xe6, 0x43, 0xd1, 0xca, 0xbd, 0x0d, 0x3d, 0x53, 0x2d, 0x1e, 0x6b, 0x80, 0x78, 0x5e, 0x84,
+	0x7c, 0xdd, 0x69, 0x50, 0xc6, 0xe3, 0x52, 0x5e, 0x93, 0xad, 0x58, 0x53, 0xd1, 0x1f, 0xc4, 0xa2,
+	0x91, 0x9c, 0x54, 0x94, 0x7b, 0xc3, 0x29, 0xca, 0xa0, 0xc9, 0x8d, 0x10, 0x92, 0xa0, 0x15, 0x80,
+	0x86, 0xc3, 0xf5, 0xa5, 0xa5, 0x77, 0x3d, 0x30, 0xd6, 0xb7, 0x03, 0x0a, 0x36, 0x7a, 0xa1, 0xf7,
+	0xa1, 0x20, 0xe5, 0x35, 0xe2, 0xf9, 0x97, 0x2e, 0x4c, 0xc5, 0x07, 0xc0, 0x21, 0xd6, 0xb1, 0x83,
+	0x9c, 0x0f, 0x01, 0xdd, 0x7c, 0xd4, 0xf5, 0x28, 0x13, 0x57, 0xf7, 0x7d, 0xe2, 0x39, 0x64, 0xbb,
+	0x45, 0x4f, 0x2a, 0x8e, 0xfd, 0x62, 0x0c, 0xc6, 0x6f, 0x79, 0xd4, 0x69, 0x34, 0xf9, 0x19, 0x5c,
+	0x89, 0xcf, 0x43, 0x8e, 0xb4, 0x1c, 0xc2, 0xa4, 0x76, 0x1b, 0x53, 0x5a, 0x15, 0x8d, 0x58, 0xd1,
+	0xd0, 0x87, 0x90, 0x77, 0x3d, 0xa7, 0xe1, 0x74, 0xe6, 0x0b, 0x72, 0x12, 0x57, 0x86, 0xd3, 0x1f,
+	0xbd, 0x8a, 0x7b, 0x72, 0x68, 0xa8, 0xa2, 0xea, 0x7f, 0xac, 0x21, 0xd1, 0x03, 0x18, 0x57, 0x47,
+	0xce, 0x37, 0x63, 0xcb, 0x43, 0x9b, 0x61, 0x75, 0x6a, 0x43, 0xd3, 0xa0, 0xfe, 0x67, 0xd8, 0x07,
+	0x44, 0xd5, 0xc0, 0x0a, 0x8f, 0x49, 0xe8, 0x57, 0x52, 0x58, 0xe1, 0x81, 0x66, 0xb7, 0x1a, 0x98,
+	0xdd, 0x5c, 0x1a, 0x50, 0x69, 0x58, 0x07, 0xd9, 0x59, 0x21, 0x62, 0xed, 0xee, 0xe7, 0x47, 0x10,
+	0xb1, 0x8e, 0x35, 0xce, 0x47, 0x63, 0x04, 0x3f, 0x1a, 0xb0, 0xbf, 0x9f, 0x85, 0x59, 0xdd, 0xb3,
+	0xe2, 0xb6, 0x5a, 0xb4, 0x26, 0x7d, 0x4b, 0x65, 0xc5, 0xb3, 0x89, 0x56, 0xdc, 0xf1, 0x7d, 0x0a,
+	0x75, 0x33, 0x96, 0x53, 0xcd, 0x26, 0xe4, 0x51, 0x92, 0x7e, 0x84, 0xb2, 0x11, 0xc1, 0x2e, 0xe9,
+	0x5e, 0xda, 0xbb, 0x40, 0xbf, 0x6f, 0xc1, 0x85, 0x3d, 0xea, 0x39, 0x3b, 0x4e, 0x4d, 0x9e, 0xc7,
+	0x3b, 0x0e, 0xe3, 0xae, 0xb7, 0xaf, 0xef, 0xcd, 0x37, 0x86, 0xe3, 0x7c, 0xdf, 0x00, 0x58, 0xef,
+	0xec, 0xb8, 0xe5, 0x67, 0x35, 0xb7, 0x0b, 0xf7, 0xfb, 0xa1, 0x71, 0x12, 0xbf, 0x85, 0x2e, 0x40,
+	0x38, 0xdb, 0x04, 0x73, 0xb0, 0x61, 0x1e, 0xde, 0xa1, 0x27, 0xe6, 0x2f, 0xd6, 0xb7, 0x9d, 0xa6,
+	0x19, 0xf9, 0xcc, 0x82, 0xa2, 0xa6, 0x9f, 0x81, 0x9b, 0x88, 0xa3, 0x6e, 0xe2, 0x6b, 0xa9, 0xe6,
+	0x3f, 0xc0, 0x33, 0xf4, 0x60, 0x2a, 0x72, 0xc8, 0xd1, 0x55, 0x9d, 0x2e, 0x51, 0x36, 0xf0, 0xe7,
+	0xcd, 0x74, 0xc9, 0xe3, 0x83, 0xa5, 0xd9, 0x48, 0xe7, 0x30, 0x87, 0x72, 0x74, 0xec, 0x72, 0x7d,
+	0xe2, 0x07, 0x7f, 0xb9, 0x74, 0xee, 0x3b, 0x3f, 0xbe, 0x74, 0xce, 0xfe, 0x34, 0x0b, 0x33, 0x71,
+	0xa9, 0x0e, 0x61, 0x7b, 0x43, 0x1b, 0x36, 0x71, 0xaa, 0x36, 0x2c, 0x73, 0x7a, 0x36, 0x2c, 0x7b,
+	0x1a, 0x36, 0x6c, 0xec, 0xc4, 0x6c, 0x98, 0xfd, 0x2f, 0x16, 0x9c, 0x0f, 0x76, 0xe6, 0x93, 0x9e,
+	0xf0, 0x3f, 0x42, 0xa9, 0x5b, 0x27, 0x2f, 0xf5, 0x87, 0x30, 0xce, 0xdc, 0x9e, 0x57, 0x93, 0x4e,
+	0xb6, 0x40, 0x7f, 0x3d, 0x9d, 0xd1, 0x54, 0x63, 0x0d, 0xcf, 0x52, 0x35, 0x60, 0x1f, 0xd5, 0xfe,
+	0x2c, 0x13, 0x2c, 0x48, 0xd3, 0x94, 0xe3, 0xe5, 0x09, 0xb7, 0x54, 0x2c, 0x68, 0xc2, 0x74, 0xbc,
+	0x44, 0x2b, 0xd6, 0x54, 0x64, 0x4b, 0x7b, 0xee, 0xfb, 0xff, 0x85, 0x32, 0x68, 0xb3, 0x2c, 0x37,
+	0x41, 0x51, 0x50, 0x17, 0x66, 0x3c, 0xfa, 0x49, 0xcf, 0xf1, 0x68, 0xbd, 0xea, 0x92, 0x5d, 0xe1,
+	0xb4, 0xe8, 0x24, 0xd7, 0x90, 0xe7, 0x7e, 0xad, 0xe7, 0x49, 0x13, 0x56, 0x9e, 0x13, 0xb1, 0x3b,
+	0x8e, 0x61, 0xe1, 0x3e, 0x74, 0xe4, 0xc2, 0x1c, 0xd9, 0x23, 0x4e, 0x8b, 0x6c, 0x3b, 0x2d, 0x87,
+	0xef, 0x57, 0xb9, 0x47, 0x38, 0x6d, 0xec, 0x6b, 0x17, 0xfb, 0x6d, 0xbd, 0x96, 0xb9, 0xd5, 0x84,
+	0x3e, 0x8f, 0x0f, 0x96, 0x9e, 0xd5, 0xb2, 0x48, 0x22, 0xe3, 0x44, 0x60, 0xfb, 0xbf, 0x72, 0x81,
+	0x85, 0xd0, 0x79, 0xad, 0x5f, 0x87, 0x62, 0x4d, 0x05, 0x93, 0xad, 0xfd, 0xf5, 0x8e, 0xd6, 0xe9,
+	0xb5, 0x11, 0x6e, 0xbb, 0x52, 0x25, 0x84, 0x89, 0x79, 0xa1, 0x06, 0x05, 0x9b, 0xdc, 0xd0, 0xb7,
+	0x01, 0x94, 0xe9, 0xa7, 0xf5, 0xf5, 0x8e, 0xbe, 0xdb, 0x2a, 0xa3, 0xf0, 0xbe, 0x1f, 0xa0, 0x28,
+	0xd6, 0x81, 0x93, 0x15, 0x12, 0xb0, 0xc1, 0x4a, 0xac, 0xda, 0xcf, 0xe2, 0xde, 0x72, 0x3d, 0x6d,
+	0x24, 0x46, 0x5a, 0xf5, 0x6a, 0x08, 0x13, 0xf7, 0xbd, 0x43, 0x0a, 0x36, 0xb9, 0x2d, 0x78, 0x30,
+	0x13, 0x97, 0x55, 0xc2, 0xfd, 0x76, 0x27, 0x7a, 0xbf, 0xad, 0x0c, 0x69, 0x11, 0x8c, 0xc4, 0x80,
+	0xf9, 0x8e, 0xe0, 0xc1, 0x74, 0x4c, 0x46, 0x09, 0x2c, 0xd7, 0xa3, 0x2c, 0xaf, 0xa4, 0xb9, 0xeb,
+	0x75, 0xb6, 0xdd, 0xe4, 0xc9, 0x60, 0x26, 0x2e, 0x9d, 0x13, 0x63, 0x1a, 0x49, 0xf1, 0x9b, 0x97,
+	0xf8, 0x9f, 0x67, 0xa0, 0x10, 0x98, 0xf1, 0x34, 0xf9, 0x3a, 0xe5, 0x7e, 0x65, 0x8e, 0x08, 0xa2,
+	0xb3, 0xc3, 0x04, 0xd1, 0x63, 0x83, 0x83, 0x68, 0x3f, 0xa7, 0x9f, 0x7f, 0x72, 0x4e, 0xdf, 0x08,
+	0xa2, 0xc7, 0x87, 0x0f, 0xa2, 0x27, 0x8e, 0x0e, 0xa2, 0xed, 0xbf, 0xb2, 0x00, 0xf5, 0x67, 0x4c,
+	0xd2, 0x08, 0x8a, 0xc4, 0x2f, 0xd7, 0x37, 0xd2, 0x86, 0xaf, 0x47, 0xdd, 0xb1, 0xf6, 0x67, 0x39,
+	0x98, 0xbe, 0xed, 0x8c, 0x9c, 0x7a, 0xe5, 0xf0, 0x35, 0x85, 0x54, 0xa5, 0xda, 0xf1, 0x0d, 0x2c,
+	0xab, 0xda, 0xdf, 0xeb, 0x7a, 0xe8, 0xd7, 0x2a, 0xc9, 0xdd, 0x1e, 0x0f, 0x26, 0xe1, 0x41, 0xd0,
+	0x43, 0x2b, 0xc9, 0xdb, 0x30, 0xc5, 0xb8, 0xe7, 0xd4, 0xb8, 0x4a, 0xee, 0xb2, 0xf9, 0xa2, 0xbc,
+	0xb9, 0x2e, 0xea, 0xee, 0x53, 0x55, 0x93, 0x88, 0xa3, 0x7d, 0x13, 0x73, 0xc6, 0x63, 0xa9, 0x73,
+	0xc6, 0xcb, 0x50, 0x20, 0xad, 0x96, 0xfb, 0xed, 0x2d, 0xd2, 0x60, 0x3a, 0x4b, 0x13, 0x68, 0xcd,
+	0xaa, 0x4f, 0xc0, 0x61, 0x1f, 0x54, 0x02, 0x70, 0x1a, 0x1d, 0xd7, 0xa3, 0x72, 0x44, 0x5e, 0x5e,
+	0xa1, 0xf2, 0x5d, 0x6c, 0x3d, 0x68, 0xc5, 0x46, 0x0f, 0x54, 0x85, 0x8b, 0x4e, 0x87, 0xd1, 0x5a,
+	0xcf, 0xa3, 0xd5, 0x5d, 0xa7, 0xbb, 0xb5, 0x51, 0x95, 0x56, 0x62, 0x5f, 0x6a, 0xf3, 0x44, 0xf9,
+	0x39, 0xcd, 0xec, 0xe2, 0x7a, 0x52, 0x27, 0x9c, 0x3c, 0x16, 0xbd, 0x0e, 0x93, 0x4e, 0xa7, 0xd6,
+	0xea, 0xd5, 0xe9, 0x26, 0xe1, 0x4d, 0x36, 0x3f, 0x21, 0xa7, 0x31, 0x73, 0x78, 0xb0, 0x34, 0xb9,
+	0x6e, 0xb4, 0xe3, 0x48, 0x2f, 0x31, 0x8a, 0x3e, 0x32, 0x46, 0x15, 0xc2, 0x51, 0x37, 0x1f, 0x99,
+	0xa3, 0xcc, 0x5e, 0x09, 0x59, 0x75, 0x48, 0x95, 0x55, 0xff, 0x61, 0x06, 0xf2, 0xea, 0x51, 0x0b,
+	0x5d, 0x8d, 0xbd, 0x1c, 0x3d, 0xd7, 0xf7, 0x72, 0x54, 0x4c, 0x7a, 0x00, 0xb4, 0x21, 0xef, 0x30,
+	0xd6, 0x8b, 0x7a, 0x2c, 0xeb, 0xb2, 0x05, 0x6b, 0x8a, 0xcc, 0x38, 0xba, 0x9d, 0x1d, 0xa7, 0xa1,
+	0xd3, 0x31, 0x37, 0x0c, 0x3f, 0x25, 0x2c, 0x4d, 0x78, 0x18, 0xd4, 0x2e, 0x84, 0x2e, 0x4b, 0xa4,
+	0x83, 0xf0, 0x5d, 0xee, 0x56, 0xef, 0xbd, 0xab, 0x78, 0x54, 0x24, 0x22, 0xd6, 0xc8, 0x82, 0x87,
+	0xdb, 0xe3, 0xdd, 0x1e, 0x97, 0x8a, 0x72, 0x42, 0x3c, 0xee, 0x49, 0x44, 0xac, 0x91, 0xed, 0x4f,
+	0x2d, 0x98, 0x56, 0x32, 0xa8, 0x34, 0x69, 0x6d, 0xb7, 0xca, 0x69, 0x57, 0x84, 0x10, 0x3d, 0x46,
+	0x59, 0x3c, 0x84, 0x78, 0x8f, 0x51, 0x86, 0x25, 0xc5, 0x58, 0x7d, 0xe6, 0xb4, 0x56, 0x6f, 0x1f,
+	0x66, 0x20, 0x27, 0x7d, 0xf5, 0x34, 0xf6, 0x27, 0x9a, 0x5c, 0xcb, 0x0c, 0x95, 0x5c, 0x3b, 0x22,
+	0xff, 0x1a, 0x26, 0x18, 0xc7, 0x9e, 0x98, 0x60, 0x64, 0x49, 0xf9, 0xc5, 0x77, 0x52, 0x84, 0x28,
+	0xa3, 0xd4, 0x37, 0x1c, 0x37, 0x7f, 0xf7, 0x53, 0x0b, 0xe6, 0x92, 0x32, 0xed, 0x69, 0x64, 0xfe,
+	0x2a, 0x4c, 0x74, 0x5b, 0x84, 0xef, 0xb8, 0x5e, 0x3b, 0xfe, 0xc2, 0xba, 0xa9, 0xdb, 0x71, 0xd0,
+	0x03, 0x79, 0x00, 0x9e, 0x1f, 0xac, 0xfa, 0x81, 0xdc, 0x8d, 0xe3, 0x65, 0x61, 0xc3, 0x1d, 0x0e,
+	0x9a, 0x18, 0x36, 0xb8, 0xd8, 0x7f, 0x94, 0x83, 0x59, 0x39, 0x64, 0xd4, 0x6b, 0x6d, 0x14, 0xb5,
+	0xea, 0xc2, 0x33, 0x32, 0xc4, 0xec, 0xbf, 0x09, 0x95, 0xa6, 0x5d, 0xd3, 0xe3, 0x9f, 0x59, 0x4f,
+	0xec, 0xf5, 0x78, 0x20, 0x05, 0x0f, 0xc0, 0xed, 0xbf, 0xde, 0xe0, 0x67, 0xef, 0x7a, 0x33, 0x95,
+	0x6d, 0xfc, 0x48, 0x65, 0x1b, 0x78, 0x19, 0x4e, 0x1c, 0xe3, 0x32, 0xec, 0xbf, 0xa0, 0x0a, 0xa9,
+	0x2e, 0xa8, 0xbf, 0xc8, 0xc0, 0xf8, 0xa6, 0xe7, 0xca, 0x17, 0x9b, 0xd3, 0x4f, 0x6b, 0xbf, 0x37,
+	0xe2, 0x4b, 0xaf, 0x80, 0x52, 0x16, 0x59, 0xbe, 0xf4, 0x4e, 0x44, 0x5f, 0x79, 0x8d, 0x2c, 0x6d,
+	0x36, 0x4d, 0xf0, 0xa0, 0x81, 0x8f, 0xc8, 0xd2, 0xfe, 0xc8, 0x82, 0xa9, 0xc8, 0x14, 0x9e, 0xe2,
+	0x17, 0xf1, 0x98, 0x9c, 0x92, 0x5e, 0xc4, 0xff, 0xc9, 0x82, 0xd9, 0x48, 0xcf, 0x33, 0xc8, 0x71,
+	0xfe, 0x4a, 0x34, 0xc7, 0x79, 0x65, 0x84, 0xf5, 0x0c, 0xc8, 0x74, 0xfe, 0x49, 0x7c, 0x35, 0x62,
+	0xa5, 0xe8, 0x37, 0x61, 0xb6, 0xeb, 0x3f, 0xa0, 0x6f, 0xba, 0x2d, 0xa7, 0xe6, 0x50, 0x3f, 0x65,
+	0x7e, 0x35, 0x65, 0x75, 0x81, 0x1c, 0xbe, 0x5f, 0xfe, 0xba, 0xe6, 0x3e, 0xbb, 0x19, 0xc7, 0xc5,
+	0xfd, 0xac, 0x64, 0x06, 0x59, 0xcf, 0xea, 0xa9, 0xcd, 0x20, 0xeb, 0xf9, 0x0d, 0x90, 0xeb, 0xbf,
+	0x87, 0x4a, 0xaf, 0x13, 0x44, 0x35, 0x80, 0x9a, 0xdb, 0xa9, 0x3b, 0x3c, 0xa8, 0x46, 0x2a, 0xae,
+	0x2c, 0x0f, 0xb7, 0x8a, 0x8a, 0x3f, 0x2e, 0xd4, 0xfa, 0xa0, 0x89, 0x61, 0x03, 0x16, 0x5d, 0xf1,
+	0x4b, 0x07, 0xa3, 0x2e, 0xb2, 0x2a, 0x1d, 0x7c, 0x7c, 0xb0, 0x34, 0xa9, 0xe7, 0x64, 0x96, 0x12,
+	0xa6, 0x29, 0x91, 0xfb, 0xeb, 0x0c, 0x14, 0x82, 0x1d, 0x3c, 0x73, 0x7b, 0x77, 0x25, 0xa5, 0xee,
+	0x0d, 0x3a, 0xc3, 0xe8, 0xe3, 0x98, 0xbd, 0x4b, 0xab, 0xd4, 0x47, 0x58, 0xbc, 0xbf, 0x57, 0x9b,
+	0xaf, 0xfa, 0x9e, 0x81, 0x02, 0x6f, 0x45, 0x15, 0x78, 0x39, 0xe5, 0x6a, 0x06, 0xa8, 0xf0, 0x77,
+	0x2d, 0x98, 0x8e, 0x1d, 0x63, 0xf4, 0x3c, 0xe4, 0x64, 0x92, 0x57, 0xeb, 0x57, 0x30, 0x50, 0xa7,
+	0x8f, 0x24, 0x0d, 0x6d, 0xc2, 0x1c, 0xe9, 0x71, 0x37, 0x18, 0x7b, 0xb3, 0x43, 0xb6, 0x5b, 0x54,
+	0xe5, 0x84, 0x26, 0xca, 0x3f, 0x17, 0x64, 0x63, 0x13, 0xfa, 0xe0, 0xc4, 0x91, 0xf6, 0x8f, 0x32,
+	0x80, 0x82, 0xc6, 0x34, 0xaf, 0x23, 0x1f, 0xc3, 0xf8, 0x8e, 0x4a, 0x33, 0x1e, 0xef, 0x79, 0xab,
+	0x5c, 0x34, 0x5f, 0xf8, 0x7c, 0x4c, 0xf4, 0xc1, 0xc9, 0xe8, 0x11, 0xf4, 0xeb, 0x10, 0x7a, 0x00,
+	0xb0, 0xe3, 0x74, 0x1c, 0xd6, 0x1c, 0xb1, 0x4a, 0x40, 0xba, 0x5d, 0xb7, 0x02, 0x04, 0x6c, 0xa0,
+	0xd9, 0x7f, 0x9a, 0x31, 0xf4, 0x53, 0x1a, 0xfc, 0xa1, 0xf6, 0xf5, 0xe5, 0xa8, 0x30, 0x0b, 0xfd,
+	0x4f, 0x9f, 0x81, 0x60, 0x1e, 0xc0, 0xd8, 0x1e, 0xf1, 0xfc, 0x57, 0x98, 0x21, 0x0b, 0x90, 0xfa,
+	0x6b, 0x0f, 0xc2, 0x3d, 0xbd, 0x4f, 0x3c, 0x86, 0x25, 0xa6, 0xb8, 0x0c, 0x19, 0xa7, 0x5d, 0xdf,
+	0x86, 0xa6, 0x36, 0x0a, 0x9c, 0x76, 0xcd, 0x05, 0xd2, 0x2e, 0xc3, 0x0a, 0xd0, 0xfe, 0xfe, 0xb8,
+	0xa1, 0xf1, 0xda, 0x6c, 0xdf, 0x05, 0xd4, 0x22, 0x8c, 0xdf, 0x21, 0x9d, 0xba, 0xd0, 0x4f, 0xba,
+	0xe3, 0x51, 0xd6, 0xd4, 0xbe, 0xf4, 0x82, 0x46, 0x41, 0x1b, 0x7d, 0x3d, 0x70, 0xc2, 0x28, 0x74,
+	0x35, 0x6a, 0x9d, 0x97, 0xe2, 0xd6, 0xf9, 0x7c, 0x78, 0xdc, 0x46, 0xb3, 0xcf, 0xa6, 0xbe, 0xe7,
+	0x4e, 0x41, 0xdf, 0x7f, 0x03, 0x66, 0x77, 0xe2, 0x6f, 0xe1, 0xba, 0x7e, 0xe8, 0xcd, 0x11, 0x9f,
+	0xd2, 0xcb, 0x17, 0x0f, 0xc3, 0x07, 0xd4, 0xb0, 0x19, 0xf7, 0x33, 0x42, 0xae, 0x5f, 0x38, 0x2d,
+	0x93, 0x1b, 0x2a, 0x6f, 0x35, 0xf4, 0x99, 0x8b, 0xa5, 0x45, 0xe2, 0x25, 0xd3, 0x0a, 0x12, 0x47,
+	0x18, 0xc4, 0xce, 0x60, 0xfe, 0x24, 0xcf, 0x20, 0xba, 0x1a, 0xbc, 0x17, 0x89, 0xe9, 0xc8, 0xa0,
+	0x23, 0xdb, 0xf7, 0xd2, 0x23, 0x48, 0xd8, 0xec, 0x87, 0xbe, 0x67, 0xc1, 0x45, 0xa1, 0xac, 0x37,
+	0x1f, 0xd1, 0x5a, 0x4f, 0x48, 0xc5, 0xff, 0x3d, 0xc5, 0x7c, 0x51, 0x4a, 0x63, 0xc8, 0x32, 0xf2,
+	0x6a, 0x12, 0x44, 0x18, 0x41, 0x25, 0x92, 0x71, 0x32, 0x63, 0xf4, 0x50, 0xda, 0x0e, 0x4e, 0x65,
+	0x80, 0x7a, 0xfc, 0xec, 0x51, 0x41, 0xdb, 0x1d, 0xae, 0xec, 0x0e, 0xa7, 0xf6, 0x4f, 0xb2, 0xa6,
+	0xb9, 0x1a, 0x2e, 0xa7, 0xf5, 0x00, 0xc6, 0x38, 0x61, 0xbb, 0xfa, 0x14, 0xbc, 0x33, 0x42, 0x49,
+	0x6c, 0x78, 0x16, 0x64, 0xb4, 0x24, 0x9b, 0x24, 0x26, 0x5a, 0x80, 0x0c, 0x61, 0xf1, 0x17, 0x8e,
+	0x55, 0x86, 0x33, 0x84, 0xc9, 0xd7, 0x8f, 0x1d, 0x1d, 0xcb, 0x86, 0xaf, 0x1f, 0x3b, 0x38, 0xe3,
+	0xec, 0xa0, 0x0f, 0x20, 0xe7, 0x51, 0xee, 0xed, 0x6b, 0x6b, 0x7e, 0x6d, 0x04, 0xc3, 0x85, 0xc5,
+	0x78, 0x25, 0x22, 0xf9, 0x27, 0x56, 0x88, 0x81, 0xbd, 0xcd, 0x9f, 0x82, 0xbd, 0x0d, 0xd3, 0x83,
+	0xd9, 0x53, 0x4b, 0x0f, 0xfe, 0xd0, 0x32, 0x2e, 0xf8, 0x60, 0xa1, 0xe8, 0x3d, 0x18, 0xe7, 0x4e,
+	0x9b, 0xba, 0x3d, 0x9e, 0xce, 0x6b, 0x0a, 0x1e, 0x90, 0xa5, 0x19, 0xdb, 0x52, 0x10, 0xd8, 0xc7,
+	0x12, 0x31, 0x3f, 0xf5, 0x3c, 0xd7, 0xdb, 0x6a, 0x0a, 0xb3, 0xec, 0xb6, 0x94, 0x6b, 0x32, 0x15,
+	0xc6, 0xfc, 0x37, 0x23, 0x54, 0x1c, 0xeb, 0xed, 0x47, 0xb4, 0x3f, 0x2b, 0x35, 0xde, 0x3a, 0xa2,
+	0x3d, 0xd3, 0xe2, 0xee, 0x91, 0x23, 0xda, 0x23, 0xab, 0xba, 0x3f, 0x82, 0x67, 0x92, 0xcf, 0xf1,
+	0x89, 0xfc, 0x2a, 0xea, 0x1f, 0xe2, 0xb2, 0x92, 0xee, 0x93, 0x7f, 0xfc, 0xac, 0xd3, 0x74, 0x77,
+	0x32, 0x27, 0xed, 0xee, 0x78, 0xe6, 0x52, 0xf4, 0x6f, 0xc8, 0xd0, 0xc7, 0x5a, 0xcf, 0xac, 0x34,
+	0xbf, 0x4a, 0xea, 0x83, 0x19, 0xa8, 0x6b, 0xff, 0x6c, 0xc1, 0xc5, 0xc4, 0xde, 0x81, 0x0c, 0x33,
+	0xa7, 0x29, 0x43, 0xeb, 0xa4, 0x65, 0xf8, 0x83, 0x0c, 0xcc, 0x60, 0xda, 0x75, 0x23, 0xb9, 0xe8,
+	0x4d, 0xbf, 0xfa, 0x3e, 0x45, 0x4c, 0x10, 0x7b, 0xa6, 0x2d, 0x8f, 0x47, 0xca, 0xee, 0xc5, 0x71,
+	0x69, 0xfb, 0x0e, 0xe0, 0xd0, 0xc7, 0xbf, 0x2f, 0x4b, 0xae, 0x6e, 0x0e, 0x95, 0x6f, 0x57, 0x80,
+	0x02, 0x59, 0x96, 0x39, 0x69, 0xe3, 0xfe, 0x66, 0x8a, 0x82, 0xa9, 0x7e, 0x64, 0xd9, 0x8c, 0x15,
+	0xa0, 0xfd, 0x69, 0x06, 0x54, 0xfc, 0x70, 0x06, 0xd6, 0xf1, 0x97, 0x23, 0xd6, 0x71, 0x79, 0x58,
+	0x27, 0x48, 0x88, 0x67, 0x50, 0x8e, 0x20, 0x1e, 0xdb, 0x5d, 0x4e, 0x03, 0xfa, 0xe4, 0xfc, 0xc0,
+	0xdf, 0x59, 0x50, 0x90, 0xfd, 0xce, 0xc0, 0xd0, 0x6e, 0x46, 0x0d, 0xed, 0x2b, 0x29, 0x56, 0x31,
+	0xc0, 0xc0, 0xfe, 0x4f, 0x56, 0xcf, 0x3e, 0x88, 0x1c, 0x9b, 0xc4, 0xab, 0xeb, 0x90, 0x28, 0x3c,
+	0x25, 0xa2, 0x11, 0x2b, 0x5a, 0x70, 0xb6, 0xc7, 0x4f, 0xe1, 0x6c, 0xff, 0x9a, 0xaa, 0x36, 0xa3,
+	0x8c, 0xd3, 0xfa, 0xad, 0x20, 0xf6, 0xc9, 0xa6, 0x2e, 0x9b, 0xd3, 0xa5, 0x7d, 0xe1, 0x03, 0x09,
+	0x8e, 0xa1, 0xe2, 0x3e, 0x3e, 0x22, 0x1e, 0xea, 0xc6, 0x8d, 0x99, 0x8e, 0x13, 0xde, 0x1c, 0xd1,
+	0x72, 0xaa, 0x78, 0xa8, 0xaf, 0x19, 0xf7, 0x33, 0x42, 0x4d, 0x98, 0x34, 0x0b, 0x7e, 0xb5, 0x9e,
+	0xae, 0xa4, 0xaf, 0x2c, 0x56, 0xaf, 0xf8, 0x66, 0x0b, 0x8e, 0x20, 0xdb, 0x07, 0x79, 0x28, 0x1a,
+	0x8a, 0x1d, 0xcb, 0x65, 0x4e, 0x9d, 0x4e, 0x2e, 0x33, 0x39, 0xf2, 0x2e, 0x8e, 0x14, 0x79, 0x5f,
+	0x8e, 0x46, 0xde, 0xcf, 0xc6, 0x23, 0x6f, 0x90, 0xab, 0x8b, 0x44, 0xdd, 0x0c, 0xce, 0xeb, 0x10,
+	0xd4, 0xaf, 0xdc, 0x56, 0xc9, 0x8c, 0x91, 0x03, 0x5d, 0x24, 0x3c, 0xcb, 0x5b, 0x11, 0x48, 0x1c,
+	0x63, 0x21, 0x3c, 0x53, 0xdd, 0x52, 0xed, 0xb5, 0xdb, 0xc4, 0xdb, 0x9f, 0x9f, 0x94, 0x13, 0x0e,
+	0x3c, 0xd3, 0x5b, 0x11, 0x2a, 0x8e, 0xf5, 0x46, 0x9b, 0x90, 0x57, 0x11, 0xac, 0xae, 0x06, 0x7e,
+	0x35, 0x4d, 0x70, 0xac, 0x3c, 0x73, 0xf5, 0x37, 0xd6, 0x38, 0x66, 0xf2, 0xa1, 0x70, 0x44, 0xf2,
+	0xe1, 0x2e, 0x20, 0x77, 0x5b, 0xc6, 0x00, 0xf5, 0xdb, 0xea, 0x13, 0x09, 0x42, 0x2b, 0xf3, 0x32,
+	0xb2, 0x0d, 0x36, 0xec, 0x5e, 0x5f, 0x0f, 0x9c, 0x30, 0x4a, 0x9c, 0x6a, 0x1d, 0xf6, 0x06, 0x47,
+	0x41, 0x27, 0x1a, 0xd2, 0x86, 0x4d, 0x61, 0x1c, 0x27, 0xab, 0x49, 0x2b, 0x31, 0x54, 0xdc, 0xc7,
+	0x07, 0x7d, 0x02, 0x53, 0x42, 0x85, 0x42, 0xc6, 0x70, 0x4c, 0xc6, 0xb3, 0x87, 0x07, 0x4b, 0x53,
+	0x1b, 0x26, 0x24, 0x8e, 0x72, 0xb0, 0xff, 0x30, 0x0b, 0xc9, 0x41, 0x77, 0xf8, 0x43, 0x16, 0xeb,
+	0x09, 0x3f, 0x64, 0x79, 0x1f, 0x0a, 0x8c, 0x13, 0x4f, 0xfd, 0xa2, 0x28, 0x33, 0xda, 0x2f, 0x8a,
+	0xaa, 0x3e, 0x00, 0x0e, 0xb1, 0x62, 0x19, 0x90, 0xec, 0x89, 0x66, 0x40, 0x56, 0x00, 0x64, 0x5c,
+	0x55, 0x71, 0x7b, 0xfa, 0x75, 0x7a, 0x2a, 0xb4, 0x09, 0x37, 0x03, 0x0a, 0x36, 0x7a, 0xa1, 0x6b,
+	0xc1, 0xa5, 0xac, 0x9e, 0xa3, 0x2f, 0xf5, 0xd5, 0x00, 0xc5, 0x73, 0x68, 0x09, 0xdf, 0x01, 0x38,
+	0xa2, 0x66, 0xd0, 0xfe, 0xbf, 0x0c, 0x44, 0x8c, 0x21, 0xfa, 0xae, 0x05, 0xb3, 0x24, 0xf6, 0xb1,
+	0x05, 0xdf, 0x97, 0xfc, 0xa5, 0x74, 0x5f, 0xc0, 0xe8, 0xfb, 0x56, 0x43, 0xf8, 0x32, 0x16, 0xef,
+	0xc2, 0x70, 0x3f, 0x53, 0xf4, 0x7b, 0x16, 0x5c, 0x20, 0xfd, 0x5f, 0xd3, 0xd0, 0x9b, 0xfe, 0xd6,
+	0xc8, 0x9f, 0xe3, 0x28, 0x7f, 0xed, 0xf0, 0x60, 0x29, 0xe9, 0x3b, 0x23, 0x38, 0x89, 0x1d, 0xfa,
+	0x10, 0xc6, 0x88, 0xd7, 0xf0, 0x53, 0xb0, 0xe9, 0xd9, 0xfa, 0x1f, 0x49, 0x09, 0x6f, 0xf4, 0x55,
+	0xaf, 0xc1, 0xb0, 0x04, 0xb5, 0x7f, 0x9c, 0x85, 0x99, 0xf8, 0x0f, 0x5f, 0x74, 0x59, 0xe9, 0x58,
+	0x62, 0x59, 0xa9, 0x38, 0x23, 0x35, 0x1e, 0xd4, 0x78, 0x86, 0x67, 0x44, 0x34, 0x62, 0x45, 0x0b,
+	0xce, 0x88, 0x2c, 0x47, 0xcf, 0x1d, 0xe3, 0x8c, 0xc8, 0x1a, 0xf4, 0x10, 0x0b, 0x5d, 0x8b, 0xde,
+	0x2d, 0x76, 0xfc, 0x6e, 0x99, 0x35, 0xd7, 0x32, 0x6a, 0x62, 0xb7, 0x0d, 0x45, 0x63, 0x1f, 0xf4,
+	0x49, 0xbc, 0x9e, 0x5a, 0xee, 0xa1, 0xda, 0x4d, 0xab, 0x4a, 0xa4, 0x90, 0x62, 0xe2, 0x87, 0xe7,
+	0x5e, 0x4a, 0xeb, 0x58, 0x99, 0x4f, 0x29, 0x2e, 0x03, 0xcd, 0xfe, 0x0f, 0x0b, 0xa6, 0x22, 0xb5,
+	0xce, 0x82, 0x9b, 0x5f, 0x53, 0x3e, 0xfa, 0x97, 0x45, 0xee, 0x07, 0x08, 0xd8, 0x40, 0x43, 0xdf,
+	0x82, 0x62, 0xcb, 0xed, 0x34, 0x28, 0xe3, 0x55, 0x97, 0xec, 0xea, 0x73, 0x92, 0x36, 0x8d, 0x34,
+	0x7f, 0x78, 0xb0, 0x34, 0xb7, 0xa1, 0x60, 0x2a, 0x6e, 0xbb, 0xdb, 0xa2, 0x5c, 0xfd, 0xfa, 0x00,
+	0x9b, 0xe0, 0xf2, 0x75, 0xf4, 0x7d, 0xe2, 0xd1, 0xa6, 0xdb, 0x63, 0xf4, 0x69, 0x7d, 0x1d, 0x0d,
+	0x26, 0x78, 0xd2, 0xaf, 0xa3, 0x21, 0xf0, 0xd1, 0xaf, 0xa3, 0x41, 0xdf, 0xa7, 0xf6, 0x75, 0x34,
+	0x98, 0xe1, 0x80, 0x28, 0xe8, 0x7f, 0x33, 0xc6, 0x2a, 0xa2, 0x91, 0x50, 0xe6, 0x09, 0x91, 0xd0,
+	0x47, 0x30, 0xe1, 0x74, 0x38, 0xf5, 0xf6, 0x48, 0x4b, 0xa7, 0x81, 0xd3, 0xea, 0x62, 0xb0, 0xd4,
+	0x75, 0x8d, 0x83, 0x03, 0x44, 0xd4, 0x82, 0x8b, 0xfe, 0xb3, 0x89, 0x47, 0x49, 0xf8, 0x6e, 0xab,
+	0x8b, 0xd4, 0xde, 0xf0, 0xf3, 0xfb, 0xb7, 0x92, 0x3a, 0x3d, 0x1e, 0x44, 0xc0, 0xc9, 0xa0, 0x88,
+	0xc1, 0x14, 0x33, 0x52, 0x00, 0xfe, 0x8d, 0x38, 0xe4, 0x93, 0x53, 0x3c, 0x6b, 0x62, 0x54, 0xb6,
+	0x99, 0xa0, 0x38, 0xca, 0xc3, 0xfe, 0xd7, 0x2c, 0x4c, 0xc7, 0x34, 0x2d, 0x16, 0x8e, 0x14, 0xce,
+	0x32, 0x1c, 0xc9, 0x8f, 0x14, 0x8e, 0x24, 0x7b, 0xca, 0x63, 0x23, 0x79, 0xca, 0x6f, 0x2b, 0x6f,
+	0x55, 0xef, 0xdc, 0xfa, 0x9a, 0xfe, 0xf5, 0x42, 0x20, 0xcd, 0x0d, 0x93, 0x88, 0xa3, 0x7d, 0xa5,
+	0x3b, 0x51, 0xef, 0xff, 0x4a, 0x85, 0x76, 0xb5, 0xdf, 0x4a, 0x5b, 0xc9, 0x19, 0x00, 0x28, 0x77,
+	0x22, 0x81, 0x80, 0x93, 0xd8, 0x95, 0xef, 0x7e, 0xfe, 0xd5, 0xe2, 0xb9, 0x2f, 0xbe, 0x5a, 0x3c,
+	0xf7, 0xe5, 0x57, 0x8b, 0xe7, 0xbe, 0x73, 0xb8, 0x68, 0x7d, 0x7e, 0xb8, 0x68, 0x7d, 0x71, 0xb8,
+	0x68, 0x7d, 0x79, 0xb8, 0x68, 0xfd, 0xe4, 0x70, 0xd1, 0xfa, 0xde, 0x4f, 0x17, 0xcf, 0x3d, 0x78,
+	0x61, 0x98, 0x6f, 0xdb, 0xfd, 0x7f, 0x00, 0x00, 0x00, 0xff, 0xff, 0x4e, 0x36, 0x5e, 0x9a, 0x02,
+	0x4f, 0x00, 0x00,
 }
 
 func (m *AnalysisRunArgument) Marshal() (dAtA []byte, err error) {
@@ -4117,7 +4177,7 @@ func (m *Project) MarshalToSizedBuffer(dAtA []byte) (int, error) {
 	return len(dAtA) - i, nil
 }
 
-func (m *ProjectList) Marshal() (dAtA []byte, err error) {
+func (m *ProjectConfig) Marshal() (dAtA []byte, err error) {
 	size := m.Size()
 	dAtA = make([]byte, size)
 	n, err := m.MarshalToSizedBuffer(dAtA[:size])
@@ -4127,12 +4187,55 @@ func (m *ProjectList) Marshal() (dAtA []byte, err error) {
 	return dAtA[:n], nil
 }
 
-func (m *ProjectList) MarshalTo(dAtA []byte) (int, error) {
+func (m *ProjectConfig) MarshalTo(dAtA []byte) (int, error) {
 	size := m.Size()
 	return m.MarshalToSizedBuffer(dAtA[:size])
 }
 
-func (m *ProjectList) MarshalToSizedBuffer(dAtA []byte) (int, error) {
+func (m *ProjectConfig) MarshalToSizedBuffer(dAtA []byte) (int, error) {
+	i := len(dAtA)
+	_ = i
+	var l int
+	_ = l
+	{
+		size, err := m.Spec.MarshalToSizedBuffer(dAtA[:i])
+		if err != nil {
+			return 0, err
+		}
+		i -= size
+		i = encodeVarintGenerated(dAtA, i, uint64(size))
+	}
+	i--
+	dAtA[i] = 0x12
+	{
+		size, err := m.ObjectMeta.MarshalToSizedBuffer(dAtA[:i])
+		if err != nil {
+			return 0, err
+		}
+		i -= size
+		i = encodeVarintGenerated(dAtA, i, uint64(size))
+	}
+	i--
+	dAtA[i] = 0xa
+	return len(dAtA) - i, nil
+}
+
+func (m *ProjectConfigList) 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 *ProjectConfigList) MarshalTo(dAtA []byte) (int, error) {
+	size := m.Size()
+	return m.MarshalToSizedBuffer(dAtA[:size])
+}
+
+func (m *ProjectConfigList) MarshalToSizedBuffer(dAtA []byte) (int, error) {
 	i := len(dAtA)
 	_ = i
 	var l int
@@ -4164,7 +4267,7 @@ func (m *ProjectList) MarshalToSizedBuffer(dAtA []byte) (int, error) {
 	return len(dAtA) - i, nil
 }
 
-func (m *ProjectSpec) Marshal() (dAtA []byte, err error) {
+func (m *ProjectConfigSpec) Marshal() (dAtA []byte, err error) {
 	size := m.Size()
 	dAtA = make([]byte, size)
 	n, err := m.MarshalToSizedBuffer(dAtA[:size])
@@ -4174,12 +4277,12 @@ func (m *ProjectSpec) Marshal() (dAtA []byte, err error) {
 	return dAtA[:n], nil
 }
 
-func (m *ProjectSpec) MarshalTo(dAtA []byte) (int, error) {
+func (m *ProjectConfigSpec) MarshalTo(dAtA []byte) (int, error) {
 	size := m.Size()
 	return m.MarshalToSizedBuffer(dAtA[:size])
 }
 
-func (m *ProjectSpec) MarshalToSizedBuffer(dAtA []byte) (int, error) {
+func (m *ProjectConfigSpec) MarshalToSizedBuffer(dAtA []byte) (int, error) {
 	i := len(dAtA)
 	_ = i
 	var l int
@@ -4201,6 +4304,53 @@ func (m *ProjectSpec) MarshalToSizedBuffer(dAtA []byte) (int, error) {
 	return len(dAtA) - i, nil
 }
 
+func (m *ProjectList) 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 *ProjectList) MarshalTo(dAtA []byte) (int, error) {
+	size := m.Size()
+	return m.MarshalToSizedBuffer(dAtA[:size])
+}
+
+func (m *ProjectList) MarshalToSizedBuffer(dAtA []byte) (int, error) {
+	i := len(dAtA)
+	_ = i
+	var l int
+	_ = l
+	if len(m.Items) > 0 {
+		for iNdEx := len(m.Items) - 1; iNdEx >= 0; iNdEx-- {
+			{
+				size, err := m.Items[iNdEx].MarshalToSizedBuffer(dAtA[:i])
+				if err != nil {
+					return 0, err
+				}
+				i -= size
+				i = encodeVarintGenerated(dAtA, i, uint64(size))
+			}
+			i--
+			dAtA[i] = 0x12
+		}
+	}
+	{
+		size, err := m.ListMeta.MarshalToSizedBuffer(dAtA[:i])
+		if err != nil {
+			return 0, err
+		}
+		i -= size
+		i = encodeVarintGenerated(dAtA, i, uint64(size))
+	}
+	i--
+	dAtA[i] = 0xa
+	return len(dAtA) - i, nil
+}
+
 func (m *ProjectStatus) Marshal() (dAtA []byte, err error) {
 	size := m.Size()
 	dAtA = make([]byte, size)
@@ -6564,7 +6714,20 @@ func (m *Project) Size() (n int) {
 	return n
 }
 
-func (m *ProjectList) Size() (n int) {
+func (m *ProjectConfig) Size() (n int) {
+	if m == nil {
+		return 0
+	}
+	var l int
+	_ = l
+	l = m.ObjectMeta.Size()
+	n += 1 + l + sovGenerated(uint64(l))
+	l = m.Spec.Size()
+	n += 1 + l + sovGenerated(uint64(l))
+	return n
+}
+
+func (m *ProjectConfigList) Size() (n int) {
 	if m == nil {
 		return 0
 	}
@@ -6581,7 +6744,7 @@ func (m *ProjectList) Size() (n int) {
 	return n
 }
 
-func (m *ProjectSpec) Size() (n int) {
+func (m *ProjectConfigSpec) Size() (n int) {
 	if m == nil {
 		return 0
 	}
@@ -6596,6 +6759,23 @@ func (m *ProjectSpec) Size() (n int) {
 	return n
 }
 
+func (m *ProjectList) Size() (n int) {
+	if m == nil {
+		return 0
+	}
+	var l int
+	_ = l
+	l = m.ListMeta.Size()
+	n += 1 + l + sovGenerated(uint64(l))
+	if len(m.Items) > 0 {
+		for _, e := range m.Items {
+			l = e.Size()
+			n += 1 + l + sovGenerated(uint64(l))
+		}
+	}
+	return n
+}
+
 func (m *ProjectStatus) Size() (n int) {
 	if m == nil {
 		return 0
@@ -7774,29 +7954,40 @@ func (this *Project) String() string {
 	}
 	s := strings.Join([]string{`&Project{`,
 		`ObjectMeta:` + strings.Replace(strings.Replace(fmt.Sprintf("%v", this.ObjectMeta), "ObjectMeta", "v1.ObjectMeta", 1), `&`, ``, 1) + `,`,
-		`Spec:` + strings.Replace(this.Spec.String(), "ProjectSpec", "ProjectSpec", 1) + `,`,
+		`Spec:` + strings.Replace(this.Spec.String(), "ProjectConfigSpec", "ProjectConfigSpec", 1) + `,`,
 		`Status:` + strings.Replace(strings.Replace(this.Status.String(), "ProjectStatus", "ProjectStatus", 1), `&`, ``, 1) + `,`,
 		`}`,
 	}, "")
 	return s
 }
-func (this *ProjectList) String() string {
+func (this *ProjectConfig) String() string {
 	if this == nil {
 		return "nil"
 	}
-	repeatedStringForItems := "[]Project{"
+	s := strings.Join([]string{`&ProjectConfig{`,
+		`ObjectMeta:` + strings.Replace(strings.Replace(fmt.Sprintf("%v", this.ObjectMeta), "ObjectMeta", "v1.ObjectMeta", 1), `&`, ``, 1) + `,`,
+		`Spec:` + strings.Replace(strings.Replace(this.Spec.String(), "ProjectConfigSpec", "ProjectConfigSpec", 1), `&`, ``, 1) + `,`,
+		`}`,
+	}, "")
+	return s
+}
+func (this *ProjectConfigList) String() string {
+	if this == nil {
+		return "nil"
+	}
+	repeatedStringForItems := "[]ProjectConfig{"
 	for _, f := range this.Items {
-		repeatedStringForItems += strings.Replace(strings.Replace(f.String(), "Project", "Project", 1), `&`, ``, 1) + ","
+		repeatedStringForItems += strings.Replace(strings.Replace(f.String(), "ProjectConfig", "ProjectConfig", 1), `&`, ``, 1) + ","
 	}
 	repeatedStringForItems += "}"
-	s := strings.Join([]string{`&ProjectList{`,
+	s := strings.Join([]string{`&ProjectConfigList{`,
 		`ListMeta:` + strings.Replace(strings.Replace(fmt.Sprintf("%v", this.ListMeta), "ListMeta", "v1.ListMeta", 1), `&`, ``, 1) + `,`,
 		`Items:` + repeatedStringForItems + `,`,
 		`}`,
 	}, "")
 	return s
 }
-func (this *ProjectSpec) String() string {
+func (this *ProjectConfigSpec) String() string {
 	if this == nil {
 		return "nil"
 	}
@@ -7805,12 +7996,28 @@ func (this *ProjectSpec) String() string {
 		repeatedStringForPromotionPolicies += strings.Replace(strings.Replace(f.String(), "PromotionPolicy", "PromotionPolicy", 1), `&`, ``, 1) + ","
 	}
 	repeatedStringForPromotionPolicies += "}"
-	s := strings.Join([]string{`&ProjectSpec{`,
+	s := strings.Join([]string{`&ProjectConfigSpec{`,
 		`PromotionPolicies:` + repeatedStringForPromotionPolicies + `,`,
 		`}`,
 	}, "")
 	return s
 }
+func (this *ProjectList) String() string {
+	if this == nil {
+		return "nil"
+	}
+	repeatedStringForItems := "[]Project{"
+	for _, f := range this.Items {
+		repeatedStringForItems += strings.Replace(strings.Replace(f.String(), "Project", "Project", 1), `&`, ``, 1) + ","
+	}
+	repeatedStringForItems += "}"
+	s := strings.Join([]string{`&ProjectList{`,
+		`ListMeta:` + strings.Replace(strings.Replace(fmt.Sprintf("%v", this.ListMeta), "ListMeta", "v1.ListMeta", 1), `&`, ``, 1) + `,`,
+		`Items:` + repeatedStringForItems + `,`,
+		`}`,
+	}, "")
+	return s
+}
 func (this *ProjectStatus) String() string {
 	if this == nil {
 		return "nil"
@@ -14818,7 +15025,7 @@ func (m *Project) Unmarshal(dAtA []byte) error {
 				return io.ErrUnexpectedEOF
 			}
 			if m.Spec == nil {
-				m.Spec = &ProjectSpec{}
+				m.Spec = &ProjectConfigSpec{}
 			}
 			if err := m.Spec.Unmarshal(dAtA[iNdEx:postIndex]); err != nil {
 				return err
@@ -14878,7 +15085,7 @@ func (m *Project) Unmarshal(dAtA []byte) error {
 	}
 	return nil
 }
-func (m *ProjectList) Unmarshal(dAtA []byte) error {
+func (m *ProjectConfig) Unmarshal(dAtA []byte) error {
 	l := len(dAtA)
 	iNdEx := 0
 	for iNdEx < l {
@@ -14901,10 +15108,126 @@ func (m *ProjectList) Unmarshal(dAtA []byte) error {
 		fieldNum := int32(wire >> 3)
 		wireType := int(wire & 0x7)
 		if wireType == 4 {
-			return fmt.Errorf("proto: ProjectList: wiretype end group for non-group")
+			return fmt.Errorf("proto: ProjectConfig: wiretype end group for non-group")
 		}
 		if fieldNum <= 0 {
-			return fmt.Errorf("proto: ProjectList: illegal tag %d (wire type %d)", fieldNum, wire)
+			return fmt.Errorf("proto: ProjectConfig: illegal tag %d (wire type %d)", fieldNum, wire)
+		}
+		switch fieldNum {
+		case 1:
+			if wireType != 2 {
+				return fmt.Errorf("proto: wrong wireType = %d for field ObjectMeta", 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
+			}
+			if err := m.ObjectMeta.Unmarshal(dAtA[iNdEx:postIndex]); err != nil {
+				return err
+			}
+			iNdEx = postIndex
+		case 2:
+			if wireType != 2 {
+				return fmt.Errorf("proto: wrong wireType = %d for field Spec", 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
+			}
+			if err := m.Spec.Unmarshal(dAtA[iNdEx:postIndex]); err != nil {
+				return err
+			}
+			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 *ProjectConfigList) 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: ProjectConfigList: wiretype end group for non-group")
+		}
+		if fieldNum <= 0 {
+			return fmt.Errorf("proto: ProjectConfigList: illegal tag %d (wire type %d)", fieldNum, wire)
 		}
 		switch fieldNum {
 		case 1:
@@ -14969,7 +15292,7 @@ func (m *ProjectList) Unmarshal(dAtA []byte) error {
 			if postIndex > l {
 				return io.ErrUnexpectedEOF
 			}
-			m.Items = append(m.Items, Project{})
+			m.Items = append(m.Items, ProjectConfig{})
 			if err := m.Items[len(m.Items)-1].Unmarshal(dAtA[iNdEx:postIndex]); err != nil {
 				return err
 			}
@@ -14995,7 +15318,7 @@ func (m *ProjectList) Unmarshal(dAtA []byte) error {
 	}
 	return nil
 }
-func (m *ProjectSpec) Unmarshal(dAtA []byte) error {
+func (m *ProjectConfigSpec) Unmarshal(dAtA []byte) error {
 	l := len(dAtA)
 	iNdEx := 0
 	for iNdEx < l {
@@ -15018,10 +15341,10 @@ func (m *ProjectSpec) Unmarshal(dAtA []byte) error {
 		fieldNum := int32(wire >> 3)
 		wireType := int(wire & 0x7)
 		if wireType == 4 {
-			return fmt.Errorf("proto: ProjectSpec: wiretype end group for non-group")
+			return fmt.Errorf("proto: ProjectConfigSpec: wiretype end group for non-group")
 		}
 		if fieldNum <= 0 {
-			return fmt.Errorf("proto: ProjectSpec: illegal tag %d (wire type %d)", fieldNum, wire)
+			return fmt.Errorf("proto: ProjectConfigSpec: illegal tag %d (wire type %d)", fieldNum, wire)
 		}
 		switch fieldNum {
 		case 1:
@@ -15079,6 +15402,123 @@ func (m *ProjectSpec) Unmarshal(dAtA []byte) error {
 	}
 	return nil
 }
+func (m *ProjectList) 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: ProjectList: wiretype end group for non-group")
+		}
+		if fieldNum <= 0 {
+			return fmt.Errorf("proto: ProjectList: illegal tag %d (wire type %d)", fieldNum, wire)
+		}
+		switch fieldNum {
+		case 1:
+			if wireType != 2 {
+				return fmt.Errorf("proto: wrong wireType = %d for field ListMeta", 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
+			}
+			if err := m.ListMeta.Unmarshal(dAtA[iNdEx:postIndex]); err != nil {
+				return err
+			}
+			iNdEx = postIndex
+		case 2:
+			if wireType != 2 {
+				return fmt.Errorf("proto: wrong wireType = %d for field Items", 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.Items = append(m.Items, Project{})
+			if err := m.Items[len(m.Items)-1].Unmarshal(dAtA[iNdEx:postIndex]); err != nil {
+				return err
+			}
+			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 *ProjectStatus) Unmarshal(dAtA []byte) error {
 	l := len(dAtA)
 	iNdEx := 0
diff --git a/api/v1alpha1/generated.proto b/api/v1alpha1/generated.proto
index b72d98f9..f9e004fe 100644
--- a/api/v1alpha1/generated.proto
+++ b/api/v1alpha1/generated.proto
@@ -817,26 +817,49 @@ message Project {
   optional .k8s.io.apimachinery.pkg.apis.meta.v1.ObjectMeta metadata = 1;
 
   // Spec describes a Project.
-  optional ProjectSpec spec = 2;
+  //
+  // Deprecated: Create a ProjectConfig resource with the same name as the
+  // Project resource in the Project's namespace. The ProjectConfig resource
+  // can be used to configure the Project.
+  optional ProjectConfigSpec spec = 2;
 
   // Status describes the Project's current status.
   optional ProjectStatus status = 3;
 }
 
-// ProjectList is a list of Project resources.
-message ProjectList {
+// ProjectConfig is a resource type that describes the configuration of a
+// Project.
+message ProjectConfig {
+  optional .k8s.io.apimachinery.pkg.apis.meta.v1.ObjectMeta metadata = 1;
+
+  // Spec describes the configuration of a Project.
+  optional ProjectConfigSpec spec = 2;
+}
+
+// ProjectConfigList is a list of ProjectConfig resources.
+message ProjectConfigList {
   optional .k8s.io.apimachinery.pkg.apis.meta.v1.ListMeta metadata = 1;
 
-  repeated Project items = 2;
+  repeated ProjectConfig items = 2;
 }
 
-// ProjectSpec describes a Project.
-message ProjectSpec {
+// ProjectSpec is a deprecated alias for ProjectConfigSpec. It is retained for
+// backwards compatibility.
+//
+// Deprecated: Use ProjectConfigSpec instead.
+message ProjectConfigSpec {
   // PromotionPolicies defines policies governing the promotion of Freight to
-  // specific Stages within this Project.
+  // specific Stages within the Project.
   repeated PromotionPolicy promotionPolicies = 1;
 }
 
+// ProjectList is a list of Project resources.
+message ProjectList {
+  optional .k8s.io.apimachinery.pkg.apis.meta.v1.ListMeta metadata = 1;
+
+  repeated Project items = 2;
+}
+
 // ProjectStatus describes a Project's current status.
 message ProjectStatus {
   // Conditions contains the last observations of the Project's current
diff --git a/api/v1alpha1/groupversion_info.go b/api/v1alpha1/groupversion_info.go
index 8de4266c..253f4f61 100644
--- a/api/v1alpha1/groupversion_info.go
+++ b/api/v1alpha1/groupversion_info.go
@@ -35,6 +35,8 @@ func addKnownTypes(scheme *runtime.Scheme) error {
 		&StageList{},
 		&Project{},
 		&ProjectList{},
+		&ProjectConfig{},
+		&ProjectConfigList{},
 		&Promotion{},
 		&PromotionList{},
 		&PromotionTask{},
diff --git a/api/v1alpha1/project_config_types.go b/api/v1alpha1/project_config_types.go
new file mode 100644
index 00000000..f67fd600
--- /dev/null
+++ b/api/v1alpha1/project_config_types.go
@@ -0,0 +1,48 @@
+package v1alpha1
+
+import metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+
+// +kubebuilder:object:root=true
+// +kubebuilder:resource:scope=Namespaced
+// +kubebuilder:subresource:status
+
+// ProjectConfig is a resource type that describes the configuration of a
+// Project.
+type ProjectConfig struct {
+	metav1.TypeMeta   `json:",inline"`
+	metav1.ObjectMeta `json:"metadata,omitempty" protobuf:"bytes,1,opt,name=metadata"`
+	// Spec describes the configuration of a Project.
+	Spec ProjectConfigSpec `json:"spec,omitempty" protobuf:"bytes,2,opt,name=spec"`
+}
+
+// ProjectConfigSpec describes the configuration of a Project.
+type ProjectConfigSpec struct {
+	// PromotionPolicies defines policies governing the promotion of Freight to
+	// specific Stages within the Project.
+	PromotionPolicies []PromotionPolicy `json:"promotionPolicies,omitempty" protobuf:"bytes,1,rep,name=promotionPolicies"`
+}
+
+// PromotionPolicy defines policies governing the promotion of Freight to a
+// specific Stage.
+type PromotionPolicy struct {
+	// +kubebuilder:validation:MinLength=1
+	// +kubebuilder:validation:Pattern=^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$
+	Stage string `json:"stage" protobuf:"bytes,1,opt,name=stage"`
+	// AutoPromotionEnabled indicates whether new Freight can automatically be
+	// promoted into the Stage referenced by the Stage field. Note: There are may
+	// be other conditions also required for an auto-promotion to occur. This
+	// field defaults to false, but is commonly set to true for Stages that
+	// subscribe to Warehouses instead of other, upstream Stages. This allows
+	// users to define Stages that are automatically updated as soon as new
+	// artifacts are detected.
+	AutoPromotionEnabled bool `json:"autoPromotionEnabled,omitempty" protobuf:"varint,2,opt,name=autoPromotionEnabled"`
+}
+
+// +kubebuilder:object:root=true
+
+// ProjectConfigList is a list of ProjectConfig resources.
+type ProjectConfigList struct {
+	metav1.TypeMeta `json:",inline"`
+	metav1.ListMeta `json:"metadata,omitempty" protobuf:"bytes,1,opt,name=metadata"`
+	Items           []ProjectConfig `json:"items" protobuf:"bytes,2,rep,name=items"`
+}
diff --git a/api/v1alpha1/project_types.go b/api/v1alpha1/project_types.go
index bfe95cba..83469b32 100644
--- a/api/v1alpha1/project_types.go
+++ b/api/v1alpha1/project_types.go
@@ -40,38 +40,25 @@ type Project struct {
 	metav1.TypeMeta   `json:",inline"`
 	metav1.ObjectMeta `json:"metadata,omitempty" protobuf:"bytes,1,opt,name=metadata"`
 	// Spec describes a Project.
+	//
+	// Deprecated: Create a ProjectConfig resource with the same name as the
+	// Project resource in the Project's namespace. The ProjectConfig resource
+	// can be used to configure the Project.
 	Spec *ProjectSpec `json:"spec,omitempty" protobuf:"bytes,2,opt,name=spec"`
 	// Status describes the Project's current status.
 	Status ProjectStatus `json:"status,omitempty" protobuf:"bytes,3,opt,name=status"`
 }
 
+// ProjectSpec is a deprecated alias for ProjectConfigSpec. It is retained for
+// backwards compatibility.
+//
+// Deprecated: Use ProjectConfigSpec instead.
+type ProjectSpec = ProjectConfigSpec
+
 func (p *Project) GetStatus() *ProjectStatus {
 	return &p.Status
 }
 
-// ProjectSpec describes a Project.
-type ProjectSpec struct {
-	// PromotionPolicies defines policies governing the promotion of Freight to
-	// specific Stages within this Project.
-	PromotionPolicies []PromotionPolicy `json:"promotionPolicies,omitempty" protobuf:"bytes,1,rep,name=promotionPolicies"`
-}
-
-// PromotionPolicy defines policies governing the promotion of Freight to a
-// specific Stage.
-type PromotionPolicy struct {
-	// +kubebuilder:validation:MinLength=1
-	// +kubebuilder:validation:Pattern=^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$
-	Stage string `json:"stage" protobuf:"bytes,1,opt,name=stage"`
-	// AutoPromotionEnabled indicates whether new Freight can automatically be
-	// promoted into the Stage referenced by the Stage field. Note: There are may
-	// be other conditions also required for an auto-promotion to occur. This
-	// field defaults to false, but is commonly set to true for Stages that
-	// subscribe to Warehouses instead of other, upstream Stages. This allows
-	// users to define Stages that are automatically updated as soon as new
-	// artifacts are detected.
-	AutoPromotionEnabled bool `json:"autoPromotionEnabled,omitempty" protobuf:"varint,2,opt,name=autoPromotionEnabled"`
-}
-
 // ProjectStatus describes a Project's current status.
 type ProjectStatus struct {
 	// Conditions contains the last observations of the Project's current
diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go
index 10f4fc31..f2863950 100644
--- a/api/v1alpha1/zz_generated.deepcopy.go
+++ b/api/v1alpha1/zz_generated.deepcopy.go
@@ -848,31 +848,57 @@ func (in *Project) DeepCopyObject() runtime.Object {
 }
 
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
-func (in *ProjectList) DeepCopyInto(out *ProjectList) {
+func (in *ProjectConfig) DeepCopyInto(out *ProjectConfig) {
+	*out = *in
+	out.TypeMeta = in.TypeMeta
+	in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
+	in.Spec.DeepCopyInto(&out.Spec)
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ProjectConfig.
+func (in *ProjectConfig) DeepCopy() *ProjectConfig {
+	if in == nil {
+		return nil
+	}
+	out := new(ProjectConfig)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
+func (in *ProjectConfig) DeepCopyObject() runtime.Object {
+	if c := in.DeepCopy(); c != nil {
+		return c
+	}
+	return nil
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *ProjectConfigList) DeepCopyInto(out *ProjectConfigList) {
 	*out = *in
 	out.TypeMeta = in.TypeMeta
 	in.ListMeta.DeepCopyInto(&out.ListMeta)
 	if in.Items != nil {
 		in, out := &in.Items, &out.Items
-		*out = make([]Project, len(*in))
+		*out = make([]ProjectConfig, len(*in))
 		for i := range *in {
 			(*in)[i].DeepCopyInto(&(*out)[i])
 		}
 	}
 }
 
-// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ProjectList.
-func (in *ProjectList) DeepCopy() *ProjectList {
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ProjectConfigList.
+func (in *ProjectConfigList) DeepCopy() *ProjectConfigList {
 	if in == nil {
 		return nil
 	}
-	out := new(ProjectList)
+	out := new(ProjectConfigList)
 	in.DeepCopyInto(out)
 	return out
 }
 
 // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
-func (in *ProjectList) DeepCopyObject() runtime.Object {
+func (in *ProjectConfigList) DeepCopyObject() runtime.Object {
 	if c := in.DeepCopy(); c != nil {
 		return c
 	}
@@ -880,7 +906,7 @@ func (in *ProjectList) DeepCopyObject() runtime.Object {
 }
 
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
-func (in *ProjectSpec) DeepCopyInto(out *ProjectSpec) {
+func (in *ProjectConfigSpec) DeepCopyInto(out *ProjectConfigSpec) {
 	*out = *in
 	if in.PromotionPolicies != nil {
 		in, out := &in.PromotionPolicies, &out.PromotionPolicies
@@ -889,16 +915,48 @@ func (in *ProjectSpec) DeepCopyInto(out *ProjectSpec) {
 	}
 }
 
-// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ProjectSpec.
-func (in *ProjectSpec) DeepCopy() *ProjectSpec {
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ProjectConfigSpec.
+func (in *ProjectConfigSpec) DeepCopy() *ProjectConfigSpec {
 	if in == nil {
 		return nil
 	}
-	out := new(ProjectSpec)
+	out := new(ProjectConfigSpec)
 	in.DeepCopyInto(out)
 	return out
 }
 
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *ProjectList) DeepCopyInto(out *ProjectList) {
+	*out = *in
+	out.TypeMeta = in.TypeMeta
+	in.ListMeta.DeepCopyInto(&out.ListMeta)
+	if in.Items != nil {
+		in, out := &in.Items, &out.Items
+		*out = make([]Project, len(*in))
+		for i := range *in {
+			(*in)[i].DeepCopyInto(&(*out)[i])
+		}
+	}
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ProjectList.
+func (in *ProjectList) DeepCopy() *ProjectList {
+	if in == nil {
+		return nil
+	}
+	out := new(ProjectList)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
+func (in *ProjectList) DeepCopyObject() runtime.Object {
+	if c := in.DeepCopy(); c != nil {
+		return c
+	}
+	return nil
+}
+
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *ProjectStatus) DeepCopyInto(out *ProjectStatus) {
 	*out = *in
diff --git a/charts/kargo/resources/crds/kargo.akuity.io_projectconfigs.yaml b/charts/kargo/resources/crds/kargo.akuity.io_projectconfigs.yaml
new file mode 100644
index 00000000..60511a40
--- /dev/null
+++ b/charts/kargo/resources/crds/kargo.akuity.io_projectconfigs.yaml
@@ -0,0 +1,76 @@
+---
+apiVersion: apiextensions.k8s.io/v1
+kind: CustomResourceDefinition
+metadata:
+  annotations:
+    controller-gen.kubebuilder.io/version: v0.17.3
+  name: projectconfigs.kargo.akuity.io
+spec:
+  group: kargo.akuity.io
+  names:
+    kind: ProjectConfig
+    listKind: ProjectConfigList
+    plural: projectconfigs
+    singular: projectconfig
+  scope: Namespaced
+  versions:
+  - name: v1alpha1
+    schema:
+      openAPIV3Schema:
+        description: |-
+          ProjectConfig is a resource type that describes the configuration of a
+          Project.
+        properties:
+          apiVersion:
+            description: |-
+              APIVersion defines the versioned schema of this representation of an object.
+              Servers should convert recognized schemas to the latest internal value, and
+              may reject unrecognized values.
+              More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources
+            type: string
+          kind:
+            description: |-
+              Kind is a string value representing the REST resource this object represents.
+              Servers may infer this from the endpoint the client submits requests to.
+              Cannot be updated.
+              In CamelCase.
+              More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds
+            type: string
+          metadata:
+            type: object
+          spec:
+            description: Spec describes the configuration of a Project.
+            properties:
+              promotionPolicies:
+                description: |-
+                  PromotionPolicies defines policies governing the promotion of Freight to
+                  specific Stages within the Project.
+                items:
+                  description: |-
+                    PromotionPolicy defines policies governing the promotion of Freight to a
+                    specific Stage.
+                  properties:
+                    autoPromotionEnabled:
+                      description: |-
+                        AutoPromotionEnabled indicates whether new Freight can automatically be
+                        promoted into the Stage referenced by the Stage field. Note: There are may
+                        be other conditions also required for an auto-promotion to occur. This
+                        field defaults to false, but is commonly set to true for Stages that
+                        subscribe to Warehouses instead of other, upstream Stages. This allows
+                        users to define Stages that are automatically updated as soon as new
+                        artifacts are detected.
+                      type: boolean
+                    stage:
+                      minLength: 1
+                      pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$
+                      type: string
+                  required:
+                  - stage
+                  type: object
+                type: array
+            type: object
+        type: object
+    served: true
+    storage: true
+    subresources:
+      status: {}
diff --git a/charts/kargo/resources/crds/kargo.akuity.io_projects.yaml b/charts/kargo/resources/crds/kargo.akuity.io_projects.yaml
index 0d60211f..4f09a30a 100644
--- a/charts/kargo/resources/crds/kargo.akuity.io_projects.yaml
+++ b/charts/kargo/resources/crds/kargo.akuity.io_projects.yaml
@@ -49,12 +49,17 @@ spec:
           metadata:
             type: object
           spec:
-            description: Spec describes a Project.
+            description: |-
+              Spec describes a Project.
+
+              Deprecated: Create a ProjectConfig resource with the same name as the
+              Project resource in the Project's namespace. The ProjectConfig resource
+              can be used to configure the Project.
             properties:
               promotionPolicies:
                 description: |-
                   PromotionPolicies defines policies governing the promotion of Freight to
-                  specific Stages within this Project.
+                  specific Stages within the Project.
                 items:
                   description: |-
                     PromotionPolicy defines policies governing the promotion of Freight to a
diff --git a/charts/kargo/templates/controller/cluster-roles.yaml b/charts/kargo/templates/controller/cluster-roles.yaml
index 45907b11..db3b4ec0 100644
--- a/charts/kargo/templates/controller/cluster-roles.yaml
+++ b/charts/kargo/templates/controller/cluster-roles.yaml
@@ -36,6 +36,7 @@ rules:
   - kargo.akuity.io
   resources:
   - projects
+  - projectconfigs
   verbs:
   - get
   - list
diff --git a/charts/kargo/templates/management-controller/cluster-roles.yaml b/charts/kargo/templates/management-controller/cluster-roles.yaml
index 3fcba234..d3e8a086 100644
--- a/charts/kargo/templates/management-controller/cluster-roles.yaml
+++ b/charts/kargo/templates/management-controller/cluster-roles.yaml
@@ -41,10 +41,17 @@ rules:
   - get
   - list
   - watch
+  - update
 - apiGroups:
   - kargo.akuity.io
   resources:
   - projects/status
   verbs:
   - patch
+- apiGroups:
+  - kargo.akuity.io
+  resources:
+  - projectconfigs
+  verbs:
+  - create
 {{- end }}  
diff --git a/charts/kargo/templates/users/cluster-roles.yaml b/charts/kargo/templates/users/cluster-roles.yaml
index 03096b77..1e7666ff 100644
--- a/charts/kargo/templates/users/cluster-roles.yaml
+++ b/charts/kargo/templates/users/cluster-roles.yaml
@@ -32,6 +32,7 @@ rules:
   - clusterpromotiontasks
   - freights
   - projects
+  - projectconfigs
   - promotiontasks
   - stages
   - warehouses
@@ -116,6 +117,7 @@ rules:
   - clusterpromotiontasks
   - freights
   - projects
+  - projectconfigs
   - promotions
   - promotiontasks
   - stages
diff --git a/charts/kargo/templates/webhooks-server/cluster-role.yaml b/charts/kargo/templates/webhooks-server/cluster-role.yaml
index ffe528fa..20751789 100644
--- a/charts/kargo/templates/webhooks-server/cluster-role.yaml
+++ b/charts/kargo/templates/webhooks-server/cluster-role.yaml
@@ -45,6 +45,7 @@ rules:
   - clusterpromotiontasks
   - freights
   - projects
+  - projectconfigs
   - promotiontasks
   - stages
   - warehouses
diff --git a/charts/kargo/templates/webhooks/webhooks.yaml b/charts/kargo/templates/webhooks/webhooks.yaml
index cb728872..413bd2f2 100644
--- a/charts/kargo/templates/webhooks/webhooks.yaml
+++ b/charts/kargo/templates/webhooks/webhooks.yaml
@@ -108,7 +108,22 @@ webhooks:
     apiGroups: ["kargo.akuity.io"]
     apiVersions: ["v1alpha1"]
     resources: ["projects"]
-    operations: ["CREATE"]
+    operations: ["CREATE", "UPDATE"]
+  failurePolicy: Fail
+- name: projectconfig.kargo.akuity.io
+  admissionReviewVersions: ["v1"]
+  sideEffects: NoneOnDryRun
+  clientConfig:
+    service:
+      namespace: {{ .Release.Namespace }}
+      name: kargo-webhooks-server
+      path: /validate-kargo-akuity-io-v1alpha1-projectconfig
+  rules:
+    - scope: Namespaced
+      apiGroups: ["kargo.akuity.io"]
+      apiVersions: ["v1alpha1"]
+      resources: ["projectconfigs"]
+      operations: ["CREATE", "UPDATE"]
   failurePolicy: Fail
 - name: promotion.kargo.akuity.io
   admissionReviewVersions: ["v1"]
diff --git a/cmd/controlplane/webhooks.go b/cmd/controlplane/webhooks.go
index eab9c277..204f6c0a 100644
--- a/cmd/controlplane/webhooks.go
+++ b/cmd/controlplane/webhooks.go
@@ -22,6 +22,7 @@ import (
 	libWebhook "github.com/akuity/kargo/internal/webhook"
 	"github.com/akuity/kargo/internal/webhook/freight"
 	"github.com/akuity/kargo/internal/webhook/project"
+	"github.com/akuity/kargo/internal/webhook/projectconfig"
 	"github.com/akuity/kargo/internal/webhook/promotion"
 	"github.com/akuity/kargo/internal/webhook/promotiontask"
 	"github.com/akuity/kargo/internal/webhook/stage"
@@ -133,6 +134,9 @@ func (o *webhooksServerOptions) run(ctx context.Context) error {
 	); err != nil {
 		return fmt.Errorf("setup Project webhook: %w", err)
 	}
+	if err = projectconfig.SetupWebhookWithManager(mgr); err != nil {
+		return fmt.Errorf("setup ProjectConfig webhook: %w", err)
+	}
 	if err = promotion.SetupWebhookWithManager(ctx, webhookCfg, mgr); err != nil {
 		return fmt.Errorf("setup Promotion webhook: %w", err)
 	}
diff --git a/docs/docs/50-user-guide/20-how-to-guides/20-working-with-projects.md b/docs/docs/50-user-guide/20-how-to-guides/20-working-with-projects.md
index 23ab7833..865411c2 100644
--- a/docs/docs/50-user-guide/20-how-to-guides/20-working-with-projects.md
+++ b/docs/docs/50-user-guide/20-how-to-guides/20-working-with-projects.md
@@ -45,29 +45,33 @@ creation. This includes things such as project-level RBAC resources and
 
 ## Promotion Policies
 
-A `Project` resource can additionally define project-level configuration. At
-present, this only includes **promotion policies** that describe which `Stage`s
-are eligible for automatic promotion of newly available `Freight`.
+A `ProjectConfig` resource defines project-level configuration for an associated
+`Project`. At present, this only includes **promotion policies** that describe
+which `Stage`s are eligible for automatic promotion of newly available `Freight`.
 
-:::note
-Promotion policies are defined at the project-level because users with
-permission to update `Stage` resources in a given project `Namespace` may _not_
-have permission to create `Promotion` resources. Defining promotion policies at
-the project-level therefore restricts such users from enabling automatic
-promotions for a `Stage` to which they may lack permission to promote to
-manually. It leaves decisions about eligibility for auto-promotion squarely in
-the hands of someone like a "project admin."
-:::
+The `ProjectConfig` resource must have the same name as its associated `Project`
+and be created in the `Namespace` of the `Project`. This separation of
+configuration from the `Project` resource enables more granular RBAC control.
+Users can be granted permission to modify project configurations via
+`ProjectConfig` resources without necessarily having broader access to `Project`
+resources themselves.
 
 In the example below, the `test` and `uat` `Stage`s are eligible for automatic
 promotion of newly available `Freight`, but any other `Stage`s in the `Project`
 are not:
 
 ```yaml
+---
+apiVersion: kargo.akuity.io/v1alpha1
+kind: Project
+metadata:
+   name: example
+---
 apiVersion: kargo.akuity.io/v1alpha1
 kind: Project
 metadata:
   name: example
+  namespace: example
 spec:
   promotionPolicies:
   - stage: test
@@ -86,11 +90,13 @@ This enables pre-configuring such `Namespace`s according to your
 own requirements.
 
 :::info
-Requiring a `Namespace` to have the `kargo.akuity.io/project: "true"` label to be eligible for adoption by a new `Project` is intended to prevent accidental or willful hijacking of an existing `Namespace`.
+Requiring a `Namespace` to have the `kargo.akuity.io/project: "true"` label to
+be eligible for adoption by a new `Project` is intended to prevent accidental or
+willful hijacking of an existing `Namespace`.
 :::
 
 The following example demonstrates adoption of a `Namespace` that's been
-pre-configured with with a label unrelated to Kargo:
+pre-configured with a label unrelated to Kargo:
 
 ```yaml
 apiVersion: v1
diff --git a/docs/old-docs/30-how-to-guides/11-working-with-projects.md b/docs/old-docs/30-how-to-guides/11-working-with-projects.md
index 7e3ca6e4..5971032c 100644
--- a/docs/old-docs/30-how-to-guides/11-working-with-projects.md
+++ b/docs/old-docs/30-how-to-guides/11-working-with-projects.md
@@ -45,29 +45,33 @@ creation. This includes things such as project-level RBAC resources and
 
 ## Promotion Policies
 
-A `Project` resource can additionally define project-level configuration. At
-present, this only includes **promotion policies** that describe which `Stage`s
-are eligible for automatic promotion of newly available `Freight`.
+A `ProjectConfig` resource defines project-level configuration for an associated
+`Project`. At present, this only includes **promotion policies** that describe
+which `Stage`s are eligible for automatic promotion of newly available `Freight`.
 
-:::note
-Promotion policies are defined at the project-level because users with
-permission to update `Stage` resources in a given project `Namespace` may _not_
-have permission to create `Promotion` resources. Defining promotion policies at
-the project-level therefore restricts such users from enabling automatic
-promotions for a `Stage` to which they may lack permission to promote to
-manually. It leaves decisions about eligibility for auto-promotion squarely in
-the hands of someone like a "project admin."
-:::
+The `ProjectConfig` resource must have the same name as its associated `Project`
+and be created in the `Namespace` of the `Project`. This separation of
+configuration from the `Project` resource enables more granular RBAC control.
+Users can be granted permission to modify project configurations via
+`ProjectConfig` resources without necessarily having broader access to `Project`
+resources themselves.
 
 In the example below, the `test` and `uat` `Stage`s are eligible for automatic
 promotion of newly available `Freight`, but any other `Stage`s in the `Project`
 are not:
 
 ```yaml
+---
+apiVersion: kargo.akuity.io/v1alpha1
+kind: Project
+metadata:
+   name: example
+---
 apiVersion: kargo.akuity.io/v1alpha1
 kind: Project
 metadata:
   name: example
+  namespace: example
 spec:
   promotionPolicies:
   - stage: test
@@ -86,11 +90,13 @@ This enables pre-configuring such `Namespace`s according to your
 own requirements.
 
 :::info
-Requiring a `Namespace` to have the `kargo.akuity.io/project: "true"` label to be eligible for adoption by a new `Project` is intended to prevent accidental or willful hijacking of an existing `Namespace`.
+Requiring a `Namespace` to have the `kargo.akuity.io/project: "true"` label to
+be eligible for adoption by a new `Project` is intended to prevent accidental or
+willful hijacking of an existing `Namespace`.
 :::
 
 The following example demonstrates adoption of a `Namespace` that's been
-pre-configured with with a label unrelated to Kargo:
+pre-configured with a label unrelated to Kargo:
 
 ```yaml
 apiVersion: v1
diff --git a/internal/controller/management/projects/projects.go b/internal/controller/management/projects/projects.go
index 0eaecd18..121ce728 100644
--- a/internal/controller/management/projects/projects.go
+++ b/internal/controller/management/projects/projects.go
@@ -72,6 +72,12 @@ type reconciler struct {
 		*kargoapi.Project,
 	) (kargoapi.ProjectStatus, error)
 
+	updateProjectFn func(
+		context.Context,
+		client.Object,
+		...client.UpdateOption,
+	) error
+
 	patchProjectStatusFn func(
 		context.Context,
 		*kargoapi.Project,
@@ -126,6 +132,12 @@ type reconciler struct {
 		client.Object,
 		...client.CreateOption,
 	) error
+
+	createProjectConfigFn func(
+		context.Context,
+		client.Object,
+		...client.CreateOption,
+	) error
 }
 
 // SetupReconcilerWithManager initializes a reconciler for Project resources and
@@ -166,6 +178,7 @@ func newReconciler(kubeClient client.Client, cfg ReconcilerConfig) *reconciler {
 	r.getProjectFn = api.GetProject
 	r.syncProjectFn = r.syncProject
 	r.ensureNamespaceFn = r.ensureNamespace
+	r.updateProjectFn = r.client.Update
 	r.patchProjectStatusFn = r.patchProjectStatus
 	r.getNamespaceFn = r.client.Get
 	r.createNamespaceFn = r.client.Create
@@ -177,6 +190,7 @@ func newReconciler(kubeClient client.Client, cfg ReconcilerConfig) *reconciler {
 	r.createServiceAccountFn = r.client.Create
 	r.createRoleFn = r.client.Create
 	r.createRoleBindingFn = r.client.Create
+	r.createProjectConfigFn = r.client.Create
 	return r
 }
 
@@ -214,6 +228,35 @@ func (r *reconciler) Reconcile(
 		return ctrl.Result{Requeue: true}, patchErr
 	}
 
+	// TODO(hidde): Remove this migration code when the spec field is removed
+	// from the Project.
+	if projectCfg, ok := migrateSpecToProjectConfig(project); ok {
+		// If we receive a ProjectConfig, we need to create it.
+		if projectCfg != nil {
+			if err = r.createProjectConfigFn(ctx, projectCfg); err != nil {
+				// If the ProjectConfig already exists, we can ignore the error.
+				// This can for example happen if the ProjectConfig was created
+				// by the user without them removing the spec from the Project.
+				if !kubeerr.IsAlreadyExists(err) {
+					return ctrl.Result{}, err
+				}
+				logger.Debug("ProjectConfig already exists")
+			} else {
+				logger.Debug(fmt.Sprintf(
+					"migrated Project spec to ProjectConfig %q",
+					projectCfg.Name,
+				))
+			}
+		}
+
+		// Remove the spec from the Project.
+		if err = r.updateProjectFn(ctx, project); err != nil {
+			return ctrl.Result{}, err
+		}
+		logger.Debug("removed deprecated spec from Project")
+		return ctrl.Result{Requeue: true}, nil
+	}
+
 	if reason, ok := mustReconcileProject(project); !ok {
 		logger.Debug("nothing to do", "reason", reason)
 		return ctrl.Result{}, nil
@@ -618,7 +661,7 @@ func (r *reconciler) ensureDefaultProjectRoles(
 				},
 				{ // Full access to all mutable Kargo resource types
 					APIGroups: []string{kargoapi.GroupVersion.Group},
-					Resources: []string{"freights", "stages", "warehouses"},
+					Resources: []string{"freights", "stages", "warehouses", "projectconfigs"},
 					Verbs:     []string{"*"},
 				},
 				{ // Promote permission on all stages
@@ -670,7 +713,7 @@ func (r *reconciler) ensureDefaultProjectRoles(
 				},
 				{
 					APIGroups: []string{kargoapi.GroupVersion.Group},
-					Resources: []string{"freights", "promotions", "stages", "warehouses"},
+					Resources: []string{"freights", "promotions", "stages", "warehouses", "projectconfigs"},
 					Verbs:     []string{"get", "list", "watch"},
 				},
 				{
@@ -822,6 +865,40 @@ func migratePhaseToConditions(project *kargoapi.Project) (kargoapi.ProjectStatus
 	return status, true
 }
 
+// migrateSpecToProjectConfig migrates the Project's Spec to a dedicated
+// ProjectConfig resource. It returns the new ProjectConfig and a boolean
+// indicating whether the Project's Spec was cleared.
+func migrateSpecToProjectConfig(
+	project *kargoapi.Project,
+) (*kargoapi.ProjectConfig, bool) {
+	if project.Spec == nil { // nolint:staticcheck
+		return nil, false
+	}
+
+	// If the Project has no PromotionPolicies, we can remove the spec entirely.
+	if len(project.Spec.PromotionPolicies) == 0 { // nolint:staticcheck
+		project.Spec = nil // nolint:staticcheck
+		return nil, true
+	}
+
+	// If the Project has PromotionPolicies, we need to migrate them to a
+	// ProjectConfig.
+	projectConfig := &kargoapi.ProjectConfig{
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      project.Name,
+			Namespace: project.Name,
+		},
+		Spec: kargoapi.ProjectConfigSpec{
+			PromotionPolicies: project.Spec.PromotionPolicies, // nolint:staticcheck
+		},
+	}
+
+	// Clear the spec now that we've migrated it to a ProjectConfig.
+	project.Spec = nil // nolint:staticcheck
+
+	return projectConfig, true
+}
+
 // mustReconcileProject returns if the Project should be reconciled, or if it
 // should be left alone, and the reason why.
 func mustReconcileProject(project *kargoapi.Project) (string, bool) {
diff --git a/internal/controller/stages/regular_stages.go b/internal/controller/stages/regular_stages.go
index b4b0c369..5823108a 100644
--- a/internal/controller/stages/regular_stages.go
+++ b/internal/controller/stages/regular_stages.go
@@ -1728,17 +1728,24 @@ func (r *RegularStageReconciler) autoPromotionAllowed(
 ) (bool, error) {
 	logger := logging.LoggerFromContext(ctx)
 
-	project := &kargoapi.Project{}
-	if err := r.client.Get(ctx, types.NamespacedName{Name: stage.Namespace}, project); err != nil {
-		return false, fmt.Errorf("error getting Project %q in namespace %q: %w", stage.Name, stage.Namespace, err)
+	projectCfg := &kargoapi.ProjectConfig{}
+	if err := r.client.Get(ctx, types.NamespacedName{
+		Name:      stage.Namespace,
+		Namespace: stage.Namespace,
+	}, projectCfg); err != nil {
+		if apierrors.IsNotFound(err) {
+			logger.Debug("found no ProjectConfig associated with Project; auto-promotion is disabled")
+			return false, nil
+		}
+		return false, fmt.Errorf("error getting ProjectConfig for Project %q: %w", stage.Namespace, err)
 	}
 
-	if project.Spec == nil || len(project.Spec.PromotionPolicies) == 0 {
+	if len(projectCfg.Spec.PromotionPolicies) == 0 {
 		logger.Debug("found no PromotionPolicy associated with Stage")
 		return false, nil
 	}
 
-	for _, policy := range project.Spec.PromotionPolicies {
+	for _, policy := range projectCfg.Spec.PromotionPolicies {
 		if policy.Stage == stage.Name {
 			logger.Debug(
 				"found PromotionPolicy associated with Stage",
diff --git a/internal/webhook/project/webhook.go b/internal/webhook/project/webhook.go
index 5f179e1c..d7d2e946 100644
--- a/internal/webhook/project/webhook.go
+++ b/internal/webhook/project/webhook.go
@@ -3,6 +3,7 @@ package project
 import (
 	"context"
 	"fmt"
+	"reflect"
 
 	"github.com/kelseyhightower/envconfig"
 	corev1 "k8s.io/api/core/v1"
@@ -44,68 +45,41 @@ func WebhookConfigFromEnv() WebhookConfig {
 }
 
 type webhook struct {
-	cfg WebhookConfig
-
-	// The following behaviors are overridable for testing purposes:
-
-	validateSpecFn func(*field.Path, *kargoapi.ProjectSpec) field.ErrorList
-
-	ensureNamespaceFn func(context.Context, *kargoapi.Project) error
-
-	ensureProjectAdminPermissionsFn func(
-		context.Context,
-		*kargoapi.Project,
-	) error
-
-	getNamespaceFn func(
-		context.Context,
-		types.NamespacedName,
-		client.Object,
-		...client.GetOption,
-	) error
-
-	createNamespaceFn func(
-		context.Context,
-		client.Object,
-		...client.CreateOption,
-	) error
-
-	createRoleBindingFn func(
-		context.Context,
-		client.Object,
-		...client.CreateOption,
-	) error
+	cfg    WebhookConfig
+	client client.Client
 }
 
 func SetupWebhookWithManager(mgr ctrl.Manager, cfg WebhookConfig) error {
-	w := newWebhook(mgr.GetClient(), cfg)
+	w := &webhook{
+		cfg: cfg,
+	}
 	return ctrl.NewWebhookManagedBy(mgr).
 		For(&kargoapi.Project{}).
 		WithValidator(w).
 		Complete()
 }
 
-func newWebhook(kubeClient client.Client, cfg WebhookConfig) *webhook {
-	w := &webhook{
-		cfg: cfg,
-	}
-	w.validateSpecFn = w.validateSpec
-	w.ensureNamespaceFn = w.ensureNamespace
-	w.ensureProjectAdminPermissionsFn = w.ensureProjectAdminPermissions
-	w.getNamespaceFn = kubeClient.Get
-	w.createNamespaceFn = kubeClient.Create
-	w.createRoleBindingFn = kubeClient.Create
-	return w
-}
-
 func (w *webhook) ValidateCreate(
 	ctx context.Context,
 	obj runtime.Object,
 ) (admission.Warnings, error) {
 	project := obj.(*kargoapi.Project) // nolint: forcetypeassert
 
-	if errs := w.validateSpecFn(field.NewPath("spec"), project.Spec); len(errs) > 0 {
-		return nil, apierrors.NewInvalid(projectGroupKind, project.Name, errs)
+	// TODO(hidde): Remove this when the deprecated Spec field is removed.
+	if project.Spec != nil { // nolint: staticcheck
+		return nil, apierrors.NewInvalid(
+			projectGroupKind,
+			project.Name,
+			field.ErrorList{
+				field.Forbidden(
+					field.NewPath("spec"),
+					fmt.Sprintf(
+						"deprecated field: create a ProjectConfig named %q with the config instead",
+						project.Name,
+					),
+				),
+			},
+		)
 	}
 
 	req, err := admission.RequestFromContext(ctx)
@@ -122,7 +96,7 @@ func (w *webhook) ValidateCreate(
 	// We synchronously ensure the existence of a namespace with the same name as
 	// the Project because resources following the Project in a manifest are
 	// likely to be scoped to that namespace.
-	if err := w.ensureNamespaceFn(ctx, project); err != nil {
+	if err := w.ensureNamespace(ctx, project); err != nil {
 		return nil, err
 	}
 
@@ -130,20 +104,40 @@ func (w *webhook) ValidateCreate(
 	// permissions to manage ServiceAccounts, Roles, RoleBindings, and Secrets in
 	// the Project namespace just in time. This prevents us from having to give
 	// the Kargo API server carte blanche access these resources throughout the
-	// cluster. We do this synchronously because resources of these types are are
+	// cluster. We do this synchronously because resources of these types are
 	// likely to follow the Project in a manifest.
-	return nil, w.ensureProjectAdminPermissionsFn(ctx, project)
+	return nil, w.ensureProjectAdminPermissions(ctx, project)
 }
 
 func (w *webhook) ValidateUpdate(
 	_ context.Context,
-	_ runtime.Object,
+	oldObj runtime.Object,
 	newObj runtime.Object,
 ) (admission.Warnings, error) {
-	project := newObj.(*kargoapi.Project) // nolint: forcetypeassert
-	if errs := w.validateSpecFn(field.NewPath("spec"), project.Spec); len(errs) > 0 {
-		return nil, apierrors.NewInvalid(projectGroupKind, project.Name, errs)
+	oldProject := oldObj.(*kargoapi.Project) // nolint: forcetypeassert
+	newProject := newObj.(*kargoapi.Project) // nolint: forcetypeassert
+
+	specPath := field.NewPath("spec")
+
+	// TODO(hidde): Remove this when the deprecated Spec field is removed.
+	if newProject.Spec != nil { // nolint: staticcheck
+		if !reflect.DeepEqual(oldProject.Spec, newProject.Spec) { // nolint: staticcheck
+			return nil, apierrors.NewInvalid(
+				projectGroupKind,
+				newProject.Name,
+				field.ErrorList{
+					field.Forbidden(
+						specPath,
+						fmt.Sprintf(
+							"deprecated field: create a ProjectConfig named %q with the config instead",
+							newProject.Name,
+						),
+					),
+				},
+			)
+		}
 	}
+
 	return nil, nil
 }
 
@@ -154,43 +148,6 @@ func (w *webhook) ValidateDelete(
 	return nil, nil
 }
 
-func (w *webhook) validateSpec(
-	f *field.Path,
-	spec *kargoapi.ProjectSpec,
-) field.ErrorList {
-	if spec == nil { // nil spec is valid
-		return nil
-	}
-	return w.validatePromotionPolicies(
-		f.Child("promotionPolicies"),
-		spec.PromotionPolicies,
-	)
-}
-
-func (w *webhook) validatePromotionPolicies(
-	f *field.Path,
-	promotionPolicies []kargoapi.PromotionPolicy,
-) field.ErrorList {
-	stageNames := make(map[string]struct{}, len(promotionPolicies))
-	for _, promotionPolicy := range promotionPolicies {
-		if _, found := stageNames[promotionPolicy.Stage]; found {
-			return field.ErrorList{
-				field.Invalid(
-					f,
-					promotionPolicies,
-					fmt.Sprintf(
-						"multiple %s reference stage %s",
-						f.String(),
-						promotionPolicy.Stage,
-					),
-				),
-			}
-		}
-		stageNames[promotionPolicy.Stage] = struct{}{}
-	}
-	return nil
-}
-
 // ensureNamespace is used to ensure the existence of a namespace with the same
 // name as the Project. If the namespace does not exist, it is created. If the
 // namespace exists, it is checked for any ownership conflicts with the Project
@@ -202,7 +159,7 @@ func (w *webhook) ensureNamespace(
 	logger := logging.LoggerFromContext(ctx).WithValues("project", project.Name)
 
 	ns := &corev1.Namespace{}
-	if err := w.getNamespaceFn(
+	if err := w.client.Get(
 		ctx,
 		types.NamespacedName{Name: project.Name},
 		ns,
@@ -229,7 +186,7 @@ func (w *webhook) ensureNamespace(
 		return nil
 	}
 
-	// If we get to here, we had a not found error and we can proceed with
+	// If we get to here, we had a not found error, and we can proceed with
 	// creating the namespace.
 
 	logger.Debug("namespace does not exist; creating it")
@@ -255,7 +212,7 @@ func (w *webhook) ensureNamespace(
 	// to the namespace and use our own namespace reconciler to clear it after
 	// deleting the Project.
 	controllerutil.AddFinalizer(ns, kargoapi.FinalizerName)
-	if err := w.createNamespaceFn(ctx, ns); err != nil {
+	if err := w.client.Create(ctx, ns); err != nil {
 		return apierrors.NewInternalError(
 			fmt.Errorf("error creating namespace %q: %w", project.Name, err),
 		)
@@ -301,7 +258,7 @@ func (w *webhook) ensureProjectAdminPermissions(
 			},
 		},
 	}
-	if err := w.createRoleBindingFn(ctx, roleBinding); err != nil {
+	if err := w.client.Create(ctx, roleBinding); err != nil {
 		if apierrors.IsAlreadyExists(err) {
 			logger.Debug("role binding already exists in project namespace")
 			return nil
diff --git a/internal/webhook/projectconfig/webhook.go b/internal/webhook/projectconfig/webhook.go
new file mode 100644
index 00000000..532ac6c8
--- /dev/null
+++ b/internal/webhook/projectconfig/webhook.go
@@ -0,0 +1,187 @@
+package projectconfig
+
+import (
+	"context"
+	"fmt"
+
+	corev1 "k8s.io/api/core/v1"
+	apierrors "k8s.io/apimachinery/pkg/api/errors"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/runtime"
+	"k8s.io/apimachinery/pkg/runtime/schema"
+	"k8s.io/apimachinery/pkg/types"
+	"k8s.io/apimachinery/pkg/util/validation/field"
+	ctrl "sigs.k8s.io/controller-runtime"
+	"sigs.k8s.io/controller-runtime/pkg/client"
+	"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
+
+	kargoapi "github.com/akuity/kargo/api/v1alpha1"
+)
+
+var (
+	projectConfigGroupKind = schema.GroupKind{
+		Group: kargoapi.GroupVersion.Group,
+		Kind:  "ProjectConfig",
+	}
+	projectConfigGroupResource = schema.GroupResource{
+		Group:    kargoapi.GroupVersion.Group,
+		Resource: "projectconfigs",
+	}
+)
+
+type webhook struct {
+	client client.Client
+}
+
+func SetupWebhookWithManager(mgr ctrl.Manager) error {
+	w := &webhook{
+		client: mgr.GetClient(),
+	}
+	return ctrl.NewWebhookManagedBy(mgr).
+		For(&kargoapi.ProjectConfig{}).
+		WithValidator(w).
+		Complete()
+}
+
+func (w *webhook) ValidateCreate(
+	ctx context.Context,
+	obj runtime.Object,
+) (admission.Warnings, error) {
+	projectCfg := obj.(*kargoapi.ProjectConfig) // nolint: forcetypeassert
+
+	var errs field.ErrorList
+	if metaErrs := w.validateObjectMeta(
+		field.NewPath("metadata"),
+		projectCfg.ObjectMeta,
+	); len(metaErrs) > 0 {
+		errs = append(errs, metaErrs...)
+	}
+
+	if specErrs := w.validateSpec(
+		field.NewPath("spec"),
+		projectCfg.Spec,
+	); len(specErrs) > 0 {
+		errs = append(errs, specErrs...)
+	}
+
+	if len(errs) > 0 {
+		return nil, apierrors.NewInvalid(
+			projectConfigGroupKind,
+			projectCfg.Name,
+			errs,
+		)
+	}
+
+	req, err := admission.RequestFromContext(ctx)
+	if err != nil {
+		return nil, apierrors.NewInternalError(
+			fmt.Errorf("error getting admission request from context: %w", err),
+		)
+	}
+
+	if req.DryRun != nil && *req.DryRun {
+		return nil, nil
+	}
+
+	if err = w.ensureProjectNamespace(ctx, projectCfg.ObjectMeta); err != nil {
+		return nil, err
+	}
+
+	return nil, nil
+}
+
+func (w *webhook) ValidateUpdate(
+	_ context.Context,
+	_ runtime.Object,
+	newObj runtime.Object,
+) (admission.Warnings, error) {
+	projectCfg := newObj.(*kargoapi.ProjectConfig) // nolint: forcetypeassert
+	if errs := w.validateSpec(field.NewPath("spec"), projectCfg.Spec); len(errs) > 0 {
+		return nil, apierrors.NewInvalid(projectConfigGroupKind, projectCfg.Name, errs)
+	}
+	return nil, nil
+}
+
+func (w *webhook) ValidateDelete(
+	context.Context,
+	runtime.Object,
+) (admission.Warnings, error) {
+	return nil, nil
+}
+
+func (w *webhook) validateObjectMeta(
+	f *field.Path,
+	meta metav1.ObjectMeta,
+) field.ErrorList {
+	if meta.Name != meta.Namespace {
+		return field.ErrorList{
+			field.Invalid(
+				f.Child("name"),
+				meta.Name,
+				fmt.Sprintf(
+					"name %q must match project name %q",
+					meta.Name,
+					meta.Namespace,
+				),
+			),
+		}
+	}
+	return nil
+}
+
+func (w *webhook) validateSpec(
+	f *field.Path,
+	spec kargoapi.ProjectConfigSpec,
+) field.ErrorList {
+	return w.validatePromotionPolicies(
+		f.Child("promotionPolicies"),
+		spec.PromotionPolicies,
+	)
+}
+
+func (w *webhook) validatePromotionPolicies(
+	f *field.Path,
+	promotionPolicies []kargoapi.PromotionPolicy,
+) field.ErrorList {
+	stageNames := make(map[string]struct{}, len(promotionPolicies))
+	for _, promotionPolicy := range promotionPolicies {
+		if _, found := stageNames[promotionPolicy.Stage]; found {
+			return field.ErrorList{
+				field.Invalid(
+					f,
+					promotionPolicies,
+					fmt.Sprintf(
+						"multiple %s reference stage %q",
+						f.String(),
+						promotionPolicy.Stage,
+					),
+				),
+			}
+		}
+		stageNames[promotionPolicy.Stage] = struct{}{}
+	}
+	return nil
+}
+
+func (w *webhook) ensureProjectNamespace(ctx context.Context, meta metav1.ObjectMeta) error {
+	ns := &corev1.Namespace{}
+	if err := w.client.Get(ctx, types.NamespacedName{Name: meta.Namespace}, ns); err != nil {
+		return apierrors.NewInternalError(
+			fmt.Errorf("error getting namespace %q: %w", meta.Namespace, err),
+		)
+	}
+
+	v, ok := ns.Labels[kargoapi.ProjectLabelKey]
+	if !ok || v != kargoapi.LabelTrueValue {
+		return apierrors.NewForbidden(
+			projectConfigGroupResource,
+			meta.Name,
+			fmt.Errorf(
+				"namespace %q does not belong to Kargo project (missing %q label)",
+				meta.Namespace, kargoapi.ProjectLabelKey,
+			),
+		)
+	}
+
+	return nil
+}
diff --git a/ui/src/gen/api/v1alpha1/generated_pb.ts b/ui/src/gen/api/v1alpha1/generated_pb.ts
index adebccb7..cc7c5d60 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/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]);
+  fileDesc("ChxhcGkvdjFhbHBoYTEvZ2VuZXJhdGVkLnByb3RvEiRnaXRodWIuY29tLmFrdWl0eS5rYXJnby5hcGkudjFhbHBoYTEiMgoTQW5hbHlzaXNSdW5Bcmd1bWVudBIMCgRuYW1lGAEgASgJEg0KBXZhbHVlGAIgASgJIrACChNBbmFseXNpc1J1bk1ldGFkYXRhElUKBmxhYmVscxgBIAMoCzJFLmdpdGh1Yi5jb20uYWt1aXR5LmthcmdvLmFwaS52MWFscGhhMS5BbmFseXNpc1J1bk1ldGFkYXRhLkxhYmVsc0VudHJ5El8KC2Fubm90YXRpb25zGAIgAygLMkouZ2l0aHViLmNvbS5ha3VpdHkua2FyZ28uYXBpLnYxYWxwaGExLkFuYWx5c2lzUnVuTWV0YWRhdGEuQW5ub3RhdGlvbnNFbnRyeRotCgtMYWJlbHNFbnRyeRILCgNrZXkYASABKAkSDQoFdmFsdWUYAiABKAk6AjgBGjIKEEFubm90YXRpb25zRW50cnkSCwoDa2V5GAEgASgJEg0KBXZhbHVlGAIgASgJOgI4ASJGChRBbmFseXNpc1J1blJlZmVyZW5jZRIRCgluYW1lc3BhY2UYASABKAkSDAoEbmFtZRgCIAEoCRINCgVwaGFzZRgDIAEoCSI3ChlBbmFseXNpc1RlbXBsYXRlUmVmZXJlbmNlEgwKBG5hbWUYASABKAkSDAoEa2luZBgCIAEoCSJPCg1BcHByb3ZlZFN0YWdlEj4KCmFwcHJvdmVkQXQYASABKAsyKi5rOHMuaW8uYXBpbWFjaGluZXJ5LnBrZy5hcGlzLm1ldGEudjEuVGltZSI4ChVBcmdvQ0RBcHBIZWFsdGhTdGF0dXMSDgoGc3RhdHVzGAEgASgJEg8KB21lc3NhZ2UYAiABKAki1AEKD0FyZ29DREFwcFN0YXR1cxIRCgluYW1lc3BhY2UYASABKAkSDAoEbmFtZRgCIAEoCRJRCgxoZWFsdGhTdGF0dXMYAyABKAsyOy5naXRodWIuY29tLmFrdWl0eS5rYXJnby5hcGkudjFhbHBoYTEuQXJnb0NEQXBwSGVhbHRoU3RhdHVzEk0KCnN5bmNTdGF0dXMYBCABKAsyOS5naXRodWIuY29tLmFrdWl0eS5rYXJnby5hcGkudjFhbHBoYTEuQXJnb0NEQXBwU3luY1N0YXR1cyJKChNBcmdvQ0RBcHBTeW5jU3RhdHVzEg4KBnN0YXR1cxgBIAEoCRIQCghyZXZpc2lvbhgCIAEoCRIRCglyZXZpc2lvbnMYAyADKAkiNwoFQ2hhcnQSDwoHcmVwb1VSTBgBIAEoCRIMCgRuYW1lGAIgASgJEg8KB3ZlcnNpb24YAyABKAkiYQoUQ2hhcnREaXNjb3ZlcnlSZXN1bHQSDwoHcmVwb1VSTBgBIAEoCRIMCgRuYW1lGAIgASgJEhgKEHNlbXZlckNvbnN0cmFpbnQYAyABKAkSEAoIdmVyc2lvbnMYBCADKAkiZAoRQ2hhcnRTdWJzY3JpcHRpb24SDwoHcmVwb1VSTBgBIAEoCRIMCgRuYW1lGAIgASgJEhgKEHNlbXZlckNvbnN0cmFpbnQYAyABKAkSFgoOZGlzY292ZXJ5TGltaXQYBCABKAUioQEKFENsdXN0ZXJQcm9tb3Rpb25UYXNrEkIKCG1ldGFkYXRhGAEgASgLMjAuazhzLmlvLmFwaW1hY2hpbmVyeS5wa2cuYXBpcy5tZXRhLnYxLk9iamVjdE1ldGESRQoEc3BlYxgCIAEoCzI3LmdpdGh1Yi5jb20uYWt1aXR5LmthcmdvLmFwaS52MWFscGhhMS5Qcm9tb3Rpb25UYXNrU3BlYyKnAQoYQ2x1c3RlclByb21vdGlvblRhc2tMaXN0EkAKCG1ldGFkYXRhGAEgASgLMi4uazhzLmlvLmFwaW1hY2hpbmVyeS5wa2cuYXBpcy5tZXRhLnYxLkxpc3RNZXRhEkkKBWl0ZW1zGAIgAygLMjouZ2l0aHViLmNvbS5ha3VpdHkua2FyZ28uYXBpLnYxYWxwaGExLkNsdXN0ZXJQcm9tb3Rpb25UYXNrIkkKDEN1cnJlbnRTdGFnZRI5CgVzaW5jZRgBIAEoCzIqLms4cy5pby5hcGltYWNoaW5lcnkucGtnLmFwaXMubWV0YS52MS5UaW1lIrYCChNEaXNjb3ZlcmVkQXJ0aWZhY3RzEkAKDGRpc2NvdmVyZWRBdBgEIAEoCzIqLms4cy5pby5hcGltYWNoaW5lcnkucGtnLmFwaXMubWV0YS52MS5UaW1lEkUKA2dpdBgBIAMoCzI4LmdpdGh1Yi5jb20uYWt1aXR5LmthcmdvLmFwaS52MWFscGhhMS5HaXREaXNjb3ZlcnlSZXN1bHQSSgoGaW1hZ2VzGAIgAygLMjouZ2l0aHViLmNvbS5ha3VpdHkua2FyZ28uYXBpLnYxYWxwaGExLkltYWdlRGlzY292ZXJ5UmVzdWx0EkoKBmNoYXJ0cxgDIAMoCzI6LmdpdGh1Yi5jb20uYWt1aXR5LmthcmdvLmFwaS52MWFscGhhMS5DaGFydERpc2NvdmVyeVJlc3VsdCKwAQoQRGlzY292ZXJlZENvbW1pdBIKCgJpZBgBIAEoCRIOCgZicmFuY2gYAiABKAkSCwoDdGFnGAMgASgJEg8KB3N1YmplY3QYBCABKAkSDgoGYXV0aG9yGAUgASgJEhEKCWNvbW1pdHRlchgGIAEoCRI/CgtjcmVhdG9yRGF0ZRgHIAEoCzIqLms4cy5pby5hcGltYWNoaW5lcnkucGtnLmFwaXMubWV0YS52MS5UaW1lIqQCChhEaXNjb3ZlcmVkSW1hZ2VSZWZlcmVuY2USCwoDdGFnGAEgASgJEg4KBmRpZ2VzdBgCIAEoCRJkCgthbm5vdGF0aW9ucxgFIAMoCzJPLmdpdGh1Yi5jb20uYWt1aXR5LmthcmdvLmFwaS52MWFscGhhMS5EaXNjb3ZlcmVkSW1hZ2VSZWZlcmVuY2UuQW5ub3RhdGlvbnNFbnRyeRISCgpnaXRSZXBvVVJMGAMgASgJEj0KCWNyZWF0ZWRBdBgEIAEoCzIqLms4cy5pby5hcGltYWNoaW5lcnkucGtnLmFwaXMubWV0YS52MS5UaW1lGjIKEEFubm90YXRpb25zRW50cnkSCwoDa2V5GAEgASgJEg0KBXZhbHVlGAIgASgJOgI4ASIxChJFeHByZXNzaW9uVmFyaWFibGUSDAoEbmFtZRgBIAEoCRINCgV2YWx1ZRgCIAEoCSKiAwoHRnJlaWdodBJCCghtZXRhZGF0YRgBIAEoCzIwLms4cy5pby5hcGltYWNoaW5lcnkucGtnLmFwaXMubWV0YS52MS5PYmplY3RNZXRhEg0KBWFsaWFzGAcgASgJEkMKBm9yaWdpbhgJIAEoCzIzLmdpdGh1Yi5jb20uYWt1aXR5LmthcmdvLmFwaS52MWFscGhhMS5GcmVpZ2h0T3JpZ2luEkAKB2NvbW1pdHMYAyADKAsyLy5naXRodWIuY29tLmFrdWl0eS5rYXJnby5hcGkudjFhbHBoYTEuR2l0Q29tbWl0EjsKBmltYWdlcxgEIAMoCzIrLmdpdGh1Yi5jb20uYWt1aXR5LmthcmdvLmFwaS52MWFscGhhMS5JbWFnZRI7CgZjaGFydHMYBSADKAsyKy5naXRodWIuY29tLmFrdWl0eS5rYXJnby5hcGkudjFhbHBoYTEuQ2hhcnQSQwoGc3RhdHVzGAYgASgLMjMuZ2l0aHViLmNvbS5ha3VpdHkua2FyZ28uYXBpLnYxYWxwaGExLkZyZWlnaHRTdGF0dXMirQIKEUZyZWlnaHRDb2xsZWN0aW9uEgoKAmlkGAMgASgJElEKBWl0ZW1zGAEgAygLMkIuZ2l0aHViLmNvbS5ha3VpdHkua2FyZ28uYXBpLnYxYWxwaGExLkZyZWlnaHRDb2xsZWN0aW9uLkl0ZW1zRW50cnkSUwoTdmVyaWZpY2F0aW9uSGlzdG9yeRgCIAMoCzI2LmdpdGh1Yi5jb20uYWt1aXR5LmthcmdvLmFwaS52MWFscGhhMS5WZXJpZmljYXRpb25JbmZvGmQKCkl0ZW1zRW50cnkSCwoDa2V5GAEgASgJEkUKBXZhbHVlGAIgASgLMjYuZ2l0aHViLmNvbS5ha3VpdHkua2FyZ28uYXBpLnYxYWxwaGExLkZyZWlnaHRSZWZlcmVuY2U6AjgBIo0BCgtGcmVpZ2h0TGlzdBJACghtZXRhZGF0YRgBIAEoCzIuLms4cy5pby5hcGltYWNoaW5lcnkucGtnLmFwaXMubWV0YS52MS5MaXN0TWV0YRI8CgVpdGVtcxgCIAMoCzItLmdpdGh1Yi5jb20uYWt1aXR5LmthcmdvLmFwaS52MWFscGhhMS5GcmVpZ2h0IisKDUZyZWlnaHRPcmlnaW4SDAoEa2luZBgBIAEoCRIMCgRuYW1lGAIgASgJIqECChBGcmVpZ2h0UmVmZXJlbmNlEgwKBG5hbWUYASABKAkSQwoGb3JpZ2luGAggASgLMjMuZ2l0aHViLmNvbS5ha3VpdHkua2FyZ28uYXBpLnYxYWxwaGExLkZyZWlnaHRPcmlnaW4SQAoHY29tbWl0cxgCIAMoCzIvLmdpdGh1Yi5jb20uYWt1aXR5LmthcmdvLmFwaS52MWFscGhhMS5HaXRDb21taXQSOwoGaW1hZ2VzGAMgAygLMisuZ2l0aHViLmNvbS5ha3VpdHkua2FyZ28uYXBpLnYxYWxwaGExLkltYWdlEjsKBmNoYXJ0cxgEIAMoCzIrLmdpdGh1Yi5jb20uYWt1aXR5LmthcmdvLmFwaS52MWFscGhhMS5DaGFydCKcAQoORnJlaWdodFJlcXVlc3QSQwoGb3JpZ2luGAEgASgLMjMuZ2l0aHViLmNvbS5ha3VpdHkua2FyZ28uYXBpLnYxYWxwaGExLkZyZWlnaHRPcmlnaW4SRQoHc291cmNlcxgCIAEoCzI0LmdpdGh1Yi5jb20uYWt1aXR5LmthcmdvLmFwaS52MWFscGhhMS5GcmVpZ2h0U291cmNlcyKYAQoORnJlaWdodFNvdXJjZXMSDgoGZGlyZWN0GAEgASgIEg4KBnN0YWdlcxgCIAMoCRJIChByZXF1aXJlZFNvYWtUaW1lGAMgASgLMi4uazhzLmlvLmFwaW1hY2hpbmVyeS5wa2cuYXBpcy5tZXRhLnYxLkR1cmF0aW9uEhwKFGF2YWlsYWJpbGl0eVN0cmF0ZWd5GAQgASgJItcECg1GcmVpZ2h0U3RhdHVzElkKC2N1cnJlbnRseUluGAMgAygLMkQuZ2l0aHViLmNvbS5ha3VpdHkua2FyZ28uYXBpLnYxYWxwaGExLkZyZWlnaHRTdGF0dXMuQ3VycmVudGx5SW5FbnRyeRJXCgp2ZXJpZmllZEluGAEgAygLMkMuZ2l0aHViLmNvbS5ha3VpdHkua2FyZ28uYXBpLnYxYWxwaGExLkZyZWlnaHRTdGF0dXMuVmVyaWZpZWRJbkVudHJ5ElkKC2FwcHJvdmVkRm9yGAIgAygLMkQuZ2l0aHViLmNvbS5ha3VpdHkua2FyZ28uYXBpLnYxYWxwaGExLkZyZWlnaHRTdGF0dXMuQXBwcm92ZWRGb3JFbnRyeRpmChBDdXJyZW50bHlJbkVudHJ5EgsKA2tleRgBIAEoCRJBCgV2YWx1ZRgCIAEoCzIyLmdpdGh1Yi5jb20uYWt1aXR5LmthcmdvLmFwaS52MWFscGhhMS5DdXJyZW50U3RhZ2U6AjgBGmYKD1ZlcmlmaWVkSW5FbnRyeRILCgNrZXkYASABKAkSQgoFdmFsdWUYAiABKAsyMy5naXRodWIuY29tLmFrdWl0eS5rYXJnby5hcGkudjFhbHBoYTEuVmVyaWZpZWRTdGFnZToCOAEaZwoQQXBwcm92ZWRGb3JFbnRyeRILCgNrZXkYASABKAkSQgoFdmFsdWUYAiABKAsyMy5naXRodWIuY29tLmFrdWl0eS5rYXJnby5hcGkudjFhbHBoYTEuQXBwcm92ZWRTdGFnZToCOAEieQoJR2l0Q29tbWl0Eg8KB3JlcG9VUkwYASABKAkSCgoCaWQYAiABKAkSDgoGYnJhbmNoGAMgASgJEgsKA3RhZxgEIAEoCRIPCgdtZXNzYWdlGAYgASgJEg4KBmF1dGhvchgHIAEoCRIRCgljb21taXR0ZXIYCCABKAkibgoSR2l0RGlzY292ZXJ5UmVzdWx0Eg8KB3JlcG9VUkwYASABKAkSRwoHY29tbWl0cxgCIAMoCzI2LmdpdGh1Yi5jb20uYWt1aXR5LmthcmdvLmFwaS52MWFscGhhMS5EaXNjb3ZlcmVkQ29tbWl0Io4CCg9HaXRTdWJzY3JpcHRpb24SDwoHcmVwb1VSTBgBIAEoCRIfChdjb21taXRTZWxlY3Rpb25TdHJhdGVneRgCIAEoCRIOCgZicmFuY2gYAyABKAkSFQoNc3RyaWN0U2VtdmVycxgLIAEoCBIYChBzZW12ZXJDb25zdHJhaW50GAQgASgJEhEKCWFsbG93VGFncxgFIAEoCRISCgppZ25vcmVUYWdzGAYgAygJEh0KFWluc2VjdXJlU2tpcFRMU1ZlcmlmeRgHIAEoCBIUCgxpbmNsdWRlUGF0aHMYCCADKAkSFAoMZXhjbHVkZVBhdGhzGAkgAygJEhYKDmRpc2NvdmVyeUxpbWl0GAogASgFIsgBCgZIZWFsdGgSDgoGc3RhdHVzGAEgASgJEg4KBmlzc3VlcxgCIAMoCRJOCgZjb25maWcYBCABKAsyPi5rOHMuaW8uYXBpZXh0ZW5zaW9uc19hcGlzZXJ2ZXIucGtnLmFwaXMuYXBpZXh0ZW5zaW9ucy52MS5KU09OEk4KBm91dHB1dBgFIAEoCzI+Lms4cy5pby5hcGlleHRlbnNpb25zX2FwaXNlcnZlci5wa2cuYXBpcy5hcGlleHRlbnNpb25zLnYxLkpTT04ibwoPSGVhbHRoQ2hlY2tTdGVwEgwKBHVzZXMYASABKAkSTgoGY29uZmlnGAIgASgLMj4uazhzLmlvLmFwaWV4dGVuc2lvbnNfYXBpc2VydmVyLnBrZy5hcGlzLmFwaWV4dGVuc2lvbnMudjEuSlNPTiLQAQoFSW1hZ2USDwoHcmVwb1VSTBgBIAEoCRISCgpnaXRSZXBvVVJMGAIgASgJEgsKA3RhZxgDIAEoCRIOCgZkaWdlc3QYBCABKAkSUQoLYW5ub3RhdGlvbnMYBSADKAsyPC5naXRodWIuY29tLmFrdWl0eS5rYXJnby5hcGkudjFhbHBoYTEuSW1hZ2UuQW5ub3RhdGlvbnNFbnRyeRoyChBBbm5vdGF0aW9uc0VudHJ5EgsKA2tleRgBIAEoCRINCgV2YWx1ZRgCIAEoCToCOAEijQEKFEltYWdlRGlzY292ZXJ5UmVzdWx0Eg8KB3JlcG9VUkwYASABKAkSEAoIcGxhdGZvcm0YAiABKAkSUgoKcmVmZXJlbmNlcxgDIAMoCzI+LmdpdGh1Yi5jb20uYWt1aXR5LmthcmdvLmFwaS52MWFscGhhMS5EaXNjb3ZlcmVkSW1hZ2VSZWZlcmVuY2Ui+QEKEUltYWdlU3Vic2NyaXB0aW9uEg8KB3JlcG9VUkwYASABKAkSEgoKZ2l0UmVwb1VSTBgCIAEoCRIeChZpbWFnZVNlbGVjdGlvblN0cmF0ZWd5GAMgASgJEhUKDXN0cmljdFNlbXZlcnMYCiABKAgSGAoQc2VtdmVyQ29uc3RyYWludBgEIAEoCRIRCglhbGxvd1RhZ3MYBSABKAkSEgoKaWdub3JlVGFncxgGIAMoCRIQCghwbGF0Zm9ybRgHIAEoCRIdChVpbnNlY3VyZVNraXBUTFNWZXJpZnkYCCABKAgSFgoOZGlzY292ZXJ5TGltaXQYCSABKAUi2QEKB1Byb2plY3QSQgoIbWV0YWRhdGEYASABKAsyMC5rOHMuaW8uYXBpbWFjaGluZXJ5LnBrZy5hcGlzLm1ldGEudjEuT2JqZWN0TWV0YRJFCgRzcGVjGAIgASgLMjcuZ2l0aHViLmNvbS5ha3VpdHkua2FyZ28uYXBpLnYxYWxwaGExLlByb2plY3RDb25maWdTcGVjEkMKBnN0YXR1cxgDIAEoCzIzLmdpdGh1Yi5jb20uYWt1aXR5LmthcmdvLmFwaS52MWFscGhhMS5Qcm9qZWN0U3RhdHVzIpoBCg1Qcm9qZWN0Q29uZmlnEkIKCG1ldGFkYXRhGAEgASgLMjAuazhzLmlvLmFwaW1hY2hpbmVyeS5wa2cuYXBpcy5tZXRhLnYxLk9iamVjdE1ldGESRQoEc3BlYxgCIAEoCzI3LmdpdGh1Yi5jb20uYWt1aXR5LmthcmdvLmFwaS52MWFscGhhMS5Qcm9qZWN0Q29uZmlnU3BlYyKZAQoRUHJvamVjdENvbmZpZ0xpc3QSQAoIbWV0YWRhdGEYASABKAsyLi5rOHMuaW8uYXBpbWFjaGluZXJ5LnBrZy5hcGlzLm1ldGEudjEuTGlzdE1ldGESQgoFaXRlbXMYAiADKAsyMy5naXRodWIuY29tLmFrdWl0eS5rYXJnby5hcGkudjFhbHBoYTEuUHJvamVjdENvbmZpZyJlChFQcm9qZWN0Q29uZmlnU3BlYxJQChFwcm9tb3Rpb25Qb2xpY2llcxgBIAMoCzI1LmdpdGh1Yi5jb20uYWt1aXR5LmthcmdvLmFwaS52MWFscGhhMS5Qcm9tb3Rpb25Qb2xpY3kijQEKC1Byb2plY3RMaXN0EkAKCG1ldGFkYXRhGAEgASgLMi4uazhzLmlvLmFwaW1hY2hpbmVyeS5wa2cuYXBpcy5tZXRhLnYxLkxpc3RNZXRhEjwKBWl0ZW1zGAIgAygLMi0uZ2l0aHViLmNvbS5ha3VpdHkua2FyZ28uYXBpLnYxYWxwaGExLlByb2plY3QidAoNUHJvamVjdFN0YXR1cxJDCgpjb25kaXRpb25zGAMgAygLMi8uazhzLmlvLmFwaW1hY2hpbmVyeS5wa2cuYXBpcy5tZXRhLnYxLkNvbmRpdGlvbhINCgVwaGFzZRgBIAEoCRIPCgdtZXNzYWdlGAIgASgJItkBCglQcm9tb3Rpb24SQgoIbWV0YWRhdGEYASABKAsyMC5rOHMuaW8uYXBpbWFjaGluZXJ5LnBrZy5hcGlzLm1ldGEudjEuT2JqZWN0TWV0YRJBCgRzcGVjGAIgASgLMjMuZ2l0aHViLmNvbS5ha3VpdHkua2FyZ28uYXBpLnYxYWxwaGExLlByb21vdGlvblNwZWMSRQoGc3RhdHVzGAMgASgLMjUuZ2l0aHViLmNvbS5ha3VpdHkua2FyZ28uYXBpLnYxYWxwaGExLlByb21vdGlvblN0YXR1cyKRAQoNUHJvbW90aW9uTGlzdBJACghtZXRhZGF0YRgBIAEoCzIuLms4cy5pby5hcGltYWNoaW5lcnkucGtnLmFwaXMubWV0YS52MS5MaXN0TWV0YRI+CgVpdGVtcxgCIAMoCzIvLmdpdGh1Yi5jb20uYWt1aXR5LmthcmdvLmFwaS52MWFscGhhMS5Qcm9tb3Rpb24iPgoPUHJvbW90aW9uUG9saWN5Eg0KBXN0YWdlGAEgASgJEhwKFGF1dG9Qcm9tb3Rpb25FbmFibGVkGAIgASgIIvIBChJQcm9tb3Rpb25SZWZlcmVuY2USDAoEbmFtZRgBIAEoCRJHCgdmcmVpZ2h0GAIgASgLMjYuZ2l0aHViLmNvbS5ha3VpdHkua2FyZ28uYXBpLnYxYWxwaGExLkZyZWlnaHRSZWZlcmVuY2USRQoGc3RhdHVzGAMgASgLMjUuZ2l0aHViLmNvbS5ha3VpdHkua2FyZ28uYXBpLnYxYWxwaGExLlByb21vdGlvblN0YXR1cxI+CgpmaW5pc2hlZEF0GAQgASgLMiouazhzLmlvLmFwaW1hY2hpbmVyeS5wa2cuYXBpcy5tZXRhLnYxLlRpbWUiuwEKDVByb21vdGlvblNwZWMSDQoFc3RhZ2UYASABKAkSDwoHZnJlaWdodBgCIAEoCRJGCgR2YXJzGAQgAygLMjguZ2l0aHViLmNvbS5ha3VpdHkua2FyZ28uYXBpLnYxYWxwaGExLkV4cHJlc3Npb25WYXJpYWJsZRJCCgVzdGVwcxgDIAMoCzIzLmdpdGh1Yi5jb20uYWt1aXR5LmthcmdvLmFwaS52MWFscGhhMS5Qcm9tb3Rpb25TdGVwIrcECg9Qcm9tb3Rpb25TdGF0dXMSGgoSbGFzdEhhbmRsZWRSZWZyZXNoGAQgASgJEg0KBXBoYXNlGAEgASgJEg8KB21lc3NhZ2UYAiABKAkSRwoHZnJlaWdodBgFIAEoCzI2LmdpdGh1Yi5jb20uYWt1aXR5LmthcmdvLmFwaS52MWFscGhhMS5GcmVpZ2h0UmVmZXJlbmNlElIKEWZyZWlnaHRDb2xsZWN0aW9uGAcgASgLMjcuZ2l0aHViLmNvbS5ha3VpdHkua2FyZ28uYXBpLnYxYWxwaGExLkZyZWlnaHRDb2xsZWN0aW9uEksKDGhlYWx0aENoZWNrcxgIIAMoCzI1LmdpdGh1Yi5jb20uYWt1aXR5LmthcmdvLmFwaS52MWFscGhhMS5IZWFsdGhDaGVja1N0ZXASPgoKZmluaXNoZWRBdBgGIAEoCzIqLms4cy5pby5hcGltYWNoaW5lcnkucGtnLmFwaXMubWV0YS52MS5UaW1lEhMKC2N1cnJlbnRTdGVwGAkgASgDEloKFXN0ZXBFeGVjdXRpb25NZXRhZGF0YRgLIAMoCzI7LmdpdGh1Yi5jb20uYWt1aXR5LmthcmdvLmFwaS52MWFscGhhMS5TdGVwRXhlY3V0aW9uTWV0YWRhdGESTQoFc3RhdGUYCiABKAsyPi5rOHMuaW8uYXBpZXh0ZW5zaW9uc19hcGlzZXJ2ZXIucGtnLmFwaXMuYXBpZXh0ZW5zaW9ucy52MS5KU09OIuICCg1Qcm9tb3Rpb25TdGVwEgwKBHVzZXMYASABKAkSSgoEdGFzaxgFIAEoCzI8LmdpdGh1Yi5jb20uYWt1aXR5LmthcmdvLmFwaS52MWFscGhhMS5Qcm9tb3Rpb25UYXNrUmVmZXJlbmNlEgoKAmFzGAIgASgJEgoKAmlmGAcgASgJEkcKBXJldHJ5GAQgASgLMjguZ2l0aHViLmNvbS5ha3VpdHkua2FyZ28uYXBpLnYxYWxwaGExLlByb21vdGlvblN0ZXBSZXRyeRJGCgR2YXJzGAYgAygLMjguZ2l0aHViLmNvbS5ha3VpdHkua2FyZ28uYXBpLnYxYWxwaGExLkV4cHJlc3Npb25WYXJpYWJsZRJOCgZjb25maWcYAyABKAsyPi5rOHMuaW8uYXBpZXh0ZW5zaW9uc19hcGlzZXJ2ZXIucGtnLmFwaXMuYXBpZXh0ZW5zaW9ucy52MS5KU09OIm0KElByb21vdGlvblN0ZXBSZXRyeRI/Cgd0aW1lb3V0GAEgASgLMi4uazhzLmlvLmFwaW1hY2hpbmVyeS5wa2cuYXBpcy5tZXRhLnYxLkR1cmF0aW9uEhYKDmVycm9yVGhyZXNob2xkGAIgASgNIpoBCg1Qcm9tb3Rpb25UYXNrEkIKCG1ldGFkYXRhGAEgASgLMjAuazhzLmlvLmFwaW1hY2hpbmVyeS5wa2cuYXBpcy5tZXRhLnYxLk9iamVjdE1ldGESRQoEc3BlYxgCIAEoCzI3LmdpdGh1Yi5jb20uYWt1aXR5LmthcmdvLmFwaS52MWFscGhhMS5Qcm9tb3Rpb25UYXNrU3BlYyKZAQoRUHJvbW90aW9uVGFza0xpc3QSQAoIbWV0YWRhdGEYASABKAsyLi5rOHMuaW8uYXBpbWFjaGluZXJ5LnBrZy5hcGlzLm1ldGEudjEuTGlzdE1ldGESQgoFaXRlbXMYAiADKAsyMy5naXRodWIuY29tLmFrdWl0eS5rYXJnby5hcGkudjFhbHBoYTEuUHJvbW90aW9uVGFzayI0ChZQcm9tb3Rpb25UYXNrUmVmZXJlbmNlEgwKBG5hbWUYASABKAkSDAoEa2luZBgCIAEoCSKfAQoRUHJvbW90aW9uVGFza1NwZWMSRgoEdmFycxgBIAMoCzI4LmdpdGh1Yi5jb20uYWt1aXR5LmthcmdvLmFwaS52MWFscGhhMS5FeHByZXNzaW9uVmFyaWFibGUSQgoFc3RlcHMYAiADKAsyMy5naXRodWIuY29tLmFrdWl0eS5rYXJnby5hcGkudjFhbHBoYTEuUHJvbW90aW9uU3RlcCJeChFQcm9tb3Rpb25UZW1wbGF0ZRJJCgRzcGVjGAEgASgLMjsuZ2l0aHViLmNvbS5ha3VpdHkua2FyZ28uYXBpLnYxYWxwaGExLlByb21vdGlvblRlbXBsYXRlU3BlYyKjAQoVUHJvbW90aW9uVGVtcGxhdGVTcGVjEkYKBHZhcnMYAiADKAsyOC5naXRodWIuY29tLmFrdWl0eS5rYXJnby5hcGkudjFhbHBoYTEuRXhwcmVzc2lvblZhcmlhYmxlEkIKBXN0ZXBzGAEgAygLMjMuZ2l0aHViLmNvbS5ha3VpdHkua2FyZ28uYXBpLnYxYWxwaGExLlByb21vdGlvblN0ZXAi5gEKEFJlcG9TdWJzY3JpcHRpb24SQgoDZ2l0GAEgASgLMjUuZ2l0aHViLmNvbS5ha3VpdHkua2FyZ28uYXBpLnYxYWxwaGExLkdpdFN1YnNjcmlwdGlvbhJGCgVpbWFnZRgCIAEoCzI3LmdpdGh1Yi5jb20uYWt1aXR5LmthcmdvLmFwaS52MWFscGhhMS5JbWFnZVN1YnNjcmlwdGlvbhJGCgVjaGFydBgDIAEoCzI3LmdpdGh1Yi5jb20uYWt1aXR5LmthcmdvLmFwaS52MWFscGhhMS5DaGFydFN1YnNjcmlwdGlvbiLNAQoFU3RhZ2USQgoIbWV0YWRhdGEYASABKAsyMC5rOHMuaW8uYXBpbWFjaGluZXJ5LnBrZy5hcGlzLm1ldGEudjEuT2JqZWN0TWV0YRI9CgRzcGVjGAIgASgLMi8uZ2l0aHViLmNvbS5ha3VpdHkua2FyZ28uYXBpLnYxYWxwaGExLlN0YWdlU3BlYxJBCgZzdGF0dXMYAyABKAsyMS5naXRodWIuY29tLmFrdWl0eS5rYXJnby5hcGkudjFhbHBoYTEuU3RhZ2VTdGF0dXMiiQEKCVN0YWdlTGlzdBJACghtZXRhZGF0YRgBIAEoCzIuLms4cy5pby5hcGltYWNoaW5lcnkucGtnLmFwaXMubWV0YS52MS5MaXN0TWV0YRI6CgVpdGVtcxgCIAMoCzIrLmdpdGh1Yi5jb20uYWt1aXR5LmthcmdvLmFwaS52MWFscGhhMS5TdGFnZSLQAgoJU3RhZ2VTcGVjEg0KBXNoYXJkGAQgASgJEkYKBHZhcnMYByADKAsyOC5naXRodWIuY29tLmFrdWl0eS5rYXJnby5hcGkudjFhbHBoYTEuRXhwcmVzc2lvblZhcmlhYmxlEk4KEHJlcXVlc3RlZEZyZWlnaHQYBSADKAsyNC5naXRodWIuY29tLmFrdWl0eS5rYXJnby5hcGkudjFhbHBoYTEuRnJlaWdodFJlcXVlc3QSUgoRcHJvbW90aW9uVGVtcGxhdGUYBiABKAsyNy5naXRodWIuY29tLmFrdWl0eS5rYXJnby5hcGkudjFhbHBoYTEuUHJvbW90aW9uVGVtcGxhdGUSSAoMdmVyaWZpY2F0aW9uGAMgASgLMjIuZ2l0aHViLmNvbS5ha3VpdHkua2FyZ28uYXBpLnYxYWxwaGExLlZlcmlmaWNhdGlvbiL2AwoLU3RhZ2VTdGF0dXMSQwoKY29uZGl0aW9ucxgNIAMoCzIvLms4cy5pby5hcGltYWNoaW5lcnkucGtnLmFwaXMubWV0YS52MS5Db25kaXRpb24SGgoSbGFzdEhhbmRsZWRSZWZyZXNoGAsgASgJEg0KBXBoYXNlGAEgASgJEk8KDmZyZWlnaHRIaXN0b3J5GAQgAygLMjcuZ2l0aHViLmNvbS5ha3VpdHkua2FyZ28uYXBpLnYxYWxwaGExLkZyZWlnaHRDb2xsZWN0aW9uEhYKDmZyZWlnaHRTdW1tYXJ5GAwgASgJEjwKBmhlYWx0aBgIIAEoCzIsLmdpdGh1Yi5jb20uYWt1aXR5LmthcmdvLmFwaS52MWFscGhhMS5IZWFsdGgSDwoHbWVzc2FnZRgJIAEoCRIaChJvYnNlcnZlZEdlbmVyYXRpb24YBiABKAMSUgoQY3VycmVudFByb21vdGlvbhgHIAEoCzI4LmdpdGh1Yi5jb20uYWt1aXR5LmthcmdvLmFwaS52MWFscGhhMS5Qcm9tb3Rpb25SZWZlcmVuY2USTwoNbGFzdFByb21vdGlvbhgKIAEoCzI4LmdpdGh1Yi5jb20uYWt1aXR5LmthcmdvLmFwaS52MWFscGhhMS5Qcm9tb3Rpb25SZWZlcmVuY2Ui2gEKFVN0ZXBFeGVjdXRpb25NZXRhZGF0YRINCgVhbGlhcxgBIAEoCRI9CglzdGFydGVkQXQYAiABKAsyKi5rOHMuaW8uYXBpbWFjaGluZXJ5LnBrZy5hcGlzLm1ldGEudjEuVGltZRI+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]);
 
 /**
  * AnalysisRunArgument represents an argument to be added to an AnalysisRun.
@@ -1694,9 +1694,13 @@ export type Project = Message<"github.com.akuity.kargo.api.v1alpha1.Project"> &
   /**
    * Spec describes a Project.
    *
-   * @generated from field: optional github.com.akuity.kargo.api.v1alpha1.ProjectSpec spec = 2;
+   * Deprecated: Create a ProjectConfig resource with the same name as the
+   * Project resource in the Project's namespace. The ProjectConfig resource
+   * can be used to configure the Project.
+   *
+   * @generated from field: optional github.com.akuity.kargo.api.v1alpha1.ProjectConfigSpec spec = 2;
    */
-  spec?: ProjectSpec;
+  spec?: ProjectConfigSpec;
 
   /**
    * Status describes the Project's current status.
@@ -1714,38 +1718,68 @@ export const ProjectSchema: GenMessage<Project> = /*@__PURE__*/
   messageDesc(file_api_v1alpha1_generated, 34);
 
 /**
- * ProjectList is a list of Project resources.
+ * ProjectConfig is a resource type that describes the configuration of a
+ * Project.
  *
- * @generated from message github.com.akuity.kargo.api.v1alpha1.ProjectList
+ * @generated from message github.com.akuity.kargo.api.v1alpha1.ProjectConfig
  */
-export type ProjectList = Message<"github.com.akuity.kargo.api.v1alpha1.ProjectList"> & {
+export type ProjectConfig = Message<"github.com.akuity.kargo.api.v1alpha1.ProjectConfig"> & {
+  /**
+   * @generated from field: optional k8s.io.apimachinery.pkg.apis.meta.v1.ObjectMeta metadata = 1;
+   */
+  metadata?: ObjectMeta;
+
+  /**
+   * Spec describes the configuration of a Project.
+   *
+   * @generated from field: optional github.com.akuity.kargo.api.v1alpha1.ProjectConfigSpec spec = 2;
+   */
+  spec?: ProjectConfigSpec;
+};
+
+/**
+ * Describes the message github.com.akuity.kargo.api.v1alpha1.ProjectConfig.
+ * Use `create(ProjectConfigSchema)` to create a new message.
+ */
+export const ProjectConfigSchema: GenMessage<ProjectConfig> = /*@__PURE__*/
+  messageDesc(file_api_v1alpha1_generated, 35);
+
+/**
+ * ProjectConfigList is a list of ProjectConfig resources.
+ *
+ * @generated from message github.com.akuity.kargo.api.v1alpha1.ProjectConfigList
+ */
+export type ProjectConfigList = Message<"github.com.akuity.kargo.api.v1alpha1.ProjectConfigList"> & {
   /**
    * @generated from field: optional k8s.io.apimachinery.pkg.apis.meta.v1.ListMeta metadata = 1;
    */
   metadata?: ListMeta;
 
   /**
-   * @generated from field: repeated github.com.akuity.kargo.api.v1alpha1.Project items = 2;
+   * @generated from field: repeated github.com.akuity.kargo.api.v1alpha1.ProjectConfig items = 2;
    */
-  items: Project[];
+  items: ProjectConfig[];
 };
 
 /**
- * Describes the message github.com.akuity.kargo.api.v1alpha1.ProjectList.
- * Use `create(ProjectListSchema)` to create a new message.
+ * Describes the message github.com.akuity.kargo.api.v1alpha1.ProjectConfigList.
+ * Use `create(ProjectConfigListSchema)` to create a new message.
  */
-export const ProjectListSchema: GenMessage<ProjectList> = /*@__PURE__*/
-  messageDesc(file_api_v1alpha1_generated, 35);
+export const ProjectConfigListSchema: GenMessage<ProjectConfigList> = /*@__PURE__*/
+  messageDesc(file_api_v1alpha1_generated, 36);
 
 /**
- * ProjectSpec describes a Project.
+ * ProjectSpec is a deprecated alias for ProjectConfigSpec. It is retained for
+ * backwards compatibility.
  *
- * @generated from message github.com.akuity.kargo.api.v1alpha1.ProjectSpec
+ * Deprecated: Use ProjectConfigSpec instead.
+ *
+ * @generated from message github.com.akuity.kargo.api.v1alpha1.ProjectConfigSpec
  */
-export type ProjectSpec = Message<"github.com.akuity.kargo.api.v1alpha1.ProjectSpec"> & {
+export type ProjectConfigSpec = Message<"github.com.akuity.kargo.api.v1alpha1.ProjectConfigSpec"> & {
   /**
    * PromotionPolicies defines policies governing the promotion of Freight to
-   * specific Stages within this Project.
+   * specific Stages within the Project.
    *
    * @generated from field: repeated github.com.akuity.kargo.api.v1alpha1.PromotionPolicy promotionPolicies = 1;
    */
@@ -1753,11 +1787,35 @@ export type ProjectSpec = Message<"github.com.akuity.kargo.api.v1alpha1.ProjectS
 };
 
 /**
- * Describes the message github.com.akuity.kargo.api.v1alpha1.ProjectSpec.
- * Use `create(ProjectSpecSchema)` to create a new message.
+ * Describes the message github.com.akuity.kargo.api.v1alpha1.ProjectConfigSpec.
+ * Use `create(ProjectConfigSpecSchema)` to create a new message.
  */
-export const ProjectSpecSchema: GenMessage<ProjectSpec> = /*@__PURE__*/
-  messageDesc(file_api_v1alpha1_generated, 36);
+export const ProjectConfigSpecSchema: GenMessage<ProjectConfigSpec> = /*@__PURE__*/
+  messageDesc(file_api_v1alpha1_generated, 37);
+
+/**
+ * ProjectList is a list of Project resources.
+ *
+ * @generated from message github.com.akuity.kargo.api.v1alpha1.ProjectList
+ */
+export type ProjectList = Message<"github.com.akuity.kargo.api.v1alpha1.ProjectList"> & {
+  /**
+   * @generated from field: optional k8s.io.apimachinery.pkg.apis.meta.v1.ListMeta metadata = 1;
+   */
+  metadata?: ListMeta;
+
+  /**
+   * @generated from field: repeated github.com.akuity.kargo.api.v1alpha1.Project items = 2;
+   */
+  items: Project[];
+};
+
+/**
+ * Describes the message github.com.akuity.kargo.api.v1alpha1.ProjectList.
+ * Use `create(ProjectListSchema)` to create a new message.
+ */
+export const ProjectListSchema: GenMessage<ProjectList> = /*@__PURE__*/
+  messageDesc(file_api_v1alpha1_generated, 38);
 
 /**
  * ProjectStatus describes a Project's current status.
@@ -1803,7 +1861,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, 37);
+  messageDesc(file_api_v1alpha1_generated, 39);
 
 /**
  * Promotion represents a request to transition a particular Stage into a
@@ -1841,7 +1899,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, 38);
+  messageDesc(file_api_v1alpha1_generated, 40);
 
 /**
  * PromotionList contains a list of Promotion
@@ -1865,7 +1923,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, 39);
+  messageDesc(file_api_v1alpha1_generated, 41);
 
 /**
  * PromotionPolicy defines policies governing the promotion of Freight to a
@@ -1901,7 +1959,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, 40);
+  messageDesc(file_api_v1alpha1_generated, 42);
 
 /**
  * PromotionReference contains the relevant information about a Promotion
@@ -1944,7 +2002,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, 41);
+  messageDesc(file_api_v1alpha1_generated, 43);
 
 /**
  * PromotionSpec describes the desired transition of a specific Stage into a
@@ -2007,7 +2065,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, 42);
+  messageDesc(file_api_v1alpha1_generated, 44);
 
 /**
  * PromotionStatus describes the current state of the transition represented by
@@ -2105,7 +2163,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, 43);
+  messageDesc(file_api_v1alpha1_generated, 45);
 
 /**
  * PromotionStep describes a directive to be executed as part of a Promotion.
@@ -2180,7 +2238,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, 44);
+  messageDesc(file_api_v1alpha1_generated, 46);
 
 /**
  * PromotionStepRetry describes the retry policy for a PromotionStep.
@@ -2239,7 +2297,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, 45);
+  messageDesc(file_api_v1alpha1_generated, 47);
 
 /**
  * @generated from message github.com.akuity.kargo.api.v1alpha1.PromotionTask
@@ -2266,7 +2324,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, 46);
+  messageDesc(file_api_v1alpha1_generated, 48);
 
 /**
  * PromotionTaskList contains a list of PromotionTasks.
@@ -2290,7 +2348,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, 47);
+  messageDesc(file_api_v1alpha1_generated, 49);
 
 /**
  * PromotionTaskReference describes a reference to a PromotionTask.
@@ -2327,7 +2385,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, 48);
+  messageDesc(file_api_v1alpha1_generated, 50);
 
 /**
  * @generated from message github.com.akuity.kargo.api.v1alpha1.PromotionTaskSpec
@@ -2361,7 +2419,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, 49);
+  messageDesc(file_api_v1alpha1_generated, 51);
 
 /**
  * PromotionTemplate defines a template for a Promotion that can be used to
@@ -2381,7 +2439,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, 50);
+  messageDesc(file_api_v1alpha1_generated, 52);
 
 /**
  * PromotionTemplateSpec describes the (partial) specification of a Promotion
@@ -2417,7 +2475,7 @@ 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, 51);
+  messageDesc(file_api_v1alpha1_generated, 53);
 
 /**
  * RepoSubscription describes a subscription to ONE OF a Git repository, a
@@ -2453,7 +2511,7 @@ export type RepoSubscription = Message<"github.com.akuity.kargo.api.v1alpha1.Rep
  * Use `create(RepoSubscriptionSchema)` to create a new message.
  */
 export const RepoSubscriptionSchema: GenMessage<RepoSubscription> = /*@__PURE__*/
-  messageDesc(file_api_v1alpha1_generated, 52);
+  messageDesc(file_api_v1alpha1_generated, 54);
 
 /**
  * Stage is the Kargo API's main type.
@@ -2489,7 +2547,7 @@ export type Stage = Message<"github.com.akuity.kargo.api.v1alpha1.Stage"> & {
  * Use `create(StageSchema)` to create a new message.
  */
 export const StageSchema: GenMessage<Stage> = /*@__PURE__*/
-  messageDesc(file_api_v1alpha1_generated, 53);
+  messageDesc(file_api_v1alpha1_generated, 55);
 
 /**
  * StageList is a list of Stage resources.
@@ -2513,7 +2571,7 @@ export type StageList = Message<"github.com.akuity.kargo.api.v1alpha1.StageList"
  * Use `create(StageListSchema)` to create a new message.
  */
 export const StageListSchema: GenMessage<StageList> = /*@__PURE__*/
-  messageDesc(file_api_v1alpha1_generated, 54);
+  messageDesc(file_api_v1alpha1_generated, 56);
 
 /**
  * StageSpec describes the sources of Freight used by a Stage and how to
@@ -2580,7 +2638,7 @@ export type StageSpec = Message<"github.com.akuity.kargo.api.v1alpha1.StageSpec"
  * Use `create(StageSpecSchema)` to create a new message.
  */
 export const StageSpecSchema: GenMessage<StageSpec> = /*@__PURE__*/
-  messageDesc(file_api_v1alpha1_generated, 55);
+  messageDesc(file_api_v1alpha1_generated, 57);
 
 /**
  * StageStatus describes a Stages's current and recent Freight, health, and
@@ -2687,7 +2745,7 @@ export type StageStatus = Message<"github.com.akuity.kargo.api.v1alpha1.StageSta
  * Use `create(StageStatusSchema)` to create a new message.
  */
 export const StageStatusSchema: GenMessage<StageStatus> = /*@__PURE__*/
-  messageDesc(file_api_v1alpha1_generated, 56);
+  messageDesc(file_api_v1alpha1_generated, 58);
 
 /**
  * StepExecutionMetadata tracks metadata pertaining to the execution of
@@ -2746,7 +2804,7 @@ export type StepExecutionMetadata = Message<"github.com.akuity.kargo.api.v1alpha
  * Use `create(StepExecutionMetadataSchema)` to create a new message.
  */
 export const StepExecutionMetadataSchema: GenMessage<StepExecutionMetadata> = /*@__PURE__*/
-  messageDesc(file_api_v1alpha1_generated, 57);
+  messageDesc(file_api_v1alpha1_generated, 59);
 
 /**
  * Verification describes how to verify that a Promotion has been successful
@@ -2785,7 +2843,7 @@ export type Verification = Message<"github.com.akuity.kargo.api.v1alpha1.Verific
  * Use `create(VerificationSchema)` to create a new message.
  */
 export const VerificationSchema: GenMessage<Verification> = /*@__PURE__*/
-  messageDesc(file_api_v1alpha1_generated, 58);
+  messageDesc(file_api_v1alpha1_generated, 60);
 
 /**
  * VerificationInfo contains the details of an instance of a Verification
@@ -2855,7 +2913,7 @@ export type VerificationInfo = Message<"github.com.akuity.kargo.api.v1alpha1.Ver
  * Use `create(VerificationInfoSchema)` to create a new message.
  */
 export const VerificationInfoSchema: GenMessage<VerificationInfo> = /*@__PURE__*/
-  messageDesc(file_api_v1alpha1_generated, 59);
+  messageDesc(file_api_v1alpha1_generated, 61);
 
 /**
  * VerifiedStage describes a Stage in which Freight has been verified.
@@ -2887,7 +2945,7 @@ export type VerifiedStage = Message<"github.com.akuity.kargo.api.v1alpha1.Verifi
  * Use `create(VerifiedStageSchema)` to create a new message.
  */
 export const VerifiedStageSchema: GenMessage<VerifiedStage> = /*@__PURE__*/
-  messageDesc(file_api_v1alpha1_generated, 60);
+  messageDesc(file_api_v1alpha1_generated, 62);
 
 /**
  * Warehouse is a source of Freight.
@@ -2922,7 +2980,7 @@ export type Warehouse = Message<"github.com.akuity.kargo.api.v1alpha1.Warehouse"
  * Use `create(WarehouseSchema)` to create a new message.
  */
 export const WarehouseSchema: GenMessage<Warehouse> = /*@__PURE__*/
-  messageDesc(file_api_v1alpha1_generated, 61);
+  messageDesc(file_api_v1alpha1_generated, 63);
 
 /**
  * WarehouseList is a list of Warehouse resources.
@@ -2946,7 +3004,7 @@ export type WarehouseList = Message<"github.com.akuity.kargo.api.v1alpha1.Wareho
  * Use `create(WarehouseListSchema)` to create a new message.
  */
 export const WarehouseListSchema: GenMessage<WarehouseList> = /*@__PURE__*/
-  messageDesc(file_api_v1alpha1_generated, 62);
+  messageDesc(file_api_v1alpha1_generated, 64);
 
 /**
  * WarehouseSpec describes sources of versioned artifacts to be included in
@@ -3012,7 +3070,7 @@ export type WarehouseSpec = Message<"github.com.akuity.kargo.api.v1alpha1.Wareho
  * Use `create(WarehouseSpecSchema)` to create a new message.
  */
 export const WarehouseSpecSchema: GenMessage<WarehouseSpec> = /*@__PURE__*/
-  messageDesc(file_api_v1alpha1_generated, 63);
+  messageDesc(file_api_v1alpha1_generated, 65);
 
 /**
  * WarehouseStatus describes a Warehouse's most recently observed state.
@@ -3071,5 +3129,5 @@ export type WarehouseStatus = Message<"github.com.akuity.kargo.api.v1alpha1.Ware
  * Use `create(WarehouseStatusSchema)` to create a new message.
  */
 export const WarehouseStatusSchema: GenMessage<WarehouseStatus> = /*@__PURE__*/
-  messageDesc(file_api_v1alpha1_generated, 64);
+  messageDesc(file_api_v1alpha1_generated, 66);
 
diff --git a/ui/src/gen/schema/projectconfigs.kargo.akuity.io_v1alpha1.json b/ui/src/gen/schema/projectconfigs.kargo.akuity.io_v1alpha1.json
new file mode 100644
index 00000000..cbc6f956
--- /dev/null
+++ b/ui/src/gen/schema/projectconfigs.kargo.akuity.io_v1alpha1.json
@@ -0,0 +1,46 @@
+{
+  "$schema": "http://json-schema.org/draft-04/schema#",
+  "description": "ProjectConfig is a resource type that describes the configuration of a\nProject.",
+  "properties": {
+    "apiVersion": {
+      "description": "APIVersion defines the versioned schema of this representation of an object.\nServers should convert recognized schemas to the latest internal value, and\nmay reject unrecognized values.\nMore info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources",
+      "type": "string"
+    },
+    "kind": {
+      "description": "Kind is a string value representing the REST resource this object represents.\nServers may infer this from the endpoint the client submits requests to.\nCannot be updated.\nIn CamelCase.\nMore info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds",
+      "type": "string"
+    },
+    "metadata": {
+      "type": "object"
+    },
+    "spec": {
+      "description": "Spec describes the configuration of a Project.",
+      "properties": {
+        "promotionPolicies": {
+          "description": "PromotionPolicies defines policies governing the promotion of Freight to\nspecific Stages within the Project.",
+          "items": {
+            "description": "PromotionPolicy defines policies governing the promotion of Freight to a\nspecific Stage.",
+            "properties": {
+              "autoPromotionEnabled": {
+                "description": "AutoPromotionEnabled indicates whether new Freight can automatically be\npromoted into the Stage referenced by the Stage field. Note: There are may\nbe other conditions also required for an auto-promotion to occur. This\nfield defaults to false, but is commonly set to true for Stages that\nsubscribe to Warehouses instead of other, upstream Stages. This allows\nusers to define Stages that are automatically updated as soon as new\nartifacts are detected.",
+                "type": "boolean"
+              },
+              "stage": {
+                "minLength": 1,
+                "pattern": "^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$",
+                "type": "string"
+              }
+            },
+            "required": [
+              "stage"
+            ],
+            "type": "object"
+          },
+          "type": "array"
+        }
+      },
+      "type": "object"
+    }
+  },
+  "type": "object"
+}
\ No newline at end of file
diff --git a/ui/src/gen/schema/projects.kargo.akuity.io_v1alpha1.json b/ui/src/gen/schema/projects.kargo.akuity.io_v1alpha1.json
index 6b82a20f..8d6120c8 100644
--- a/ui/src/gen/schema/projects.kargo.akuity.io_v1alpha1.json
+++ b/ui/src/gen/schema/projects.kargo.akuity.io_v1alpha1.json
@@ -14,10 +14,10 @@
       "type": "object"
     },
     "spec": {
-      "description": "Spec describes a Project.",
+      "description": "Spec describes a Project.\n\nDeprecated: Create a ProjectConfig resource with the same name as the\nProject resource in the Project's namespace. The ProjectConfig resource\ncan be used to configure the Project.",
       "properties": {
         "promotionPolicies": {
-          "description": "PromotionPolicies defines policies governing the promotion of Freight to\nspecific Stages within this Project.",
+          "description": "PromotionPolicies defines policies governing the promotion of Freight to\nspecific Stages within the Project.",
           "items": {
             "description": "PromotionPolicy defines policies governing the promotion of Freight to a\nspecific Stage.",
             "properties": {



Test patch:
diff --git a/internal/controller/management/projects/projects_test.go b/internal/controller/management/projects/projects_test.go
index 82c55ce0..0b0819c9 100644
--- a/internal/controller/management/projects/projects_test.go
+++ b/internal/controller/management/projects/projects_test.go
@@ -31,16 +31,19 @@ func TestNewReconciler(t *testing.T) {
 	require.NotNil(t, r.getProjectFn)
 	require.NotNil(t, r.syncProjectFn)
 	require.NotNil(t, r.ensureNamespaceFn)
+	require.NotNil(t, r.updateProjectFn)
 	require.NotNil(t, r.patchProjectStatusFn)
 	require.NotNil(t, r.getNamespaceFn)
 	require.NotNil(t, r.createNamespaceFn)
 	require.NotNil(t, r.patchOwnerReferencesFn)
 	require.NotNil(t, r.ensureFinalizerFn)
 	require.NotNil(t, r.ensureAPIAdminPermissionsFn)
+	require.NotNil(t, r.ensureControllerPermissionsFn)
 	require.NotNil(t, r.ensureDefaultProjectRolesFn)
 	require.NotNil(t, r.createServiceAccountFn)
 	require.NotNil(t, r.createRoleFn)
 	require.NotNil(t, r.createRoleBindingFn)
+	require.NotNil(t, r.createProjectConfigFn)
 }
 
 func TestReconcile(t *testing.T) {
@@ -1218,6 +1221,67 @@ func TestMigratePhaseToConditions(t *testing.T) {
 	}
 }
 
+func TestMigrateSpecToProjectConfig(t *testing.T) {
+	tests := []struct {
+		name       string
+		project    *kargoapi.Project
+		assertions func(t *testing.T, projectConfig *kargoapi.ProjectConfig, ok bool)
+	}{
+		{
+			name: "nil spec",
+			project: &kargoapi.Project{
+				Spec: nil,
+			},
+			assertions: func(t *testing.T, projectConfig *kargoapi.ProjectConfig, ok bool) {
+				require.Nil(t, projectConfig)
+				require.False(t, ok)
+			},
+		},
+		{
+			name: "empty promotion policies",
+			project: &kargoapi.Project{
+				Spec: &kargoapi.ProjectSpec{ // nolint:staticcheck
+					PromotionPolicies: []kargoapi.PromotionPolicy{},
+				},
+			},
+			assertions: func(t *testing.T, projectConfig *kargoapi.ProjectConfig, ok bool) {
+				require.Nil(t, projectConfig)
+				require.True(t, ok)
+				// Verify that the spec was cleared
+				require.Nil(t, projectConfig)
+			},
+		},
+		{
+			name: "with promotion policies",
+			project: &kargoapi.Project{
+				ObjectMeta: metav1.ObjectMeta{
+					Name: "test-project",
+				},
+				Spec: &kargoapi.ProjectSpec{ // nolint:staticcheck
+					PromotionPolicies: []kargoapi.PromotionPolicy{
+						{Stage: "policy-1"},
+					},
+				},
+			},
+			assertions: func(t *testing.T, projectConfig *kargoapi.ProjectConfig, ok bool) {
+				require.NotNil(t, projectConfig)
+				require.True(t, ok)
+				require.Equal(t, "test-project", projectConfig.Name)
+				require.Equal(t, "test-project", projectConfig.Namespace)
+				require.Len(t, projectConfig.Spec.PromotionPolicies, 1)
+				require.Equal(t, "policy-1", projectConfig.Spec.PromotionPolicies[0].Stage)
+			},
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			projectConfig, ok := migrateSpecToProjectConfig(tt.project)
+			tt.assertions(t, projectConfig, ok)
+		})
+	}
+}
+
 func TestMustReconcileProject(t *testing.T) {
 	tests := []struct {
 		name       string
diff --git a/internal/controller/stages/regular_stages_test.go b/internal/controller/stages/regular_stages_test.go
index aeaf5270..88e935eb 100644
--- a/internal/controller/stages/regular_stages_test.go
+++ b/internal/controller/stages/regular_stages_test.go
@@ -4405,11 +4405,12 @@ func TestRegularStageReconciler_autoPromoteFreight(t *testing.T) {
 				},
 			},
 			objects: []client.Object{
-				&kargoapi.Project{
+				&kargoapi.ProjectConfig{
 					ObjectMeta: metav1.ObjectMeta{
-						Name: "fake-project",
+						Name:      "fake-project",
+						Namespace: "fake-project",
 					},
-					Spec: &kargoapi.ProjectSpec{
+					Spec: kargoapi.ProjectConfigSpec{
 						PromotionPolicies: []kargoapi.PromotionPolicy{
 							{
 								Stage:                "test-stage",
@@ -4435,7 +4436,7 @@ func TestRegularStageReconciler_autoPromoteFreight(t *testing.T) {
 			},
 		},
 		{
-			name: "project not found",
+			name: "projectconfig not found",
 			stage: &kargoapi.Stage{
 				ObjectMeta: metav1.ObjectMeta{
 					Namespace: "fake-project",
@@ -4455,11 +4456,16 @@ func TestRegularStageReconciler_autoPromoteFreight(t *testing.T) {
 			assertions: func(
 				t *testing.T,
 				_ *fakeevent.EventRecorder,
-				_ client.Client,
+				c client.Client,
 				_ kargoapi.StageStatus,
 				err error,
 			) {
-				require.ErrorContains(t, err, "error getting Project")
+				require.NoError(t, err)
+
+				// Verify no promotions were created
+				promoList := &kargoapi.PromotionList{}
+				require.NoError(t, c.List(context.Background(), promoList, client.InNamespace("fake-project")))
+				assert.Empty(t, promoList.Items)
 			},
 		},
 		{
@@ -4493,11 +4499,12 @@ func TestRegularStageReconciler_autoPromoteFreight(t *testing.T) {
 				},
 			},
 			objects: []client.Object{
-				&kargoapi.Project{
+				&kargoapi.ProjectConfig{
 					ObjectMeta: metav1.ObjectMeta{
-						Name: "fake-project",
+						Name:      "fake-project",
+						Namespace: "fake-project",
 					},
-					Spec: &kargoapi.ProjectSpec{
+					Spec: kargoapi.ProjectConfigSpec{
 						PromotionPolicies: []kargoapi.PromotionPolicy{
 							{
 								Stage:                "test-stage",
@@ -4582,11 +4589,12 @@ func TestRegularStageReconciler_autoPromoteFreight(t *testing.T) {
 				},
 			},
 			objects: []client.Object{
-				&kargoapi.Project{
+				&kargoapi.ProjectConfig{
 					ObjectMeta: metav1.ObjectMeta{
-						Name: "fake-project",
+						Name:      "fake-project",
+						Namespace: "fake-project",
 					},
-					Spec: &kargoapi.ProjectSpec{
+					Spec: kargoapi.ProjectConfigSpec{
 						PromotionPolicies: []kargoapi.PromotionPolicy{
 							{
 								Stage:                "test-stage",
@@ -4650,11 +4658,12 @@ func TestRegularStageReconciler_autoPromoteFreight(t *testing.T) {
 				},
 			},
 			objects: []client.Object{
-				&kargoapi.Project{
+				&kargoapi.ProjectConfig{
 					ObjectMeta: metav1.ObjectMeta{
-						Name: "fake-project",
+						Name:      "fake-project",
+						Namespace: "fake-project",
 					},
-					Spec: &kargoapi.ProjectSpec{
+					Spec: kargoapi.ProjectConfigSpec{
 						PromotionPolicies: []kargoapi.PromotionPolicy{
 							{
 								Stage:                "test-stage",
@@ -4741,11 +4750,12 @@ func TestRegularStageReconciler_autoPromoteFreight(t *testing.T) {
 				},
 			},
 			objects: []client.Object{
-				&kargoapi.Project{
+				&kargoapi.ProjectConfig{
 					ObjectMeta: metav1.ObjectMeta{
-						Name: "fake-project",
+						Name:      "fake-project",
+						Namespace: "fake-project",
 					},
-					Spec: &kargoapi.ProjectSpec{
+					Spec: kargoapi.ProjectConfigSpec{
 						PromotionPolicies: []kargoapi.PromotionPolicy{
 							{
 								Stage:                "test-stage",
@@ -4825,11 +4835,12 @@ func TestRegularStageReconciler_autoPromoteFreight(t *testing.T) {
 				},
 			},
 			objects: []client.Object{
-				&kargoapi.Project{
+				&kargoapi.ProjectConfig{
 					ObjectMeta: metav1.ObjectMeta{
-						Name: "fake-project",
+						Name:      "fake-project",
+						Namespace: "fake-project",
 					},
-					Spec: &kargoapi.ProjectSpec{
+					Spec: kargoapi.ProjectConfigSpec{
 						PromotionPolicies: []kargoapi.PromotionPolicy{
 							{
 								Stage:                "test-stage",
@@ -4940,11 +4951,12 @@ func TestRegularStageReconciler_autoPromoteFreight(t *testing.T) {
 				},
 			},
 			objects: []client.Object{
-				&kargoapi.Project{
+				&kargoapi.ProjectConfig{
 					ObjectMeta: metav1.ObjectMeta{
-						Name: "fake-project",
+						Name:      "fake-project",
+						Namespace: "fake-project",
 					},
-					Spec: &kargoapi.ProjectSpec{
+					Spec: kargoapi.ProjectConfigSpec{
 						PromotionPolicies: []kargoapi.PromotionPolicy{
 							{
 								Stage:                "test-stage",
@@ -5032,11 +5044,12 @@ func TestRegularStageReconciler_autoPromoteFreight(t *testing.T) {
 				},
 			},
 			objects: []client.Object{
-				&kargoapi.Project{
+				&kargoapi.ProjectConfig{
 					ObjectMeta: metav1.ObjectMeta{
-						Name: "fake-project",
+						Name:      "fake-project",
+						Namespace: "fake-project",
 					},
-					Spec: &kargoapi.ProjectSpec{
+					Spec: kargoapi.ProjectConfigSpec{
 						PromotionPolicies: []kargoapi.PromotionPolicy{
 							{
 								Stage:                "test-stage",
@@ -5137,11 +5150,12 @@ func TestRegularStageReconciler_autoPromoteFreight(t *testing.T) {
 				},
 			},
 			objects: []client.Object{
-				&kargoapi.Project{
+				&kargoapi.ProjectConfig{
 					ObjectMeta: metav1.ObjectMeta{
-						Name: "fake-project",
+						Name:      "fake-project",
+						Namespace: "fake-project",
 					},
-					Spec: &kargoapi.ProjectSpec{
+					Spec: kargoapi.ProjectConfigSpec{
 						PromotionPolicies: []kargoapi.PromotionPolicy{
 							{
 								Stage:                "test-stage",
@@ -5217,11 +5231,12 @@ func TestRegularStageReconciler_autoPromoteFreight(t *testing.T) {
 				},
 			},
 			objects: []client.Object{
-				&kargoapi.Project{
+				&kargoapi.ProjectConfig{
 					ObjectMeta: metav1.ObjectMeta{
-						Name: "fake-project",
+						Name:      "fake-project",
+						Namespace: "fake-project",
 					},
-					Spec: &kargoapi.ProjectSpec{
+					Spec: kargoapi.ProjectConfigSpec{
 						PromotionPolicies: []kargoapi.PromotionPolicy{
 							{
 								Stage:                "test-stage",
@@ -5295,11 +5310,12 @@ func TestRegularStageReconciler_autoPromoteFreight(t *testing.T) {
 				},
 			},
 			objects: []client.Object{
-				&kargoapi.Project{
+				&kargoapi.ProjectConfig{
 					ObjectMeta: metav1.ObjectMeta{
-						Name: "fake-project",
+						Name:      "fake-project",
+						Namespace: "fake-project",
 					},
-					Spec: &kargoapi.ProjectSpec{
+					Spec: kargoapi.ProjectConfigSpec{
 						PromotionPolicies: []kargoapi.PromotionPolicy{
 							{
 								Stage:                "test-stage",
@@ -5368,11 +5384,12 @@ func TestRegularStageReconciler_autoPromoteFreight(t *testing.T) {
 				},
 			},
 			objects: []client.Object{
-				&kargoapi.Project{
+				&kargoapi.ProjectConfig{
 					ObjectMeta: metav1.ObjectMeta{
-						Name: "fake-project",
+						Name:      "fake-project",
+						Namespace: "fake-project",
 					},
-					Spec: &kargoapi.ProjectSpec{
+					Spec: kargoapi.ProjectConfigSpec{
 						PromotionPolicies: []kargoapi.PromotionPolicy{
 							{
 								Stage:                "test-stage",
@@ -5471,20 +5488,20 @@ func TestRegularStageReconciler_autoPromotionAllowed(t *testing.T) {
 		assertions  func(*testing.T, bool, error)
 	}{
 		{
-			name: "project not found",
+			name: "no ProjectConfig for Project",
 			stage: types.NamespacedName{
-				Namespace: "default",
+				Namespace: "test-project",
 				Name:      "test-stage",
 			},
 			assertions: func(t *testing.T, allowed bool, err error) {
-				require.ErrorContains(t, err, "error getting Project")
+				require.NoError(t, err)
 				assert.False(t, allowed)
 			},
 		},
 		{
-			name: "error getting project",
+			name: "error getting ProjectConfig",
 			stage: types.NamespacedName{
-				Namespace: "default",
+				Namespace: "test-project",
 				Name:      "test-stage",
 			},
 			interceptor: interceptor.Funcs{
@@ -5504,16 +5521,18 @@ func TestRegularStageReconciler_autoPromotionAllowed(t *testing.T) {
 			},
 		},
 		{
-			name: "nil project spec",
+			name: "empty ProjectConfig spec",
 			stage: types.NamespacedName{
-				Namespace: "default",
+				Namespace: "test-project",
 				Name:      "test-stage",
 			},
 			objects: []client.Object{
-				&kargoapi.Project{
+				&kargoapi.ProjectConfig{
 					ObjectMeta: metav1.ObjectMeta{
-						Name: "default",
+						Name:      "test-project",
+						Namespace: "test-project",
 					},
+					Spec: kargoapi.ProjectConfigSpec{},
 				},
 			},
 			assertions: func(t *testing.T, allowed bool, err error) {
@@ -5524,15 +5543,16 @@ func TestRegularStageReconciler_autoPromotionAllowed(t *testing.T) {
 		{
 			name: "empty promotion policies",
 			stage: types.NamespacedName{
-				Namespace: "default",
+				Namespace: "test-project",
 				Name:      "test-stage",
 			},
 			objects: []client.Object{
-				&kargoapi.Project{
+				&kargoapi.ProjectConfig{
 					ObjectMeta: metav1.ObjectMeta{
-						Name: "default",
+						Name:      "test-project",
+						Namespace: "test-project",
 					},
-					Spec: &kargoapi.ProjectSpec{
+					Spec: kargoapi.ProjectConfigSpec{
 						PromotionPolicies: []kargoapi.PromotionPolicy{},
 					},
 				},
@@ -5545,15 +5565,16 @@ func TestRegularStageReconciler_autoPromotionAllowed(t *testing.T) {
 		{
 			name: "stage not found in policies",
 			stage: types.NamespacedName{
-				Namespace: "default",
+				Namespace: "test-project",
 				Name:      "test-stage",
 			},
 			objects: []client.Object{
-				&kargoapi.Project{
+				&kargoapi.ProjectConfig{
 					ObjectMeta: metav1.ObjectMeta{
-						Name: "default",
+						Name:      "test-project",
+						Namespace: "test-project",
 					},
-					Spec: &kargoapi.ProjectSpec{
+					Spec: kargoapi.ProjectConfigSpec{
 						PromotionPolicies: []kargoapi.PromotionPolicy{
 							{
 								Stage:                "other-stage",
@@ -5571,15 +5592,16 @@ func TestRegularStageReconciler_autoPromotionAllowed(t *testing.T) {
 		{
 			name: "auto-promotion enabled",
 			stage: types.NamespacedName{
-				Namespace: "default",
+				Namespace: "test-project",
 				Name:      "test-stage",
 			},
 			objects: []client.Object{
-				&kargoapi.Project{
+				&kargoapi.ProjectConfig{
 					ObjectMeta: metav1.ObjectMeta{
-						Name: "default",
+						Name:      "test-project",
+						Namespace: "test-project",
 					},
-					Spec: &kargoapi.ProjectSpec{
+					Spec: kargoapi.ProjectConfigSpec{
 						PromotionPolicies: []kargoapi.PromotionPolicy{
 							{
 								Stage:                "test-stage",
@@ -5597,15 +5619,16 @@ func TestRegularStageReconciler_autoPromotionAllowed(t *testing.T) {
 		{
 			name: "auto-promotion disabled",
 			stage: types.NamespacedName{
-				Namespace: "default",
+				Namespace: "test-project",
 				Name:      "test-stage",
 			},
 			objects: []client.Object{
-				&kargoapi.Project{
+				&kargoapi.ProjectConfig{
 					ObjectMeta: metav1.ObjectMeta{
-						Name: "default",
+						Name:      "test-project",
+						Namespace: "test-project",
 					},
-					Spec: &kargoapi.ProjectSpec{
+					Spec: kargoapi.ProjectConfigSpec{
 						PromotionPolicies: []kargoapi.PromotionPolicy{
 							{
 								Stage:                "test-stage",
@@ -5623,15 +5646,16 @@ func TestRegularStageReconciler_autoPromotionAllowed(t *testing.T) {
 		{
 			name: "multiple policies - finds correct stage",
 			stage: types.NamespacedName{
-				Namespace: "default",
+				Namespace: "test-project",
 				Name:      "test-stage",
 			},
 			objects: []client.Object{
-				&kargoapi.Project{
+				&kargoapi.ProjectConfig{
 					ObjectMeta: metav1.ObjectMeta{
-						Name: "default",
+						Name:      "test-project",
+						Namespace: "test-project",
 					},
-					Spec: &kargoapi.ProjectSpec{
+					Spec: kargoapi.ProjectConfigSpec{
 						PromotionPolicies: []kargoapi.PromotionPolicy{
 							{
 								Stage:                "stage-1",
@@ -5661,11 +5685,12 @@ func TestRegularStageReconciler_autoPromotionAllowed(t *testing.T) {
 				Name:      "test-stage",
 			},
 			objects: []client.Object{
-				&kargoapi.Project{
+				&kargoapi.ProjectConfig{
 					ObjectMeta: metav1.ObjectMeta{
-						Name: "other-namespace",
+						Name:      "other-namespace",
+						Namespace: "other-namespace",
 					},
-					Spec: &kargoapi.ProjectSpec{
+					Spec: kargoapi.ProjectConfigSpec{
 						PromotionPolicies: []kargoapi.PromotionPolicy{
 							{
 								Stage:                "test-stage",
@@ -5683,15 +5708,16 @@ func TestRegularStageReconciler_autoPromotionAllowed(t *testing.T) {
 		{
 			name: "matches first policy for stage",
 			stage: types.NamespacedName{
-				Namespace: "default",
+				Namespace: "test-project",
 				Name:      "test-stage",
 			},
 			objects: []client.Object{
-				&kargoapi.Project{
+				&kargoapi.ProjectConfig{
 					ObjectMeta: metav1.ObjectMeta{
-						Name: "default",
+						Name:      "test-project",
+						Namespace: "test-project",
 					},
-					Spec: &kargoapi.ProjectSpec{
+					Spec: kargoapi.ProjectConfigSpec{
 						PromotionPolicies: []kargoapi.PromotionPolicy{
 							{
 								Stage:                "test-stage",
diff --git a/internal/webhook/project/webhook_test.go b/internal/webhook/project/webhook_test.go
index e61d27b8..0cea4928 100644
--- a/internal/webhook/project/webhook_test.go
+++ b/internal/webhook/project/webhook_test.go
@@ -3,18 +3,16 @@ package project
 import (
 	"context"
 	"errors"
-	"net/http"
 	"testing"
 
+	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
 	admissionv1 "k8s.io/api/admission/v1"
 	corev1 "k8s.io/api/core/v1"
+	rbacv1 "k8s.io/api/rbac/v1"
 	apierrors "k8s.io/apimachinery/pkg/api/errors"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
-	"k8s.io/apimachinery/pkg/runtime/schema"
-	"k8s.io/apimachinery/pkg/types"
-	"k8s.io/apimachinery/pkg/util/validation/field"
-	"k8s.io/utils/ptr"
+	"k8s.io/apimachinery/pkg/runtime"
 	"sigs.k8s.io/controller-runtime/pkg/client"
 	"sigs.k8s.io/controller-runtime/pkg/client/fake"
 	"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
@@ -22,391 +20,447 @@ import (
 	kargoapi "github.com/akuity/kargo/api/v1alpha1"
 )
 
-func TestNewWebhook(t *testing.T) {
-	testCfg := WebhookConfig{
-		KargoNamespace: "fake-namespace",
-	}
-	w := newWebhook(fake.NewClientBuilder().Build(), testCfg)
-	require.NotNil(t, w)
-	require.Equal(t, testCfg, w.cfg)
-	require.NotNil(t, w.validateSpecFn)
-	require.NotNil(t, w.ensureNamespaceFn)
-	require.NotNil(t, w.ensureProjectAdminPermissionsFn)
-	require.NotNil(t, w.getNamespaceFn)
-	require.NotNil(t, w.createNamespaceFn)
-	require.NotNil(t, w.createRoleBindingFn)
+func TestWebhookConfigFromEnv(t *testing.T) {
+	const kargoNamespace = "test-kargo-namespace"
+	t.Setenv("KARGO_NAMESPACE", kargoNamespace)
+	cfg := WebhookConfigFromEnv()
+	assert.Equal(t, kargoNamespace, cfg.KargoNamespace)
 }
 
-func TestValidateCreate(t *testing.T) {
-	testCases := []struct {
+func Test_webhook_ValidateCreate(t *testing.T) {
+	scheme := runtime.NewScheme()
+	require.NoError(t, corev1.AddToScheme(scheme))
+	require.NoError(t, rbacv1.AddToScheme(scheme))
+	require.NoError(t, kargoapi.AddToScheme(scheme))
+
+	testProjectName := "test-project"
+	testNs := &corev1.Namespace{
+		ObjectMeta: metav1.ObjectMeta{
+			Name: testProjectName,
+			Labels: map[string]string{
+				kargoapi.ProjectLabelKey: kargoapi.LabelTrueValue,
+			},
+		},
+	}
+	testNsNoLabel := &corev1.Namespace{
+		ObjectMeta: metav1.ObjectMeta{
+			Name: testProjectName,
+		},
+	}
+
+	tests := []struct {
 		name       string
-		webhook    *webhook
-		assertions func(*testing.T, error)
+		project    *kargoapi.Project
+		objects    []client.Object
+		isDryRun   bool
+		assertions func(*testing.T, admission.Warnings, error)
 	}{
 		{
-			name: "error validating spec",
-			webhook: &webhook{
-				validateSpecFn: func(f *field.Path, spec *kargoapi.ProjectSpec) field.ErrorList {
-					return field.ErrorList{
-						field.Invalid(
-							f,
-							spec,
-							"something was invalid",
-						),
-					}
+			name: "valid project",
+			project: &kargoapi.Project{
+				ObjectMeta: metav1.ObjectMeta{
+					Name: testProjectName,
 				},
 			},
-			assertions: func(t *testing.T, err error) {
+			assertions: func(t *testing.T, warnings admission.Warnings, err error) {
+				assert.Empty(t, warnings)
+				assert.NoError(t, err)
+			},
+		},
+		{
+			name: "project with deprecated spec",
+			project: &kargoapi.Project{
+				ObjectMeta: metav1.ObjectMeta{
+					Name: testProjectName,
+				},
+				Spec: &kargoapi.ProjectSpec{}, // nolint: staticcheck
+			},
+			assertions: func(t *testing.T, warnings admission.Warnings, err error) {
+				assert.Empty(t, warnings)
 				require.Error(t, err)
+
 				var statusErr *apierrors.StatusError
 				require.True(t, errors.As(err, &statusErr))
-				require.Equal(t, int32(http.StatusUnprocessableEntity), statusErr.ErrStatus.Code)
+
+				assert.Equal(t, metav1.StatusReasonInvalid, statusErr.ErrStatus.Reason)
+				assert.Contains(t, statusErr.ErrStatus.Details.Causes[0].Message, "deprecated field")
+				assert.Equal(t, "spec", statusErr.ErrStatus.Details.Causes[0].Field)
 			},
 		},
 		{
-			name: "error ensuring namespace",
-			webhook: &webhook{
-				validateSpecFn: func(*field.Path, *kargoapi.ProjectSpec) field.ErrorList {
-					return nil
+			name: "namespace exists with project label",
+			project: &kargoapi.Project{
+				ObjectMeta: metav1.ObjectMeta{
+					Name: testProjectName,
 				},
-				ensureNamespaceFn: func(context.Context, *kargoapi.Project) error {
-					return apierrors.NewInternalError(errors.New("something went wrong"))
+			},
+			objects: []client.Object{testNs},
+			assertions: func(t *testing.T, warnings admission.Warnings, err error) {
+				assert.Empty(t, warnings)
+				assert.NoError(t, err)
+			},
+		},
+		{
+			name: "namespace exists but without project label",
+			project: &kargoapi.Project{
+				ObjectMeta: metav1.ObjectMeta{
+					Name: testProjectName,
 				},
 			},
-			assertions: func(t *testing.T, err error) {
+			objects: []client.Object{testNsNoLabel},
+			assertions: func(t *testing.T, warnings admission.Warnings, err error) {
+				assert.Empty(t, warnings)
 				require.Error(t, err)
+
 				var statusErr *apierrors.StatusError
 				require.True(t, errors.As(err, &statusErr))
-				require.Equal(t, int32(http.StatusInternalServerError), statusErr.ErrStatus.Code)
+
+				assert.Equal(t, metav1.StatusReasonConflict, statusErr.ErrStatus.Reason)
+				assert.Contains(t, statusErr.ErrStatus.Message, "not labeled as a Project namespace")
 			},
 		},
-	}
-	for _, testCase := range testCases {
-		ctx := admission.NewContextWithRequest(
-			context.Background(),
-			admission.Request{
-				AdmissionRequest: admissionv1.AdmissionRequest{
-					DryRun: ptr.To(false),
+		{
+			name: "dry run request",
+			project: &kargoapi.Project{
+				ObjectMeta: metav1.ObjectMeta{
+					Name: testProjectName,
 				},
 			},
-		)
-		t.Run(testCase.name, func(t *testing.T) {
-			_, err := testCase.webhook.ValidateCreate(
-				ctx,
-				&kargoapi.Project{
-					ObjectMeta: metav1.ObjectMeta{
-						UID: "fake-uid",
-					},
+			isDryRun: true,
+			assertions: func(t *testing.T, warnings admission.Warnings, err error) {
+				assert.Empty(t, warnings)
+				assert.NoError(t, err)
+			},
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			c := fake.NewClientBuilder().
+				WithScheme(scheme).
+				WithObjects(tt.objects...).
+				Build()
+
+			w := &webhook{
+				client: c,
+				cfg: WebhookConfig{
+					KargoNamespace: "kargo-system",
+				},
+			}
+
+			ctx := admission.NewContextWithRequest(context.Background(), admission.Request{
+				AdmissionRequest: admissionv1.AdmissionRequest{
+					DryRun: &tt.isDryRun,
 				},
-			)
-			testCase.assertions(t, err)
+			})
+
+			warnings, err := w.ValidateCreate(ctx, tt.project)
+			tt.assertions(t, warnings, err)
 		})
 	}
 }
 
-func TestValidateSpec(t *testing.T) {
-	testCases := []struct {
+func Test_webhook_ValidateUpdate(t *testing.T) {
+	scheme := runtime.NewScheme()
+	require.NoError(t, kargoapi.AddToScheme(scheme))
+
+	testProjectName := "test-project"
+
+	tests := []struct {
 		name       string
-		spec       *kargoapi.ProjectSpec
-		assertions func(*testing.T, *kargoapi.ProjectSpec, field.ErrorList)
+		oldProject *kargoapi.Project
+		newProject *kargoapi.Project
+		assertions func(*testing.T, admission.Warnings, error)
 	}{
 		{
-			name: "nil",
-			assertions: func(t *testing.T, _ *kargoapi.ProjectSpec, errs field.ErrorList) {
-				require.Nil(t, errs)
+			name: "no spec changes",
+			oldProject: &kargoapi.Project{
+				ObjectMeta: metav1.ObjectMeta{
+					Name: testProjectName,
+				},
+			},
+			newProject: &kargoapi.Project{
+				ObjectMeta: metav1.ObjectMeta{
+					Name: testProjectName,
+				},
+			},
+			assertions: func(t *testing.T, warnings admission.Warnings, err error) {
+				assert.Empty(t, warnings)
+				assert.NoError(t, err)
 			},
 		},
 		{
-			name: "invalid",
-			spec: &kargoapi.ProjectSpec{
-				// Has two conflicting PromotionPolicies...
-				PromotionPolicies: []kargoapi.PromotionPolicy{
-					{Stage: "fake-stage"},
-					{Stage: "fake-stage"},
+			name: "no change to deprecated spec",
+			oldProject: &kargoapi.Project{
+				ObjectMeta: metav1.ObjectMeta{
+					Name: testProjectName,
+				},
+				Spec: &kargoapi.ProjectSpec{}, // nolint: staticcheck
+			},
+			newProject: &kargoapi.Project{
+				ObjectMeta: metav1.ObjectMeta{
+					Name: testProjectName,
 				},
+				Spec: &kargoapi.ProjectSpec{}, // nolint: staticcheck
 			},
-			assertions: func(t *testing.T, spec *kargoapi.ProjectSpec, errs field.ErrorList) {
-				require.Equal(
-					t,
-					field.ErrorList{
+			assertions: func(t *testing.T, warnings admission.Warnings, err error) {
+				assert.Empty(t, warnings)
+				assert.NoError(t, err)
+			},
+		},
+		{
+			name: "changes to deprecated spec",
+			oldProject: &kargoapi.Project{
+				ObjectMeta: metav1.ObjectMeta{
+					Name: testProjectName,
+				},
+				Spec: &kargoapi.ProjectSpec{ // nolint: staticcheck
+					PromotionPolicies: []kargoapi.PromotionPolicy{
 						{
-							Type:     field.ErrorTypeInvalid,
-							Field:    "spec.promotionPolicies",
-							BadValue: spec.PromotionPolicies,
-							Detail:   "multiple spec.promotionPolicies reference stage fake-stage",
+							Stage:                "test-stage",
+							AutoPromotionEnabled: false,
 						},
 					},
-					errs,
-				)
+				},
 			},
-		},
-		{
-			name: "valid",
-			spec: &kargoapi.ProjectSpec{
-				PromotionPolicies: []kargoapi.PromotionPolicy{
-					{Stage: "fake-stage"},
+			newProject: &kargoapi.Project{
+				ObjectMeta: metav1.ObjectMeta{
+					Name: testProjectName,
+				},
+				Spec: &kargoapi.ProjectSpec{ // nolint: staticcheck
+					PromotionPolicies: []kargoapi.PromotionPolicy{
+						{
+							Stage:                "test-stage",
+							AutoPromotionEnabled: true,
+						},
+					},
 				},
 			},
-			assertions: func(t *testing.T, _ *kargoapi.ProjectSpec, errs field.ErrorList) {
-				require.Nil(t, errs)
+			assertions: func(t *testing.T, warnings admission.Warnings, err error) {
+				assert.Empty(t, warnings)
+				require.Error(t, err)
+
+				var statusErr *apierrors.StatusError
+				require.True(t, errors.As(err, &statusErr))
+
+				assert.Equal(t, metav1.StatusReasonInvalid, statusErr.ErrStatus.Reason)
+				assert.Contains(t, statusErr.ErrStatus.Details.Causes[0].Message, "deprecated field")
+				assert.Equal(t, "spec", statusErr.ErrStatus.Details.Causes[0].Field)
 			},
 		},
 	}
-	w := &webhook{}
-	for _, testCase := range testCases {
-		t.Run(testCase.name, func(t *testing.T) {
-			testCase.assertions(
-				t,
-				testCase.spec,
-				w.validateSpec(field.NewPath("spec"), testCase.spec),
-			)
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			w := &webhook{}
+			warnings, err := w.ValidateUpdate(context.Background(), tt.oldProject, tt.newProject)
+			tt.assertions(t, warnings, err)
 		})
 	}
 }
 
-func TestEnsureNamespace(t *testing.T) {
-	testCases := []struct {
-		name       string
-		webhook    *webhook
-		assertions func(*testing.T, error)
-	}{
+func Test_webhook_ValidateDelete(t *testing.T) {
+	w := &webhook{}
 
-		{
-			name: "error getting namespace",
-			webhook: &webhook{
-				validateSpecFn: func(*field.Path, *kargoapi.ProjectSpec) field.ErrorList {
-					return nil
-				},
-				getNamespaceFn: func(
-					context.Context,
-					types.NamespacedName,
-					client.Object,
-					...client.GetOption,
-				) error {
-					return errors.New("something went wrong")
-				},
-			},
-			assertions: func(t *testing.T, err error) {
-				require.Error(t, err)
-				var statusErr *apierrors.StatusError
-				require.True(t, errors.As(err, &statusErr))
-				require.Equal(t, int32(http.StatusInternalServerError), statusErr.ErrStatus.Code)
+	project := &kargoapi.Project{
+		ObjectMeta: metav1.ObjectMeta{
+			Name: "test-project",
+		},
+	}
+
+	warnings, err := w.ValidateDelete(context.Background(), project)
+	assert.Empty(t, warnings)
+	assert.NoError(t, err)
+}
+
+func Test_webhook_ensureNamespace(t *testing.T) {
+	scheme := runtime.NewScheme()
+	require.NoError(t, corev1.AddToScheme(scheme))
+	require.NoError(t, kargoapi.AddToScheme(scheme))
+
+	testProjectName := "test-project"
+	testNs := &corev1.Namespace{
+		ObjectMeta: metav1.ObjectMeta{
+			Name: testProjectName,
+			Labels: map[string]string{
+				kargoapi.ProjectLabelKey: kargoapi.LabelTrueValue,
 			},
 		},
+	}
+	testNsNoLabel := &corev1.Namespace{
+		ObjectMeta: metav1.ObjectMeta{
+			Name: testProjectName,
+		},
+	}
 
+	tests := []struct {
+		name       string
+		project    *kargoapi.Project
+		objects    []client.Object
+		assertions func(*testing.T, error)
+	}{
 		{
-			name: "namespace exists, but isn't labeled as a project",
-			webhook: &webhook{
-				validateSpecFn: func(*field.Path, *kargoapi.ProjectSpec) field.ErrorList {
-					return nil
-				},
-				getNamespaceFn: func(
-					context.Context,
-					types.NamespacedName,
-					client.Object,
-					...client.GetOption,
-				) error {
-					return nil
+			name: "namespace does not exist, should create",
+			project: &kargoapi.Project{
+				ObjectMeta: metav1.ObjectMeta{
+					Name: testProjectName,
 				},
 			},
+			objects: []client.Object{}, // No namespace exists yet
 			assertions: func(t *testing.T, err error) {
-				require.Error(t, err)
-				var statusErr *apierrors.StatusError
-				require.True(t, errors.As(err, &statusErr))
-				require.Equal(t, int32(http.StatusConflict), statusErr.ErrStatus.Code)
+				assert.NoError(t, err)
 			},
 		},
-
 		{
-			name: "namespace exists and is labeled as a Project",
-			webhook: &webhook{
-				validateSpecFn: func(*field.Path, *kargoapi.ProjectSpec) field.ErrorList {
-					return nil
-				},
-				getNamespaceFn: func(
-					_ context.Context,
-					_ types.NamespacedName,
-					obj client.Object,
-					_ ...client.GetOption,
-				) error {
-					ns := obj.(*corev1.Namespace) // nolint: forcetypeassert
-					ns.Labels = map[string]string{
-						kargoapi.ProjectLabelKey: kargoapi.LabelTrueValue,
-					}
-					return nil
+			name: "namespace exists with project label, no conflict",
+			project: &kargoapi.Project{
+				ObjectMeta: metav1.ObjectMeta{
+					Name: testProjectName,
 				},
 			},
+			objects: []client.Object{testNs},
 			assertions: func(t *testing.T, err error) {
-				require.NoError(t, err)
+				assert.NoError(t, err)
 			},
 		},
-
 		{
-			name: "namespace does not exist; error creating it",
-			webhook: &webhook{
-				validateSpecFn: func(*field.Path, *kargoapi.ProjectSpec) field.ErrorList {
-					return nil
-				},
-				getNamespaceFn: func(
-					context.Context,
-					types.NamespacedName,
-					client.Object,
-					...client.GetOption,
-				) error {
-					return apierrors.NewNotFound(schema.GroupResource{}, "")
-				},
-				createNamespaceFn: func(
-					context.Context,
-					client.Object,
-					...client.CreateOption,
-				) error {
-					return errors.New("something went wrong")
+			name: "namespace exists without project label, conflict",
+			project: &kargoapi.Project{
+				ObjectMeta: metav1.ObjectMeta{
+					Name: testProjectName,
 				},
 			},
+			objects: []client.Object{testNsNoLabel},
 			assertions: func(t *testing.T, err error) {
 				require.Error(t, err)
 				var statusErr *apierrors.StatusError
 				require.True(t, errors.As(err, &statusErr))
-				require.Equal(
-					t,
-					int32(http.StatusInternalServerError),
-					statusErr.ErrStatus.Code,
-				)
+				assert.Equal(t, metav1.StatusReasonConflict, statusErr.ErrStatus.Reason)
+				assert.Contains(t, statusErr.ErrStatus.Message, "not labeled as a Project namespace")
 			},
 		},
+	}
 
-		{
-			name: "namespace does not exist; success creating it",
-			webhook: &webhook{
-				validateSpecFn: func(*field.Path, *kargoapi.ProjectSpec) field.ErrorList {
-					return nil
-				},
-				getNamespaceFn: func(
-					context.Context,
-					types.NamespacedName,
-					client.Object,
-					...client.GetOption,
-				) error {
-					return apierrors.NewNotFound(schema.GroupResource{}, "")
-				},
-				createNamespaceFn: func(
-					context.Context,
-					client.Object,
-					...client.CreateOption,
-				) error {
-					return nil
-				},
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			c := fake.NewClientBuilder().
+				WithScheme(scheme).
+				WithObjects(tt.objects...).
+				Build()
+
+			w := &webhook{
+				client: c,
+			}
+
+			err := w.ensureNamespace(context.Background(), tt.project)
+			tt.assertions(t, err)
+
+			// Check namespace creation in the first test case
+			if tt.name == "namespace does not exist, should create" {
+				ns := &corev1.Namespace{}
+				err := c.Get(context.Background(), client.ObjectKey{Name: testProjectName}, ns)
+				assert.NoError(t, err)
+				assert.Equal(t, kargoapi.LabelTrueValue, ns.Labels[kargoapi.ProjectLabelKey])
+				assert.Contains(t, ns.Finalizers, kargoapi.FinalizerName)
+			}
+		})
+	}
+}
+
+func Test_webhook_ensureProjectAdminPermissions(t *testing.T) {
+	scheme := runtime.NewScheme()
+	require.NoError(t, rbacv1.AddToScheme(scheme))
+	require.NoError(t, kargoapi.AddToScheme(scheme))
+
+	testProjectName := "test-project"
+	kargoNamespace := "kargo-system"
+
+	roleBindingName := "kargo-project-admin"
+	existingRoleBinding := &rbacv1.RoleBinding{
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      roleBindingName,
+			Namespace: testProjectName,
+		},
+		RoleRef: rbacv1.RoleRef{
+			APIGroup: rbacv1.GroupName,
+			Kind:     "ClusterRole",
+			Name:     "kargo-project-admin",
+		},
+		Subjects: []rbacv1.Subject{
+			{
+				Kind:      "ServiceAccount",
+				Name:      "kargo-api",
+				Namespace: kargoNamespace,
 			},
-			assertions: func(t *testing.T, err error) {
-				require.NoError(t, err)
+			{
+				Kind:      "ServiceAccount",
+				Name:      "kargo-admin",
+				Namespace: kargoNamespace,
 			},
 		},
 	}
-	for _, testCase := range testCases {
-		ctx := admission.NewContextWithRequest(
-			context.Background(),
-			admission.Request{
-				AdmissionRequest: admissionv1.AdmissionRequest{
-					DryRun: ptr.To(false),
-				},
-			},
-		)
-		t.Run(testCase.name, func(t *testing.T) {
-			testCase.assertions(
-				t,
-				testCase.webhook.ensureNamespace(
-					ctx,
-					&kargoapi.Project{
-						ObjectMeta: metav1.ObjectMeta{
-							UID: "fake-uid",
-						},
-					},
-				),
-			)
-		})
-	}
-}
 
-func TestEnsureProjectAdminPermissions(t *testing.T) {
-	testCases := []struct {
+	tests := []struct {
 		name       string
-		webhook    *webhook
-		assertions func(*testing.T, error)
+		project    *kargoapi.Project
+		objects    []client.Object
+		assertions func(*testing.T, error, client.Client)
 	}{
 		{
-			name: "error creating role binding",
-			webhook: &webhook{
-				createRoleBindingFn: func(
-					context.Context,
-					client.Object,
-					...client.CreateOption,
-				) error {
-					return errors.New("something went wrong")
+			name: "role binding does not exist, should create",
+			project: &kargoapi.Project{
+				ObjectMeta: metav1.ObjectMeta{
+					Name: testProjectName,
 				},
 			},
-			assertions: func(t *testing.T, err error) {
-				require.Error(t, err)
-				var statusErr *apierrors.StatusError
-				require.True(t, errors.As(err, &statusErr))
-				require.Equal(
-					t,
-					int32(http.StatusInternalServerError),
-					statusErr.ErrStatus.Code,
-				)
+			objects: []client.Object{},
+			assertions: func(t *testing.T, err error, c client.Client) {
+				assert.NoError(t, err)
+
+				// Check role binding creation
+				rb := &rbacv1.RoleBinding{}
+				err = c.Get(context.Background(), client.ObjectKey{
+					Name:      roleBindingName,
+					Namespace: testProjectName,
+				}, rb)
+				assert.NoError(t, err)
+				assert.Equal(t, "kargo-project-admin", rb.RoleRef.Name)
+				assert.Len(t, rb.Subjects, 2)
+				assert.Equal(t, "kargo-api", rb.Subjects[0].Name)
+				assert.Equal(t, "kargo-admin", rb.Subjects[1].Name)
+				assert.Equal(t, kargoNamespace, rb.Subjects[0].Namespace)
 			},
 		},
 		{
 			name: "role binding already exists",
-			webhook: &webhook{
-				createRoleBindingFn: func(
-					context.Context,
-					client.Object,
-					...client.CreateOption,
-				) error {
-					return apierrors.NewAlreadyExists(schema.GroupResource{}, "")
+			project: &kargoapi.Project{
+				ObjectMeta: metav1.ObjectMeta{
+					Name: testProjectName,
 				},
 			},
-			assertions: func(t *testing.T, err error) {
-				require.NoError(t, err)
-			},
-		},
-		{
-			name: "success creating role binding",
-			webhook: &webhook{
-				createRoleBindingFn: func(
-					context.Context,
-					client.Object,
-					...client.CreateOption,
-				) error {
-					return nil
-				},
-			},
-			assertions: func(t *testing.T, err error) {
-				require.NoError(t, err)
+			objects: []client.Object{existingRoleBinding},
+			assertions: func(t *testing.T, err error, _ client.Client) {
+				assert.NoError(t, err)
 			},
 		},
 	}
-	for _, testCase := range testCases {
-		ctx := admission.NewContextWithRequest(
-			context.Background(),
-			admission.Request{
-				AdmissionRequest: admissionv1.AdmissionRequest{
-					DryRun: ptr.To(false),
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			c := fake.NewClientBuilder().
+				WithScheme(scheme).
+				WithObjects(tt.objects...).
+				Build()
+
+			w := &webhook{
+				client: c,
+				cfg: WebhookConfig{
+					KargoNamespace: kargoNamespace,
 				},
-			},
-		)
-		t.Run(testCase.name, func(t *testing.T) {
-			testCase.assertions(
-				t,
-				testCase.webhook.ensureProjectAdminPermissions(
-					ctx,
-					&kargoapi.Project{
-						ObjectMeta: metav1.ObjectMeta{
-							UID: types.UID("fake-uid"),
-						},
-					},
-				),
-			)
+			}
+
+			err := w.ensureProjectAdminPermissions(context.Background(), tt.project)
+			tt.assertions(t, err, c)
 		})
 	}
 }
diff --git a/internal/webhook/projectconfig/webhook_test.go b/internal/webhook/projectconfig/webhook_test.go
new file mode 100644
index 00000000..efcb984d
--- /dev/null
+++ b/internal/webhook/projectconfig/webhook_test.go
@@ -0,0 +1,375 @@
+package projectconfig
+
+import (
+	"context"
+	"errors"
+	"fmt"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+	admissionv1 "k8s.io/api/admission/v1"
+	corev1 "k8s.io/api/core/v1"
+	apierrors "k8s.io/apimachinery/pkg/api/errors"
+	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"
+	"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
+
+	kargoapi "github.com/akuity/kargo/api/v1alpha1"
+)
+
+func Test_webhook_ValidateCreate(t *testing.T) {
+	scheme := runtime.NewScheme()
+	require.NoError(t, corev1.AddToScheme(scheme))
+	require.NoError(t, kargoapi.AddToScheme(scheme))
+
+	testProjectName := "test-project"
+	testNs := &corev1.Namespace{
+		ObjectMeta: metav1.ObjectMeta{
+			Name: testProjectName,
+			Labels: map[string]string{
+				kargoapi.ProjectLabelKey: kargoapi.LabelTrueValue,
+			},
+		},
+	}
+	testNsNoLabel := &corev1.Namespace{
+		ObjectMeta: metav1.ObjectMeta{
+			Name: testProjectName,
+		},
+	}
+	testNsWrongLabel := &corev1.Namespace{
+		ObjectMeta: metav1.ObjectMeta{
+			Name: testProjectName,
+			Labels: map[string]string{
+				kargoapi.ProjectLabelKey: "false",
+			},
+		},
+	}
+
+	tests := []struct {
+		name          string
+		projectConfig *kargoapi.ProjectConfig
+		objects       []client.Object
+		isDryRun      bool
+		assertions    func(*testing.T, admission.Warnings, error)
+	}{
+		{
+			name: "valid project config",
+			projectConfig: &kargoapi.ProjectConfig{
+				ObjectMeta: metav1.ObjectMeta{
+					Name:      testProjectName,
+					Namespace: testProjectName,
+				},
+				Spec: kargoapi.ProjectConfigSpec{
+					PromotionPolicies: []kargoapi.PromotionPolicy{
+						{Stage: "stage-1"},
+						{Stage: "stage-2"},
+					},
+				},
+			},
+			objects: []client.Object{testNs},
+			assertions: func(t *testing.T, warnings admission.Warnings, err error) {
+				assert.Empty(t, warnings)
+				assert.NoError(t, err)
+			},
+		},
+		{
+			name: "invalid metadata: name does not match namespace",
+			projectConfig: &kargoapi.ProjectConfig{
+				ObjectMeta: metav1.ObjectMeta{
+					Name:      "another-name",
+					Namespace: testProjectName,
+				},
+			},
+			objects: []client.Object{testNs}, // Namespace needs to exist for later checks
+			assertions: func(t *testing.T, warnings admission.Warnings, err error) {
+				assert.Empty(t, warnings)
+				require.Error(t, err)
+
+				var statusErr *apierrors.StatusError
+				require.True(t, errors.As(err, &statusErr))
+
+				assert.Contains(
+					t,
+					statusErr.ErrStatus.Details.Causes[0].Message,
+					`name "another-name" must match project name "test-project"`,
+				)
+				assert.Equal(t, "metadata.name", statusErr.ErrStatus.Details.Causes[0].Field)
+			},
+		},
+		{
+			name: "invalid spec: duplicate promotion policy stage",
+			projectConfig: &kargoapi.ProjectConfig{
+				ObjectMeta: metav1.ObjectMeta{
+					Name:      testProjectName,
+					Namespace: testProjectName,
+				},
+				Spec: kargoapi.ProjectConfigSpec{
+					PromotionPolicies: []kargoapi.PromotionPolicy{
+						{Stage: "stage-1"},
+						{Stage: "stage-1"}, // Duplicate
+					},
+				},
+			},
+			objects: []client.Object{testNs}, // Namespace needs to exist for later checks
+			assertions: func(t *testing.T, warnings admission.Warnings, err error) {
+				assert.Empty(t, warnings)
+				require.Error(t, err)
+
+				var statusErr *apierrors.StatusError
+				require.True(t, errors.As(err, &statusErr))
+
+				assert.Equal(t, metav1.StatusReasonInvalid, statusErr.ErrStatus.Reason)
+				assert.Contains(t,
+					statusErr.ErrStatus.Details.Causes[0].Message,
+					`multiple spec.promotionPolicies reference stage "stage-1"`,
+				)
+				assert.Equal(t, "spec.promotionPolicies", statusErr.ErrStatus.Details.Causes[0].Field)
+			},
+		},
+		{
+			name: "namespace does not exist",
+			projectConfig: &kargoapi.ProjectConfig{
+				ObjectMeta: metav1.ObjectMeta{
+					Name:      testProjectName,
+					Namespace: testProjectName,
+				},
+			},
+			// No existing namespace object
+			assertions: func(t *testing.T, warnings admission.Warnings, err error) {
+				assert.Empty(t, warnings)
+				require.Error(t, err)
+
+				var statusErr *apierrors.StatusError
+				require.True(t, errors.As(err, &statusErr))
+
+				assert.Equal(t, metav1.StatusReasonInternalError, statusErr.ErrStatus.Reason)
+				assert.Contains(t, statusErr.ErrStatus.Message, `error getting namespace "test-project"`)
+			},
+		},
+		{
+			name: "namespace exists but missing project label",
+			projectConfig: &kargoapi.ProjectConfig{
+				ObjectMeta: metav1.ObjectMeta{
+					Name:      testProjectName,
+					Namespace: testProjectName,
+				},
+			},
+			objects: []client.Object{testNsNoLabel},
+			assertions: func(t *testing.T, warnings admission.Warnings, err error) {
+				assert.Empty(t, warnings)
+				require.Error(t, err)
+
+				var statusErr *apierrors.StatusError
+				require.True(t, errors.As(err, &statusErr))
+
+				assert.Equal(t, metav1.StatusReasonForbidden, statusErr.ErrStatus.Reason)
+				assert.Contains(
+					t,
+					statusErr.ErrStatus.Message,
+					fmt.Sprintf(
+						`namespace %q does not belong to Kargo project (missing %q label)`,
+						testProjectName, kargoapi.ProjectLabelKey,
+					),
+				)
+			},
+		},
+		{
+			name: "namespace exists but wrong project label value",
+			projectConfig: &kargoapi.ProjectConfig{
+				ObjectMeta: metav1.ObjectMeta{
+					Name:      testProjectName,
+					Namespace: testProjectName,
+				},
+			},
+			objects: []client.Object{testNsWrongLabel},
+			assertions: func(t *testing.T, warnings admission.Warnings, err error) {
+				assert.Empty(t, warnings)
+				require.Error(t, err)
+
+				var statusErr *apierrors.StatusError
+				require.True(t, errors.As(err, &statusErr))
+
+				assert.Equal(t, metav1.StatusReasonForbidden, statusErr.ErrStatus.Reason)
+				assert.Contains(
+					t,
+					statusErr.ErrStatus.Message,
+					fmt.Sprintf(
+						`namespace %q does not belong to Kargo project (missing %q label)`,
+						testProjectName, kargoapi.ProjectLabelKey,
+					),
+				)
+			},
+		},
+		{
+			name: "valid project config during dry run (skips namespace check)",
+			projectConfig: &kargoapi.ProjectConfig{
+				ObjectMeta: metav1.ObjectMeta{
+					Name:      testProjectName,
+					Namespace: testProjectName,
+				},
+				Spec: kargoapi.ProjectConfigSpec{
+					PromotionPolicies: []kargoapi.PromotionPolicy{
+						{Stage: "stage-1"},
+						{Stage: "stage-2"},
+					},
+				},
+			},
+			// No namespace object provided, but should pass due to dry run
+			isDryRun: true,
+			assertions: func(t *testing.T, warnings admission.Warnings, err error) {
+				assert.Empty(t, warnings)
+				assert.NoError(t, err)
+			},
+		},
+		{
+			name: "invalid metadata during dry run (still checked)",
+			projectConfig: &kargoapi.ProjectConfig{
+				ObjectMeta: metav1.ObjectMeta{
+					Name:      "another-name",
+					Namespace: testProjectName,
+				},
+			},
+			isDryRun: true,
+			assertions: func(t *testing.T, warnings admission.Warnings, err error) {
+				assert.Empty(t, warnings)
+				require.Error(t, err)
+
+				var statusErr *apierrors.StatusError
+				require.True(t, errors.As(err, &statusErr))
+
+				assert.Equal(t, metav1.StatusReasonInvalid, statusErr.ErrStatus.Reason)
+				assert.Contains(
+					t,
+					statusErr.ErrStatus.Details.Causes[0].Message,
+					`name "another-name" must match project name "test-project"`,
+				)
+			},
+		},
+		{
+			name: "invalid spec during dry run (still checked)",
+			projectConfig: &kargoapi.ProjectConfig{
+				ObjectMeta: metav1.ObjectMeta{
+					Name:      testProjectName,
+					Namespace: testProjectName,
+				},
+				Spec: kargoapi.ProjectConfigSpec{
+					PromotionPolicies: []kargoapi.PromotionPolicy{
+						{Stage: "stage-1"},
+						{Stage: "stage-1"}, // Duplicate
+					},
+				},
+			},
+			isDryRun: true,
+			assertions: func(t *testing.T, warnings admission.Warnings, err error) {
+				assert.Empty(t, warnings)
+				require.Error(t, err)
+
+				var statusErr *apierrors.StatusError
+				require.True(t, errors.As(err, &statusErr))
+
+				assert.Equal(t, metav1.StatusReasonInvalid, statusErr.ErrStatus.Reason)
+				assert.Contains(
+					t,
+					statusErr.ErrStatus.Details.Causes[0].Message,
+					`multiple spec.promotionPolicies reference stage "stage-1"`,
+				)
+			},
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			c := fake.NewClientBuilder().
+				WithScheme(scheme).
+				WithObjects(tt.objects...).
+				Build()
+
+			w := &webhook{
+				client: c,
+			}
+
+			ctx := admission.NewContextWithRequest(context.Background(), admission.Request{
+				AdmissionRequest: admissionv1.AdmissionRequest{
+					DryRun: &tt.isDryRun,
+				},
+			})
+
+			warnings, err := w.ValidateCreate(ctx, tt.projectConfig)
+			tt.assertions(t, warnings, err)
+		})
+	}
+}
+
+func Test_webhook_ValidateUpdate(t *testing.T) {
+	scheme := runtime.NewScheme()
+	require.NoError(t, kargoapi.AddToScheme(scheme))
+
+	testProjectName := "test-project"
+
+	tests := []struct {
+		name       string
+		projectCfg *kargoapi.ProjectConfig
+		assertions func(*testing.T, admission.Warnings, error)
+	}{
+		{
+			name: "valid update",
+			projectCfg: &kargoapi.ProjectConfig{
+				ObjectMeta: metav1.ObjectMeta{
+					Name:      testProjectName,
+					Namespace: testProjectName,
+				},
+				Spec: kargoapi.ProjectConfigSpec{
+					PromotionPolicies: []kargoapi.PromotionPolicy{
+						{Stage: "stage-1"},
+						{Stage: "stage-2"}, // Added a stage
+					},
+				},
+			},
+			assertions: func(t *testing.T, warnings admission.Warnings, err error) {
+				assert.Empty(t, warnings)
+				assert.NoError(t, err)
+			},
+		},
+		{
+			name: "invalid spec: duplicate promotion policy stage",
+			projectCfg: &kargoapi.ProjectConfig{
+				ObjectMeta: metav1.ObjectMeta{
+					Name:      testProjectName,
+					Namespace: testProjectName,
+				},
+				Spec: kargoapi.ProjectConfigSpec{
+					PromotionPolicies: []kargoapi.PromotionPolicy{
+						{Stage: "stage-duplicate"},
+						{Stage: "stage-duplicate"}, // Duplicate
+					},
+				},
+			},
+			assertions: func(t *testing.T, warnings admission.Warnings, err error) {
+				assert.Empty(t, warnings)
+
+				require.Error(t, err)
+				var statusErr *apierrors.StatusError
+				require.True(t, errors.As(err, &statusErr))
+
+				assert.Equal(t, metav1.StatusReasonInvalid, statusErr.ErrStatus.Reason)
+				assert.Contains(
+					t,
+					statusErr.ErrStatus.Details.Causes[0].Message,
+					`multiple spec.promotionPolicies reference stage "stage-duplicate"`,
+				)
+				assert.Equal(t, "spec.promotionPolicies", statusErr.ErrStatus.Details.Causes[0].Field)
+			},
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			w := &webhook{}
+			warnings, err := w.ValidateUpdate(context.Background(), nil, tt.projectCfg)
+			tt.assertions(t, warnings, err)
+		})
+	}
+}