Repository: akuity/kargo
Pull request #1144 (2023-11-19 05:33:14)

chore(deps): bump library/node from 20.8.1 to 20.9.0
Bumps library/node from 20.8.1 to 20.9.0.

<details>
<summary>Most Recent Ignore Conditions Applied to This Pull Request</summary>

| Dependency Name | Ignore Conditions |
| --- | --- |
| library/node | [>= 21.a, < 22] |
</details>


[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=library/node&package-manager=docker&previous-version=20.8.1&new-version=20.9.0)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)


</details>

netlify[bot]: ### <span aria-hidden="true">👷</span> Deploy Preview for *docs-kargo-akuity-io* processing.


|  Name | Link |
|:-:|------------------------|
|<span aria-hidden="true">🔨</span> Latest commit | 91341c1189d7c7a55ae6924bb2b398b6ba912058 |
|<span aria-hidden="true">🔍</span> Latest deploy log | https://app.netlify.com/sites/docs-kargo-akuity-io/deploys/65599e1d1481720008fcf617 |
codecov[bot]: ## [Codecov](https://app.codecov.io/gh/akuity/kargo/pull/1144?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=akuity) Report
All modified and coverable lines are covered by tests :white_check_mark:
> Comparison is base [(`04421ae`)](https://app.codecov.io/gh/akuity/kargo/commit/04421ae8908a6716ee8a1c54894f1784b2d76157?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=akuity) 50.17% compared to head [(`91341c1`)](https://app.codecov.io/gh/akuity/kargo/pull/1144?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=akuity) 50.17%.


<details><summary>Additional details and impacted files</summary>


```diff
@@           Coverage Diff           @@
##             main    #1144   +/-   ##
=======================================
  Coverage   50.17%   50.17%           
=======================================
  Files         114      114           
  Lines        8068     8068           
=======================================
  Hits         4048     4048           
  Misses       3874     3874           
  Partials      146      146           
```



</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/akuity/kargo/pull/1144?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=akuity).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=akuity).

krancour: @rpelczar or @rbreeze if you guys are ok with this one, it also needs to be updated in our `.github` workflows.

Patch:
diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml
index 269a3743..ad514ac5 100644
--- a/.github/workflows/ci.yaml
+++ b/.github/workflows/ci.yaml
@@ -96,7 +96,7 @@ jobs:
     - name: Install nodejs
       uses: actions/setup-node@v4
       with:
-        node-version: "18.16.1"
+        node-version: "20.9.0"
         cache: "pnpm"
         cache-dependency-path: "**/pnpm-lock.yaml"
     - name: Install nodejs dependencies
diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml
index 08de2110..32e9398f 100644
--- a/.github/workflows/release.yaml
+++ b/.github/workflows/release.yaml
@@ -238,10 +238,9 @@ jobs:
         CF_DISTRIBUTION_ID: ${{ secrets.CF_DISTRIBUTION_ID }}
         VERSION: ${{ needs.publish-image.outputs.unstable-version }}
       run: |
-        aws s3 sync "./bin/kargo-cli/${VERSION}" "s3://kargo-release/kargo-cli/${VERSION}" --acl public-read
-
+        aws s3 sync "./bin/kargo-cli/${VERSION}" "s3://kargo-release/kargo-cli/${VERSION}"
         printf "${VERSION}" > ./bin/kargo-cli/unstable.txt
-        aws s3 cp ./bin/kargo-cli/unstable.txt s3://kargo-release/kargo-cli/unstable.txt --acl public-read
+        aws s3 cp ./bin/kargo-cli/unstable.txt s3://kargo-release/kargo-cli/unstable.txt
         aws cloudfront create-invalidation \
           --distribution-id="${CF_DISTRIBUTION_ID}" \
           --paths "/kargo-cli/unstable.txt"
diff --git a/Dockerfile b/Dockerfile
index 8320bf15..23533668 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -33,7 +33,7 @@ WORKDIR /kargo/bin
 ####################################################################################################
 # ui-builder
 ####################################################################################################
-FROM --platform=$BUILDPLATFORM docker.io/library/node:20.8.1 AS ui-builder
+FROM --platform=$BUILDPLATFORM docker.io/library/node:20.9.0 AS ui-builder
 
 RUN npm install --global pnpm
 WORKDIR /ui
diff --git a/Dockerfile.dev b/Dockerfile.dev
index fff5971e..98e60c72 100644
--- a/Dockerfile.dev
+++ b/Dockerfile.dev
@@ -6,7 +6,7 @@ ARG BUF_VERSION=v1.26.1
 ARG CONTROLLER_GEN_VERSION=v0.13.0
 ARG GOLANGCI_LINT_VERSION=1.54.2
 ARG HELM_VERSION=v3.12.3
-ARG NODE_MAJOR=18
+ARG NODE_MAJOR=20
 
 RUN go install github.com/bufbuild/buf/cmd/buf@${BUF_VERSION} \
     && go install sigs.k8s.io/controller-tools/cmd/controller-gen@${CONTROLLER_GEN_VERSION} \
diff --git a/api/v1alpha1/freight_helpers.go b/api/v1alpha1/freight_helpers.go
index 19cfb4b4..1c7628b5 100644
--- a/api/v1alpha1/freight_helpers.go
+++ b/api/v1alpha1/freight_helpers.go
@@ -31,22 +31,25 @@ func GetFreight(
 	return &freight, nil
 }
 
-// GetQualifiedFreight returns a pointer to the Freight resource specified by
-// the namespacedName argument if it is found and EITHER no Stages were
-// specified in the function call OR the Freight has qualified for ANY of the
-// specified Stages. If all other cases, nil is returned instead.
+// GetAvailableFreight returns a pointer to the Freight resource specified by
+// the namespacedName argument if it is found and:
+//
+//  1. No upstreamStages are specified
+//     OR
+//  2. The Freight has has been verified in ANY of the specified upstream stages
+//     OR
+//  3. The Freight is approved for the specified stage
 //
 // Note: The rationale for returning the found Freight (if any) instead of nil
-// when no Stages are specified is that the Stages provided are typically the
-// names of Stages UPSTREAM from some other Stage. i.e. The typical use for this
-// function is to answer whether a piece of Freight has qualified for any of a
-// given Stage's UPSTREAM Stages. Some Stages have no upstream Stages, so any
-// Freight that is found is implicitly qualified.
-func GetQualifiedFreight(
+// when no upstream stages are specified is that some Stages have no upstream
+// Stages (e.g. a Stage that subscribes to a Warehouse), so any Freight that is
+// found under those conditions is implicitly available.
+func GetAvailableFreight(
 	ctx context.Context,
 	c client.Client,
 	namespacedName types.NamespacedName,
-	stages []string,
+	upstreamStages []string,
+	stage string,
 ) (*Freight, error) {
 	freight, err := GetFreight(ctx, c, namespacedName)
 	if err != nil {
@@ -55,14 +58,17 @@ func GetQualifiedFreight(
 	if freight == nil {
 		return nil, nil
 	}
-	if len(stages) == 0 {
+	if len(upstreamStages) == 0 {
 		return freight, nil
 	}
-	for qualifiedStage := range freight.Status.Qualifications {
-		for _, stage := range stages {
-			if qualifiedStage == stage {
-				return freight, nil
-			}
+	for _, stage := range upstreamStages {
+		if _, ok := freight.Status.VerifiedIn[stage]; ok {
+			return freight, nil
+		}
+	}
+	if stage != "" {
+		if _, ok := freight.Status.ApprovedFor[stage]; ok {
+			return freight, nil
 		}
 	}
 	return nil, nil
diff --git a/api/v1alpha1/freight_types.go b/api/v1alpha1/freight_types.go
index 057fb6e9..19c08ead 100644
--- a/api/v1alpha1/freight_types.go
+++ b/api/v1alpha1/freight_types.go
@@ -89,13 +89,22 @@ type GitCommit struct {
 
 // FreightStatus describes a piece of Freight's most recently observed state.
 type FreightStatus struct {
-	// Qualifications describes the Stages for which this Freight has been
-	// qualified.
-	Qualifications map[string]Qualification `json:"qualifications,omitempty"`
+	// VerifiedIn describes the Stages in which this Freight has been verified
+	// through promotion and subsequent health checks.
+	VerifiedIn map[string]VerifiedStage `json:"verifiedIn,omitempty"`
+	// ApprovedFor describes the Stages for which this Freight has been approved
+	// preemptively/manually by a user. This is useful for hotfixes, where one
+	// might wish to promote a piece of Freight to a given Stage without
+	// transiting the entire pipeline.
+	ApprovedFor map[string]ApprovedStage `json:"approvedFor,omitempty"`
 }
 
-// Qualification describes a Freight's qualification for a Stage.
-type Qualification struct{}
+// VerifiedStage describes a Stage in which Freight has been verified.
+type VerifiedStage struct{}
+
+// ApprovedStage describes a Stage for which Freight has been (manually)
+// approved.
+type ApprovedStage struct{}
 
 //+kubebuilder:object:root=true
 
diff --git a/api/v1alpha1/types.proto b/api/v1alpha1/types.proto
index 54f81d6f..7b03d6ad 100644
--- a/api/v1alpha1/types.proto
+++ b/api/v1alpha1/types.proto
@@ -220,10 +220,14 @@ message Freight {
 }
 
 message FreightStatus {
-  map<string, Qualification> qualifications = 1;
+  map<string, VerifiedStage> verified_in = 1;
+  map<string, ApprovedStage> approved_for = 2;
 }
 
-message Qualification {
+message VerifiedStage {
+}
+
+message ApprovedStage {
 }
 
 message SimpleFreight {
diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go
index 9d7350b3..8a1a2762 100644
--- a/api/v1alpha1/zz_generated.deepcopy.go
+++ b/api/v1alpha1/zz_generated.deepcopy.go
@@ -8,6 +8,21 @@ import (
 	"k8s.io/apimachinery/pkg/runtime"
 )
 
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *ApprovedStage) DeepCopyInto(out *ApprovedStage) {
+	*out = *in
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ApprovedStage.
+func (in *ApprovedStage) DeepCopy() *ApprovedStage {
+	if in == nil {
+		return nil
+	}
+	out := new(ApprovedStage)
+	in.DeepCopyInto(out)
+	return out
+}
+
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *ArgoCDAppHealthStatus) DeepCopyInto(out *ArgoCDAppHealthStatus) {
 	*out = *in
@@ -268,9 +283,16 @@ func (in *FreightList) DeepCopyObject() runtime.Object {
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *FreightStatus) DeepCopyInto(out *FreightStatus) {
 	*out = *in
-	if in.Qualifications != nil {
-		in, out := &in.Qualifications, &out.Qualifications
-		*out = make(map[string]Qualification, len(*in))
+	if in.VerifiedIn != nil {
+		in, out := &in.VerifiedIn, &out.VerifiedIn
+		*out = make(map[string]VerifiedStage, len(*in))
+		for key, val := range *in {
+			(*out)[key] = val
+		}
+	}
+	if in.ApprovedFor != nil {
+		in, out := &in.ApprovedFor, &out.ApprovedFor
+		*out = make(map[string]ApprovedStage, len(*in))
 		for key, val := range *in {
 			(*out)[key] = val
 		}
@@ -709,21 +731,6 @@ func (in *PromotionStatus) DeepCopy() *PromotionStatus {
 	return out
 }
 
-// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
-func (in *Qualification) DeepCopyInto(out *Qualification) {
-	*out = *in
-}
-
-// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Qualification.
-func (in *Qualification) DeepCopy() *Qualification {
-	if in == nil {
-		return nil
-	}
-	out := new(Qualification)
-	in.DeepCopyInto(out)
-	return out
-}
-
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *RepoSubscription) DeepCopyInto(out *RepoSubscription) {
 	*out = *in
@@ -965,6 +972,21 @@ func (in *Subscriptions) DeepCopy() *Subscriptions {
 	return out
 }
 
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *VerifiedStage) DeepCopyInto(out *VerifiedStage) {
+	*out = *in
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new VerifiedStage.
+func (in *VerifiedStage) DeepCopy() *VerifiedStage {
+	if in == nil {
+		return nil
+	}
+	out := new(VerifiedStage)
+	in.DeepCopyInto(out)
+	return out
+}
+
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *Warehouse) DeepCopyInto(out *Warehouse) {
 	*out = *in
diff --git a/charts/kargo/crds/kargo.akuity.io_freights.yaml b/charts/kargo/crds/kargo.akuity.io_freights.yaml
index 6336aff9..79a9dd98 100644
--- a/charts/kargo/crds/kargo.akuity.io_freights.yaml
+++ b/charts/kargo/crds/kargo.akuity.io_freights.yaml
@@ -111,13 +111,23 @@ spec:
           status:
             description: Status describes the current status of this Freight.
             properties:
-              qualifications:
+              approvedFor:
                 additionalProperties:
-                  description: Qualification describes a Freight's qualification for
-                    a Stage.
+                  description: ApprovedStage describes a Stage for which Freight has
+                    been (manually) approved.
                   type: object
-                description: Qualifications describes the Stages for which this Freight
-                  has been qualified.
+                description: ApprovedFor describes the Stages for which this Freight
+                  has been approved preemptively/manually by a user. This is useful
+                  for hotfixes, where one might wish to promote a piece of Freight
+                  to a given Stage without transiting the entire pipeline.
+                type: object
+              verifiedIn:
+                additionalProperties:
+                  description: VerifiedStage describes a Stage in which Freight has
+                    been verified.
+                  type: object
+                description: VerifiedIn describes the Stages in which this Freight
+                  has been verified through promotion and subsequent health checks.
                 type: object
             type: object
         type: object
diff --git a/cmd/controlplane/api.go b/cmd/controlplane/api.go
index c8b770e0..80507fce 100644
--- a/cmd/controlplane/api.go
+++ b/cmd/controlplane/api.go
@@ -5,7 +5,7 @@ import (
 	"fmt"
 	"net"
 
-	pkgerrors "github.com/pkg/errors"
+	"github.com/pkg/errors"
 	log "github.com/sirupsen/logrus"
 	"github.com/spf13/cobra"
 	"k8s.io/apimachinery/pkg/runtime"
@@ -38,13 +38,13 @@ func newAPICommand() *cobra.Command {
 
 			restCfg, err := kubernetes.GetRestConfig(ctx, os.GetEnv("KUBECONFIG", ""))
 			if err != nil {
-				return pkgerrors.Wrap(err, "error loading REST config")
+				return errors.Wrap(err, "error loading REST config")
 			}
 			kubeClient, err := kubernetes.NewClient(ctx, restCfg, kubernetes.ClientOptions{
 				NewInternalClient: newClientForAPI,
 			})
 			if err != nil {
-				return pkgerrors.Wrap(err, "error creating Kubernetes client")
+				return errors.Wrap(err, "error creating Kubernetes client")
 			}
 
 			cfg := config.ServerConfigFromEnv()
@@ -70,11 +70,11 @@ func newAPICommand() *cobra.Command {
 				),
 			)
 			if err != nil {
-				return pkgerrors.Wrap(err, "error creating listener")
+				return errors.Wrap(err, "error creating listener")
 			}
 			defer l.Close()
 
-			return pkgerrors.Wrap(srv.Serve(ctx, l), "serve")
+			return errors.Wrap(srv.Serve(ctx, l), "serve")
 		},
 	}
 }
@@ -85,24 +85,37 @@ func newClientForAPI(ctx context.Context, r *rest.Config, scheme *runtime.Scheme
 		MetricsBindAddress: "0",
 	})
 	if err != nil {
-		return nil, pkgerrors.Wrap(err, "new manager")
+		return nil, errors.Wrap(err, "new manager")
 	}
+
 	// Index Promotions by Stage
 	if err := kubeclient.IndexPromotionsByStage(ctx, mgr); err != nil {
-		return nil, pkgerrors.Wrap(err, "index promotions by stage")
+		return nil, errors.Wrap(err, "index promotions by stage")
 	}
-	// Index Freights by Warehouse
+
+	// Index Freight by Warehouse
 	if err := kubeclient.IndexFreightByWarehouse(ctx, mgr); err != nil {
-		return nil, pkgerrors.Wrap(err, "index freight by warehouse")
+		return nil, errors.Wrap(err, "index freight by warehouse")
 	}
-	// Index Freights by Qualified Stages
-	if err := kubeclient.IndexFreightByQualifiedStages(ctx, mgr); err != nil {
-		return nil, pkgerrors.Wrap(err, "index freight by qualified stages")
+
+	// Index Freight by Stages in which it has been verified
+	if err := kubeclient.IndexFreightByVerifiedStages(ctx, mgr); err != nil {
+		return nil,
+			errors.Wrap(err, "index Freight by Stages in which it has been verified")
 	}
+
+	// Index Freight by Stages for which it is approved
+	if err :=
+		kubeclient.IndexFreightByApprovedStages(ctx, mgr); err != nil {
+		return nil,
+			errors.Wrap(err, "index Freight by Stages for which it has been approved")
+	}
+
 	go func() {
 		if err := mgr.Start(ctx); err != nil {
-			panic(pkgerrors.Wrap(err, "start manager"))
+			panic(errors.Wrap(err, "start manager"))
 		}
 	}()
+
 	return mgr.GetClient(), nil
 }
diff --git a/go.mod b/go.mod
index 0deaf509..bc9a6536 100644
--- a/go.mod
+++ b/go.mod
@@ -17,7 +17,6 @@ require (
 	github.com/bacongobbler/browser v1.1.0
 	github.com/coreos/go-oidc/v3 v3.6.0
 	github.com/evanphx/json-patch/v5 v5.6.0
-	github.com/ghodss/yaml v1.0.0
 	github.com/gobwas/glob v0.2.3
 	github.com/golang-jwt/jwt/v5 v5.0.0
 	github.com/google/uuid v1.3.1
@@ -49,7 +48,10 @@ require (
 	sigs.k8s.io/yaml v1.3.0
 )
 
-require github.com/withfig/autocomplete-tools/integrations/cobra v1.2.1
+require (
+	github.com/technosophos/moniker v0.0.0-20210218184952-3ea787d3943b
+	github.com/withfig/autocomplete-tools/integrations/cobra v1.2.1
+)
 
 require (
 	cloud.google.com/go/compute v1.21.0 // indirect
@@ -103,7 +105,7 @@ require (
 	github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
 	github.com/go-git/go-billy/v5 v5.4.1 // indirect
 	github.com/go-git/go-git/v5 v5.8.1 // indirect
-	github.com/go-jose/go-jose/v3 v3.0.0 // indirect
+	github.com/go-jose/go-jose/v3 v3.0.1 // indirect
 	github.com/go-logr/logr v1.2.4 // indirect
 	github.com/go-logr/stdr v1.2.2 // indirect
 	github.com/go-openapi/jsonpointer v0.19.5 // indirect
diff --git a/go.sum b/go.sum
index 4c4e4f6b..b2da842f 100644
--- a/go.sum
+++ b/go.sum
@@ -278,7 +278,6 @@ github.com/fvbommel/sortorder v1.0.1 h1:dSnXLt4mJYH25uDDGa3biZNQsozaUWDSWeKJ0qqF
 github.com/fvbommel/sortorder v1.0.1/go.mod h1:uk88iVf1ovNn1iLfgUVU2F9o5eO30ui720w+kxuqRs0=
 github.com/getkin/kin-openapi v0.76.0/go.mod h1:660oXbgy5JFMKreazJaQTw7o+X00qeSyhcnluiMv+Xg=
 github.com/getsentry/raven-go v0.2.0/go.mod h1:KungGk8q33+aIAZUIVWZDr2OfAEBsO49PX4NzFV5kcQ=
-github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk=
 github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
 github.com/gliderlabs/ssh v0.3.5 h1:OcaySEmAQJgyYcArR+gGGTHCyE7nvhEMTlYY+Dp8CpY=
 github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q=
@@ -294,8 +293,8 @@ github.com/go-git/go-git/v5 v5.8.1/go.mod h1:FHFuoD6yGz5OSKEBK+aWN9Oah0q54Jxl0ab
 github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
 github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
 github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
-github.com/go-jose/go-jose/v3 v3.0.0 h1:s6rrhirfEP/CGIoc6p+PZAeogN2SxKav6Wp7+dyMWVo=
-github.com/go-jose/go-jose/v3 v3.0.0/go.mod h1:RNkWWRld676jZEYoV3+XK8L2ZnNSvIsxFMht0mSX+u8=
+github.com/go-jose/go-jose/v3 v3.0.1 h1:pWmKFVtt+Jl0vBZTIpz/eAKwsm6LkIxDVVbFHKkchhA=
+github.com/go-jose/go-jose/v3 v3.0.1/go.mod h1:RNkWWRld676jZEYoV3+XK8L2ZnNSvIsxFMht0mSX+u8=
 github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
 github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
 github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY=
@@ -787,6 +786,8 @@ github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o
 github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
 github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
 github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
+github.com/technosophos/moniker v0.0.0-20210218184952-3ea787d3943b h1:fo0GUa0B+vxSZ8bgnL3fpCPHReM/QPlALdak9T/Zw5Y=
+github.com/technosophos/moniker v0.0.0-20210218184952-3ea787d3943b/go.mod h1:O1c8HleITsZqzNZDjSNzirUGsMT0oGu9LhHKoJrqO+A=
 github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
 github.com/tmc/grpc-websocket-proxy v0.0.0-20201229170055-e5319fda7802/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
 github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
diff --git a/internal/api/promote_stage_v1alpha1.go b/internal/api/promote_stage_v1alpha1.go
index 023be8b5..990e1d14 100644
--- a/internal/api/promote_stage_v1alpha1.go
+++ b/internal/api/promote_stage_v1alpha1.go
@@ -46,13 +46,13 @@ func (s *server) PromoteStage(
 		)
 	}
 
-	// Get the specified Freight. Expect a nil if it is either not found or is
-	// not qualified for any of the upstream Stages. Errors are internal problems.
+	// Get the specified Freight. Expect a nil if it is not found or not available
+	// to this Stage. Errors are indicative only of internal problems.
 	upstreamStages := make([]string, len(stage.Spec.Subscriptions.UpstreamStages))
 	for i, upstreamStage := range stage.Spec.Subscriptions.UpstreamStages {
 		upstreamStages[i] = upstreamStage.Name
 	}
-	if freight, err := s.getQualifiedFreightFn(
+	if freight, err := s.getAvailableFreightFn(
 		ctx,
 		s.client,
 		types.NamespacedName{
@@ -60,13 +60,14 @@ func (s *server) PromoteStage(
 			Name:      req.Msg.GetFreight(),
 		},
 		upstreamStages,
+		stage.Name,
 	); err != nil {
 		return nil, connect.NewError(connect.CodeInternal, err)
 	} else if freight == nil {
 		return nil, connect.NewError(
 			connect.CodeNotFound,
 			errors.Errorf(
-				"no qualified Freight %q found in namespace %q",
+				"no available Freight %q found in namespace %q",
 				req.Msg.GetFreight(),
 				req.Msg.GetProject(),
 			),
diff --git a/internal/api/promote_subscribers_v1alpha1.go b/internal/api/promote_subscribers_v1alpha1.go
index fa785e00..a14439dd 100644
--- a/internal/api/promote_subscribers_v1alpha1.go
+++ b/internal/api/promote_subscribers_v1alpha1.go
@@ -53,25 +53,31 @@ func (s *server) PromoteSubscribers(
 		)
 	}
 
-	// Get the specified Freight, but only if it has qualified for this Stage.
-	// Expect a nil if it is either not found or is not qualified . Errors are
-	// internal problems.
+	// Get the specified Freight, but only if it is verified in this Stage.
+	// Merely being approved FOR this Stage is not enough. If Freight is only
+	// approved FOR this Stage, that is because someone manually did that. This
+	// does not speak to its suitability for promotion downstream. If a user
+	// desires to promote Freight downstream that is not verified in this
+	// Stage, then they should approve the Freight for the downstream Stage(s).
+	// Expect a nil if the specified Freight is not found or doesn't meet these
+	// conditions. Errors are indicative only of internal problems.
 	var freight *kargoapi.Freight
-	if freight, err = s.getQualifiedFreightFn(
+	if freight, err = s.getAvailableFreightFn(
 		ctx,
 		s.client,
 		types.NamespacedName{
 			Namespace: req.Msg.GetProject(),
 			Name:      req.Msg.GetFreight(),
 		},
-		[]string{req.Msg.GetStage()},
+		[]string{req.Msg.GetStage()}, // verified in
+		"",                           // approved for not considered
 	); err != nil {
 		return nil, connect.NewError(connect.CodeInternal, err)
 	} else if freight == nil {
 		return nil, connect.NewError(
 			connect.CodeNotFound,
 			errors.Errorf(
-				"no qualified Freight %q found in namespace %q",
+				"no available Freight %q found in namespace %q",
 				req.Msg.GetFreight(),
 				req.Msg.GetProject(),
 			),
diff --git a/internal/api/query_freights_v1alpha1.go b/internal/api/query_freights_v1alpha1.go
index 021155f5..f96a1274 100644
--- a/internal/api/query_freights_v1alpha1.go
+++ b/internal/api/query_freights_v1alpha1.go
@@ -71,6 +71,7 @@ func (s *server) QueryFreight(
 		freight, err = s.getAvailableFreightForStageFn(
 			ctx,
 			req.Msg.GetProject(),
+			req.Msg.GetStage(),
 			*stage.Spec.Subscriptions,
 		)
 		if err != nil {
@@ -109,19 +110,77 @@ func (s *server) QueryFreight(
 	}), nil
 }
 
+// getAvailableFreightForStage gets all Freight available to the specified Stage
+// for any reason. This includes:
+//
+// 1. Any Freight from a Warehouse that the Stage subscribes to directly
+// 2. Any Freight that is verified in any upstream Stages
+// 3. Any Freight that is approved for the Stage
 func (s *server) getAvailableFreightForStage(
 	ctx context.Context,
 	project string,
+	stage string,
 	subs kargoapi.Subscriptions,
 ) ([]kargoapi.Freight, error) {
 	if subs.Warehouse != "" {
 		return s.getFreightFromWarehouseFn(ctx, project, subs.Warehouse)
 	}
-	return s.getFreightQualifiedForUpstreamStagesFn(
+	verifiedFreight, err := s.getVerifiedFreightFn(
 		ctx,
 		project,
 		subs.UpstreamStages,
 	)
+	if err != nil {
+		return nil, errors.Wrapf(
+			err,
+			"error listing Freight verified in Stages upstream from Stage %q in "+
+				"namespace %q",
+			stage,
+			project,
+		)
+	}
+	var approvedFreight kargoapi.FreightList
+	if err := s.listFreightFn(
+		ctx,
+		&approvedFreight,
+		&client.ListOptions{
+			Namespace: project,
+			FieldSelector: fields.OneTermEqualSelector(
+				kubeclient.FreightApprovedForStagesIndexField,
+				stage,
+			),
+		},
+	); err != nil {
+		return nil, errors.Wrapf(
+			err,
+			"error listing Freight approved for Stage %q in namespace %q",
+			stage,
+			project,
+		)
+	}
+	if len(verifiedFreight) == 0 &&
+		len(approvedFreight.Items) == 0 {
+		return nil, nil
+	}
+	// De-dupe
+	availableFreightMap := make(
+		map[string]kargoapi.Freight,
+		len(verifiedFreight)+len(approvedFreight.Items),
+	)
+	for _, freight := range verifiedFreight {
+		availableFreightMap[freight.Name] = freight
+	}
+	for _, freight := range approvedFreight.Items {
+		availableFreightMap[freight.Name] = freight
+	}
+	// Turn the map to a list
+	availableFreight := make([]kargoapi.Freight, len(availableFreightMap))
+	var i int
+	for _, freight := range availableFreightMap {
+		availableFreight[i] = freight
+		i++
+	}
+	return availableFreight, nil
 }
 
 func (s *server) getFreightFromWarehouse(
@@ -149,14 +208,14 @@ func (s *server) getFreightFromWarehouse(
 	)
 }
 
-func (s *server) getFreightQualifiedForUpstreamStages(
+func (s *server) getVerifiedFreight(
 	ctx context.Context,
 	project string,
 	stageSubs []kargoapi.StageSubscription,
 ) ([]kargoapi.Freight, error) {
-	// Start by building a de-duped map of Freight qualified for ANY upstream
-	// Stage
-	qualifiedFreight := map[string]kargoapi.Freight{}
+	// Start by building a de-duped map of Freight verified in any upstream
+	// Stage(s)
+	verifiedFreight := map[string]kargoapi.Freight{}
 	for _, stageSub := range stageSubs {
 		var freight kargoapi.FreightList
 		if err := s.listFreightFn(
@@ -165,33 +224,33 @@ func (s *server) getFreightQualifiedForUpstreamStages(
 			&client.ListOptions{
 				Namespace: project,
 				FieldSelector: fields.OneTermEqualSelector(
-					kubeclient.FreightByQualifiedStagesIndexField,
+					kubeclient.FreightByVerifiedStagesIndexField,
 					stageSub.Name,
 				),
 			},
 		); err != nil {
 			return nil, errors.Wrapf(
 				err,
-				"error listing Freight qualified for Stage %q in namespace %q",
+				"error listing Freight verified in Stage %q in namespace %q",
 				stageSub.Name,
 				project,
 			)
 		}
 		for _, freight := range freight.Items {
-			qualifiedFreight[freight.Name] = freight
+			verifiedFreight[freight.Name] = freight
 		}
 	}
-	if len(qualifiedFreight) == 0 {
+	if len(verifiedFreight) == 0 {
 		return nil, nil
 	}
 	// Turn the map to a list
-	qualifiedFreightList := make([]kargoapi.Freight, len(qualifiedFreight))
+	verifiedFreightList := make([]kargoapi.Freight, len(verifiedFreight))
 	i := 0
-	for _, freight := range qualifiedFreight {
-		qualifiedFreightList[i] = freight
+	for _, freight := range verifiedFreight {
+		verifiedFreightList[i] = freight
 		i++
 	}
-	return qualifiedFreightList, nil
+	return verifiedFreightList, nil
 }
 
 func groupByImageRepo(
diff --git a/internal/api/server.go b/internal/api/server.go
index 17b7c3d7..aed57f27 100644
--- a/internal/api/server.go
+++ b/internal/api/server.go
@@ -55,11 +55,12 @@ type server struct {
 		client.Client,
 		types.NamespacedName,
 	) (*kargoapi.Stage, error)
-	getQualifiedFreightFn func(
+	getAvailableFreightFn func(
 		ctx context.Context,
 		client client.Client,
 		namespacedName types.NamespacedName,
-		stages []string,
+		upstreamStages []string,
+		stage string,
 	) (*kargoapi.Freight, error)
 
 	// Common Promotions:
@@ -81,6 +82,7 @@ type server struct {
 	getAvailableFreightForStageFn func(
 		ctx context.Context,
 		project string,
+		stage string,
 		subs kargoapi.Subscriptions,
 	) ([]kargoapi.Freight, error)
 	getFreightFromWarehouseFn func(
@@ -88,7 +90,7 @@ type server struct {
 		project string,
 		warehouse string,
 	) ([]kargoapi.Freight, error)
-	getFreightQualifiedForUpstreamStagesFn func(
+	getVerifiedFreightFn func(
 		ctx context.Context,
 		project string,
 		stageSubs []kargoapi.StageSubscription,
@@ -110,18 +112,17 @@ func NewServer(
 		cfg:    cfg,
 		client: kubeClient,
 	}
-	// TODO: KR: Test that these all get set
 	s.validateProjectFn = s.validateProject
 	s.externalValidateProjectFn = validation.ValidateProject
 	s.getStageFn = kargoapi.GetStage
-	s.getQualifiedFreightFn = kargoapi.GetQualifiedFreight
+	s.getAvailableFreightFn = kargoapi.GetAvailableFreight
 	s.createPromotionFn = kubeClient.Create
 	s.findStageSubscribersFn = s.findStageSubscribers
 	s.listFreightFn = kubeClient.List
 	s.getAvailableFreightForStageFn = s.getAvailableFreightForStage
 	s.getFreightFromWarehouseFn = s.getFreightFromWarehouse
-	s.getFreightQualifiedForUpstreamStagesFn =
-		s.getFreightQualifiedForUpstreamStages
+	s.getVerifiedFreightFn =
+		s.getVerifiedFreight
 	s.parseManifestFn = manifest.NewParser(kubeClient.Scheme())
 	return s
 }
diff --git a/internal/api/types/v1alpha1/types.go b/internal/api/types/v1alpha1/types.go
index 6e3887a7..830c38b4 100644
--- a/internal/api/types/v1alpha1/types.go
+++ b/internal/api/types/v1alpha1/types.go
@@ -93,10 +93,15 @@ func FromFreightProto(f *v1alpha1.Freight) *kargoapi.Freight {
 	for idx, chart := range f.GetCharts() {
 		charts[idx] = *FromChartProto(chart)
 	}
-	qualifications :=
-		make(map[string]kargoapi.Qualification, len(f.Status.Qualifications))
-	for stageName := range f.Status.Qualifications {
-		qualifications[stageName] = kargoapi.Qualification{}
+	verifiedIn :=
+		make(map[string]kargoapi.VerifiedStage, len(f.Status.VerifiedIn))
+	for stage := range f.Status.VerifiedIn {
+		verifiedIn[stage] = kargoapi.VerifiedStage{}
+	}
+	approvedFor :=
+		make(map[string]kargoapi.ApprovedStage, len(f.Status.ApprovedFor))
+	for stage := range f.Status.ApprovedFor {
+		approvedFor[stage] = kargoapi.ApprovedStage{}
 	}
 	return &kargoapi.Freight{
 		TypeMeta: kubemetav1.TypeMeta{
@@ -109,7 +114,8 @@ func FromFreightProto(f *v1alpha1.Freight) *kargoapi.Freight {
 		Images:     images,
 		Charts:     charts,
 		Status: kargoapi.FreightStatus{
-			Qualifications: qualifications,
+			VerifiedIn:  verifiedIn,
+			ApprovedFor: approvedFor,
 		},
 	}
 }
@@ -845,10 +851,15 @@ func ToFreightProto(f kargoapi.Freight) *v1alpha1.Freight {
 	for idx := range f.Charts {
 		charts[idx] = ToChartProto(f.Charts[idx])
 	}
-	qualifications :=
-		make(map[string]*v1alpha1.Qualification, len(f.Status.Qualifications))
-	for stageName := range f.Status.Qualifications {
-		qualifications[stageName] = &v1alpha1.Qualification{}
+	verifiedIn :=
+		make(map[string]*v1alpha1.VerifiedStage, len(f.Status.VerifiedIn))
+	for stage := range f.Status.VerifiedIn {
+		verifiedIn[stage] = &v1alpha1.VerifiedStage{}
+	}
+	approvedFor :=
+		make(map[string]*v1alpha1.ApprovedStage, len(f.Status.ApprovedFor))
+	for stage := range f.Status.ApprovedFor {
+		approvedFor[stage] = &v1alpha1.ApprovedStage{}
 	}
 	return &v1alpha1.Freight{
 		ApiVersion: f.APIVersion,
@@ -859,7 +870,8 @@ func ToFreightProto(f kargoapi.Freight) *v1alpha1.Freight {
 		Commits:    commits,
 		Metadata:   typesmetav1.ToObjectMetaProto(*metadata),
 		Status: &v1alpha1.FreightStatus{
-			Qualifications: qualifications,
+			VerifiedIn:  verifiedIn,
+			ApprovedFor: approvedFor,
 		},
 	}
 }
diff --git a/internal/cli/config/config.go b/internal/cli/config/config.go
index 244ca243..9c5e1098 100644
--- a/internal/cli/config/config.go
+++ b/internal/cli/config/config.go
@@ -5,8 +5,8 @@ import (
 	"path/filepath"
 
 	"github.com/adrg/xdg"
-	"github.com/ghodss/yaml"
 	"github.com/pkg/errors"
+	"sigs.k8s.io/yaml"
 )
 
 var xdgConfigPath string
diff --git a/internal/cli/get/freight.go b/internal/cli/get/freight.go
index ba488f4b..aabb61de 100644
--- a/internal/cli/get/freight.go
+++ b/internal/cli/get/freight.go
@@ -2,11 +2,14 @@ package get
 
 import (
 	goerrors "errors"
+	"time"
 
 	"connectrpc.com/connect"
 	"github.com/pkg/errors"
 	"github.com/spf13/cobra"
 	"golang.org/x/exp/slices"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/util/duration"
 
 	kargoapi "github.com/akuity/kargo/api/v1alpha1"
 	typesv1alpha1 "github.com/akuity/kargo/internal/api/types/v1alpha1"
@@ -15,6 +18,8 @@ import (
 	v1alpha1 "github.com/akuity/kargo/pkg/api/service/v1alpha1"
 )
 
+const aliasLabelKey = "kargo.akuity.com/alias"
+
 func newGetFreightCommand(opt *option.Option) *cobra.Command {
 	cmd := &cobra.Command{
 		Use:   "freight --project=project [NAME...]",
@@ -60,14 +65,28 @@ kargo get freight --project=my-project my-freight
 				}
 			} else {
 				freightByName := make(map[string]*kargoapi.Freight, len(freight.Freight))
+				freightByAlias := make(map[string]*kargoapi.Freight, len(freight.Freight))
 				for _, f := range freight.Freight {
-					freightByName[f.GetMetadata().GetName()] = typesv1alpha1.FromFreightProto(f)
+					fr := typesv1alpha1.FromFreightProto(f)
+					freightByName[f.GetMetadata().GetName()] = fr
+					if f.GetMetadata().GetLabels() != nil {
+						freightByAlias[f.GetMetadata().GetLabels()[aliasLabelKey]] = fr
+					}
 				}
+				selectedFreight := make(map[string]struct{}, len(names))
 				for _, name := range names {
-					if f, ok := freightByName[name]; ok {
-						res = append(res, f)
+					f, ok := freightByName[name]
+					if !ok {
+						f, ok = freightByAlias[name]
+					}
+					if ok {
+						if _, selected := selectedFreight[f.Name]; !selected {
+							res = append(res, f)
+							selectedFreight[f.Name] = struct{}{}
+						}
 					} else {
-						resErr = goerrors.Join(err, errors.Errorf("freight %q not found", name))
+						resErr =
+							goerrors.Join(err, errors.Errorf("freight %q not found", name))
 					}
 				}
 			}
@@ -81,3 +100,30 @@ kargo get freight --project=my-project my-freight
 	opt.PrintFlags.AddFlags(cmd)
 	return cmd
 }
+
+func newFreightTable(list *metav1.List) *metav1.Table {
+	rows := make([]metav1.TableRow, len(list.Items))
+	for i, item := range list.Items {
+		freight := item.Object.(*kargoapi.Freight) // nolint: forcetypeassert
+		var alias string
+		if freight.Labels != nil {
+			alias = freight.Labels[aliasLabelKey]
+		}
+		rows[i] = metav1.TableRow{
+			Cells: []any{
+				freight.Name,
+				alias,
+				duration.HumanDuration(time.Since(freight.CreationTimestamp.Time)),
+			},
+			Object: list.Items[i],
+		}
+	}
+	return &metav1.Table{
+		ColumnDefinitions: []metav1.TableColumnDefinition{
+			{Name: "Name/ID", Type: "string"},
+			{Name: "Alias", Type: "string"},
+			{Name: "Age", Type: "string"},
+		},
+		Rows: rows,
+	}
+}
diff --git a/internal/cli/get/get.go b/internal/cli/get/get.go
index 460ebc7a..fe65b85f 100644
--- a/internal/cli/get/get.go
+++ b/internal/cli/get/get.go
@@ -62,12 +62,15 @@ func printObjects[T runtime.Object](opt *option.Option, objects []T) error {
 
 	var t T
 	switch any(t).(type) {
-	case *kargoapi.Stage:
-		table := newStageTable(list)
+	case *kargoapi.Freight:
+		table := newFreightTable(list)
 		return printers.NewTablePrinter(printers.PrintOptions{}).PrintObj(table, opt.IOStreams.Out)
 	case *kargoapi.Promotion:
 		table := newPromotionTable(list)
 		return printers.NewTablePrinter(printers.PrintOptions{}).PrintObj(table, opt.IOStreams.Out)
+	case *kargoapi.Stage:
+		table := newStageTable(list)
+		return printers.NewTablePrinter(printers.PrintOptions{}).PrintObj(table, opt.IOStreams.Out)
 	default:
 		return printers.NewTablePrinter(printers.PrintOptions{}).PrintObj(list, opt.IOStreams.Out)
 	}
diff --git a/internal/controller/promotions/promotions.go b/internal/controller/promotions/promotions.go
index beb50662..ecc44185 100644
--- a/internal/controller/promotions/promotions.go
+++ b/internal/controller/promotions/promotions.go
@@ -282,7 +282,9 @@ func (r *reconciler) promote(
 	for i, upstreamStage := range stage.Spec.Subscriptions.UpstreamStages {
 		upstreamStages[i] = upstreamStage.Name
 	}
-	targetFreight, err := kargoapi.GetQualifiedFreight(
+	// Get the specified Freight. Expect a nil if it is not found or not available
+	// to this Stage. Errors are indicative only of internal problems.
+	targetFreight, err := kargoapi.GetAvailableFreight(
 		ctx,
 		r.kargoClient,
 		types.NamespacedName{
@@ -290,13 +292,14 @@ func (r *reconciler) promote(
 			Name:      promo.Spec.Freight,
 		},
 		upstreamStages,
+		stageName,
 	)
 	if err != nil {
 		return err
 	}
 	if targetFreight == nil {
 		return errors.Errorf(
-			"no qualified Freight %q found in namespace %q",
+			"no available Freight %q found in namespace %q",
 			promo.Spec.Freight,
 			promo.Namespace,
 		)
diff --git a/internal/controller/stages/stages.go b/internal/controller/stages/stages.go
index bbd0cbb4..40253b8d 100644
--- a/internal/controller/stages/stages.go
+++ b/internal/controller/stages/stages.go
@@ -61,7 +61,7 @@ type reconciler struct {
 		name string,
 	) (*argocd.Application, error)
 
-	// Freight qualification:
+	// Freight verification:
 
 	getFreightFn func(
 		context.Context,
@@ -69,7 +69,7 @@ type reconciler struct {
 		types.NamespacedName,
 	) (*kargoapi.Freight, error)
 
-	qualifyFreightFn func(
+	verifyFreightInStageFn func(
 		ctx context.Context,
 		namespace string,
 		freightName string,
@@ -107,33 +107,33 @@ type reconciler struct {
 	getLatestAvailableFreightFn func(
 		ctx context.Context,
 		namespace string,
-		subs kargoapi.Subscriptions,
+		stage *kargoapi.Stage,
 	) (*kargoapi.Freight, error)
 
-	getAllFreightFromWarehouseFn func(
-		ctx context.Context,
-		namespace string,
-		warehouse string,
-	) ([]kargoapi.Freight, error)
-
 	getLatestFreightFromWarehouseFn func(
 		ctx context.Context,
 		namespace string,
 		warehouse string,
 	) (*kargoapi.Freight, error)
 
-	getAllFreightQualifiedForUpstreamStagesFn func(
+	getAllVerifiedFreightFn func(
 		ctx context.Context,
 		namespace string,
 		stageSubs []kargoapi.StageSubscription,
 	) ([]kargoapi.Freight, error)
 
-	getLatestFreightQualifiedForUpstreamStagesFn func(
+	getLatestVerifiedFreightFn func(
 		ctx context.Context,
 		namespace string,
 		stageSubs []kargoapi.StageSubscription,
 	) (*kargoapi.Freight, error)
 
+	getLatestApprovedFreightFn func(
+		ctx context.Context,
+		namespace string,
+		name string,
+	) (*kargoapi.Freight, error)
+
 	listFreightFn func(
 		context.Context,
 		client.ObjectList,
@@ -169,10 +169,22 @@ func SetupReconcilerWithManager(
 		return errors.Wrap(err, "index Freight by Warehouse")
 	}
 
-	// Index Freight by qualified Stages
+	// Index Freight by Stages in which it has been verified
 	if err :=
-		kubeclient.IndexFreightByQualifiedStages(ctx, kargoMgr); err != nil {
-		return errors.Wrap(err, "index Freight by qualified Stages")
+		kubeclient.IndexFreightByVerifiedStages(ctx, kargoMgr); err != nil {
+		return errors.Wrap(
+			err,
+			"index Freight by Stages in which it has been verified",
+		)
+	}
+
+	// Index Freight by Stages for which it has been approved
+	if err :=
+		kubeclient.IndexFreightByApprovedStages(ctx, kargoMgr); err != nil {
+		return errors.Wrap(
+			err,
+			"index Freight by Stages for which it has been approved",
+		)
 	}
 
 	// Index Stages by upstream Stages
@@ -181,6 +193,11 @@ func SetupReconcilerWithManager(
 		return errors.Wrap(err, "index Stages by upstream Stages")
 	}
 
+	// Index Stages by Warehouse
+	if err := kubeclient.IndexStagesByWarehouse(ctx, kargoMgr); err != nil {
+		return errors.Wrap(err, "index Stages by Warehouse")
+	}
+
 	shardPredicate, err := controller.GetShardPredicate(shardName)
 	if err != nil {
 		return errors.Wrap(err, "error creating shard predicate")
@@ -217,14 +234,32 @@ func SetupReconcilerWithManager(
 		return errors.Wrap(err, "unable to watch Promotions")
 	}
 
-	// Watch Freight that qualified for a Stage and enqueue downstream Stages
-	downstreamEvtHandler := &EnqueueDownstreamStagesHandler{
+	// Watch Freight that has been marked as verified in a Stage and enqueue
+	// downstream Stages
+	verifiedFreightHandler := &verifiedFreightEventHandler{
+		kargoClient: kargoMgr.GetClient(),
+		logger:      logger,
+	}
+	if err := c.Watch(&source.Kind{Type: &kargoapi.Freight{}}, verifiedFreightHandler); err != nil {
+		return errors.Wrap(err, "unable to watch Freight")
+	}
+
+	approveFreightHandler := &approvedFreightEventHandler{
+		kargoClient: kargoMgr.GetClient(),
+		logger:      logger,
+	}
+	if err := c.Watch(&source.Kind{Type: &kargoapi.Freight{}}, approveFreightHandler); err != nil {
+		return errors.Wrap(err, "unable to watch Freight")
+	}
+
+	createdFreightEventHandler := &createdFreightEventHandler{
 		kargoClient: kargoMgr.GetClient(),
 		logger:      logger,
 	}
-	if err := c.Watch(&source.Kind{Type: &kargoapi.Freight{}}, downstreamEvtHandler); err != nil {
+	if err := c.Watch(&source.Kind{Type: &kargoapi.Freight{}}, createdFreightEventHandler); err != nil {
 		return errors.Wrap(err, "unable to watch Freight")
 	}
+
 	return nil
 }
 
@@ -240,9 +275,9 @@ func newReconciler(kargoClient, argoClient client.Client) *reconciler {
 	// Health checks:
 	r.checkHealthFn = r.checkHealth
 	r.getArgoCDAppFn = argocd.GetApplication
-	// Freight qualification:
+	// Freight verification:
 	r.getFreightFn = kargoapi.GetFreight
-	r.qualifyFreightFn = r.qualifyFreight
+	r.verifyFreightInStageFn = r.verifyFreightInStage
 	r.patchFreightStatusFn = r.patchFreightStatus
 	// Auto-promotion:
 	r.isAutoPromotionPermittedFn = r.isAutoPromotionPermitted
@@ -250,10 +285,10 @@ func newReconciler(kargoClient, argoClient client.Client) *reconciler {
 	r.createPromotionFn = kargoClient.Create
 	// Discovering latest Freight:
 	r.getLatestAvailableFreightFn = r.getLatestAvailableFreight
-	r.getAllFreightFromWarehouseFn = r.getAllFreightFromWarehouse
 	r.getLatestFreightFromWarehouseFn = r.getLatestFreightFromWarehouse
-	r.getAllFreightQualifiedForUpstreamStagesFn = r.getAllFreightQualifiedForUpstreamStages
-	r.getLatestFreightQualifiedForUpstreamStagesFn = r.getLatestFreightQualifiedForUpstreamStages
+	r.getAllVerifiedFreightFn = r.getAllVerifiedFreight
+	r.getLatestVerifiedFreightFn = r.getLatestVerifiedFreight
+	r.getLatestApprovedFreightFn = r.getLatestApprovedFreight
 	r.listFreightFn = r.kargoClient.List
 	return r
 }
@@ -349,34 +384,50 @@ func (r *reconciler) syncControlFlowStage(
 	// were removed, thus becoming a control flow Stage.
 	status.CurrentFreight = nil
 
-	// For now all available Freight (qualified upstream) should automatically and
-	// immediately be qualified for this Stage, making it available downstream. In
-	// the future, we may have more options before qualifying them (e.g. require
-	// that they were qualified in all our upstreams)
+	// For now all Freight verified in any upstream Stage(s) should automatically
+	// and immediately be verified in this Stage, making it available downstream.
+	// In the future, we may have more options before marking Freight as verified
+	// in a control flow Stage (e.g. require that it was verified in ALL upstreams
+	// Stages)
 	var availableFreight []kargoapi.Freight
-	var err error
 	if stage.Spec.Subscriptions.Warehouse != "" {
-		if availableFreight, err = r.getAllFreightFromWarehouseFn(
+		var freight kargoapi.FreightList
+		if err := r.listFreightFn(
 			ctx,
-			stage.Namespace,
-			stage.Spec.Subscriptions.Warehouse,
+			&freight,
+			&client.ListOptions{
+				Namespace: stage.Namespace,
+				FieldSelector: fields.OneTermEqualSelector(
+					kubeclient.FreightByWarehouseIndexField,
+					stage.Spec.Subscriptions.Warehouse,
+				),
+			},
 		); err != nil {
 			return status, errors.Wrapf(
 				err,
-				"error finding all Freight from Warehouse %q in namespace %q",
+				"error listing Freight from Warehouse %q in namespace %q",
 				stage.Spec.Subscriptions.Warehouse,
 				stage.Namespace,
 			)
 		}
+		availableFreight = freight.Items
 	} else {
-		if availableFreight, err = r.getAllFreightQualifiedForUpstreamStagesFn(
+		// Get all Freight verified in upstream Stages. Merely being approved for an
+		// upstream Stage is not enough. If Freight is only approved for a Stage,
+		// that is because someone manually did that. This does not speak to its
+		// suitability for promotion downstream. Expect a nil if the specified
+		// Freight is not found or doesn't meet these conditions. Errors are
+		// indicative only of internal problems.
+		var err error
+		if availableFreight, err = r.getAllVerifiedFreightFn(
 			ctx,
 			stage.Namespace,
 			stage.Spec.Subscriptions.UpstreamStages,
 		); err != nil {
 			return status, errors.Wrapf(
 				err,
-				"error finding available Freight for Stage %q in namespace %q",
+				"error getting all Freight verified in Stages upstream from Stage "+
+					"%q in namespace %q",
 				stage.Name,
 				stage.Namespace,
 			)
@@ -384,17 +435,17 @@ func (r *reconciler) syncControlFlowStage(
 	}
 	for _, available := range availableFreight {
 		af := available // Avoid implicit memory aliasing
-		// Only bother to qualify if not already qualified
-		if _, qualified := af.Status.Qualifications[stage.Name]; !qualified {
+		// Only bother to mark as verified in this Stage if not already the case.
+		if _, verified := af.Status.VerifiedIn[stage.Name]; !verified {
 			newStatus := *af.Status.DeepCopy()
-			if newStatus.Qualifications == nil {
-				newStatus.Qualifications = map[string]kargoapi.Qualification{}
+			if newStatus.VerifiedIn == nil {
+				newStatus.VerifiedIn = map[string]kargoapi.VerifiedStage{}
 			}
-			newStatus.Qualifications[stage.Name] = kargoapi.Qualification{}
-			if err = r.patchFreightStatusFn(ctx, &af, newStatus); err != nil {
+			newStatus.VerifiedIn[stage.Name] = kargoapi.VerifiedStage{}
+			if err := r.patchFreightStatusFn(ctx, &af, newStatus); err != nil {
 				return status, errors.Wrapf(
 					err,
-					"error qualifying Freight %q in namespace %q for Stage %q",
+					"error marking Freight %q in namespace %q as verified in Stage %q",
 					af.ID,
 					stage.Namespace,
 					stage.Name,
@@ -437,7 +488,8 @@ func (r *reconciler) syncNormalStage(
 
 	if status.CurrentFreight == nil {
 		logger.Debug("Stage has no current Freight; no health checks to perform")
-	} else { //  Check health and qualify current Freight if applicable
+	} else {
+		//  Check health and mark current Freight as verified in Stage if applicable
 		freightLogger := logger.WithField("freight", status.CurrentFreight.ID)
 
 		// Check health
@@ -453,10 +505,10 @@ func (r *reconciler) syncNormalStage(
 			freightLogger.Debug("Stage health deemed not applicable")
 		}
 
-		// If health is not applicable or healthy, qualify the current Freight for
-		// this Stage
+		// If health is not applicable or healthy, mark the current Freight as
+		// verified in this Stage
 		if status.Health == nil || status.Health.Status == kargoapi.HealthStateHealthy {
-			if err := r.qualifyFreightFn(
+			if err := r.verifyFreightInStageFn(
 				ctx,
 				stage.Namespace,
 				status.CurrentFreight.ID,
@@ -464,7 +516,7 @@ func (r *reconciler) syncNormalStage(
 			); err != nil {
 				return status, errors.Wrapf(
 					err,
-					"error qualifying Freight %q in namespace %q for Stage %q",
+					"error marking Freight %q in namespace %q as verified in Stage %q",
 					status.CurrentFreight.ID,
 					stage.Namespace,
 					stage.Name,
@@ -505,7 +557,7 @@ func (r *reconciler) syncNormalStage(
 	// and permitted. Time to go looking for new Freight...
 
 	latestFreight, err :=
-		r.getLatestAvailableFreightFn(ctx, stage.Namespace, *stage.Spec.Subscriptions)
+		r.getLatestAvailableFreightFn(ctx, stage.Namespace, stage)
 	if err != nil {
 		return status, errors.Wrapf(
 			err,
@@ -525,7 +577,7 @@ func (r *reconciler) syncNormalStage(
 	// Only proceed if nextFreight isn't the one we already have
 	if stage.Status.CurrentFreight != nil &&
 		stage.Status.CurrentFreight.ID == latestFreight.Name {
-		logger.Debug("Stage already has latest qualified Freight")
+		logger.Debug("Stage already has latest available Freight")
 		return status, nil
 	}
 
@@ -603,7 +655,7 @@ func (r *reconciler) hasNonTerminalPromotions(
 	return len(promos.Items) > 0, nil
 }
 
-func (r *reconciler) qualifyFreight(
+func (r *reconciler) verifyFreightInStage(
 	ctx context.Context,
 	namespace string,
 	freightName string,
@@ -623,40 +675,36 @@ func (r *reconciler) qualifyFreight(
 	if err != nil {
 		return errors.Wrapf(
 			err,
-			"error finding Freight %q in namespace %q; could not qualify it for "+
-				"Stage %q",
+			"error finding Freight %q in namespace %q",
 			freightName,
 			namespace,
-			stageName,
 		)
 	}
 	if freight == nil {
 		return errors.Errorf(
-			"found no Freight %q in namespace %q; could not qualify it for "+
-				"Stage %q",
+			"found no Freight %q in namespace %q",
 			freightName,
 			namespace,
-			stageName,
 		)
 	}
 
 	newStatus := *freight.Status.DeepCopy()
-	if newStatus.Qualifications == nil {
-		newStatus.Qualifications = map[string]kargoapi.Qualification{}
+	if newStatus.VerifiedIn == nil {
+		newStatus.VerifiedIn = map[string]kargoapi.VerifiedStage{}
 	}
 
-	// Only try to qualify if not already qualified
-	if _, ok := newStatus.Qualifications[stageName]; ok {
-		logger.Debug("Freight already qualified for Stage")
+	// Only try to mark as verified in this Stage if not already the case.
+	if _, ok := newStatus.VerifiedIn[stageName]; ok {
+		logger.Debug("Freight already marked as verified in Stage")
 		return nil
 	}
 
-	newStatus.Qualifications[stageName] = kargoapi.Qualification{}
+	newStatus.VerifiedIn[stageName] = kargoapi.VerifiedStage{}
 	if err = r.patchFreightStatusFn(ctx, freight, newStatus); err != nil {
 		return err
 	}
 
-	logger.Debug("qualified Freight for Stage")
+	logger.Debug("marked Freight as verified in Stage")
 	return nil
 }
 
@@ -725,57 +773,87 @@ func (r *reconciler) isAutoPromotionPermitted(
 func (r *reconciler) getLatestAvailableFreight(
 	ctx context.Context,
 	namespace string,
-	subs kargoapi.Subscriptions,
+	stage *kargoapi.Stage,
 ) (*kargoapi.Freight, error) {
 	logger := logging.LoggerFromContext(ctx)
 
-	if subs.Warehouse != "" {
+	if stage.Spec.Subscriptions.Warehouse != "" {
 		latestFreight, err := r.getLatestFreightFromWarehouseFn(
 			ctx,
 			namespace,
-			subs.Warehouse,
+			stage.Spec.Subscriptions.Warehouse,
 		)
 		if err != nil {
 			return nil, errors.Wrapf(
 				err,
 				"error checking Warehouse %q in namespace %q for Freight",
-				subs.Warehouse,
+				stage.Spec.Subscriptions.Warehouse,
 				namespace,
 			)
 		}
 		if latestFreight == nil {
-			logger.WithField("warehouse", subs.Warehouse).
+			logger.WithField("warehouse", stage.Spec.Subscriptions.Warehouse).
 				Debug("no Freight found from Warehouse")
 		}
 		return latestFreight, nil
 	}
 
-	latestFreight, err := r.getLatestFreightQualifiedForUpstreamStagesFn(
+	latestVerifiedFreight, err := r.getLatestVerifiedFreightFn(
 		ctx,
 		namespace,
-		subs.UpstreamStages,
+		stage.Spec.Subscriptions.UpstreamStages,
 	)
 	if err != nil {
 		return nil, errors.Wrapf(
 			err,
-			"error finding Freight qualified for Stages upstream from "+
+			"error finding latest Freight verified in Stages upstream from "+
 				"Stage %q in namespace %q",
-			subs.UpstreamStages[0].Name,
+			stage.Name,
 			namespace,
 		)
 	}
-	if latestFreight == nil {
-		logger.WithField("upstreamStage", subs.UpstreamStages[0]).
-			Debug("no qualified Freight found for upstream Stage")
+	if latestVerifiedFreight == nil {
+		logger.Debug("no verified Freight found upstream from Stage")
+	}
+
+	latestApprovedFreight, err := r.getLatestApprovedFreightFn(
+		ctx,
+		namespace,
+		stage.Name,
+	)
+	if err != nil {
+		return nil, errors.Wrapf(
+			err,
+			"error finding latest Freight approved for Stage %q in namespace %q",
+			stage.Name,
+			namespace,
+		)
+	}
+	if latestVerifiedFreight == nil {
+		logger.Debug("no approved Freight found for Stage")
+	}
+
+	if latestVerifiedFreight == nil && latestApprovedFreight == nil {
+		return nil, nil
 	}
-	return latestFreight, nil
+	if latestVerifiedFreight != nil && latestApprovedFreight == nil {
+		return latestVerifiedFreight, nil
+	}
+	if latestVerifiedFreight == nil && latestApprovedFreight != nil {
+		return latestApprovedFreight, nil
+	}
+	if latestVerifiedFreight.CreationTimestamp.
+		After(latestApprovedFreight.CreationTimestamp.Time) {
+		return latestVerifiedFreight, nil
+	}
+	return latestApprovedFreight, nil
 }
 
-func (r *reconciler) getAllFreightFromWarehouse(
+func (r *reconciler) getLatestFreightFromWarehouse(
 	ctx context.Context,
 	namespace string,
 	warehouse string,
-) ([]kargoapi.Freight, error) {
+) (*kargoapi.Freight, error) {
 	var freight kargoapi.FreightList
 	if err := r.listFreightFn(
 		ctx,
@@ -803,32 +881,17 @@ func (r *reconciler) getAllFreightFromWarehouse(
 		return freight.Items[j].CreationTimestamp.
 			Before(&freight.Items[i].CreationTimestamp)
 	})
-	return freight.Items, nil
+	return &freight.Items[0], nil
 }
 
-func (r *reconciler) getLatestFreightFromWarehouse(
-	ctx context.Context,
-	namespace string,
-	warehouse string,
-) (*kargoapi.Freight, error) {
-	freight, err := r.getAllFreightFromWarehouseFn(ctx, namespace, warehouse)
-	if err != nil {
-		return nil, err
-	}
-	if len(freight) == 0 {
-		return nil, nil
-	}
-	return &freight[0], nil
-}
-
-func (r *reconciler) getAllFreightQualifiedForUpstreamStages(
+func (r *reconciler) getAllVerifiedFreight(
 	ctx context.Context,
 	namespace string,
 	stageSubs []kargoapi.StageSubscription,
 ) ([]kargoapi.Freight, error) {
-	// Start by building a de-duped map of Freight qualified for ANY upstream
-	// Stage
-	qualifiedFreight := map[string]kargoapi.Freight{}
+	// Start by building a de-duped map of Freight verified in any upstream
+	// Stage(s)
+	verifiedFreight := map[string]kargoapi.Freight{}
 	for _, stageSub := range stageSubs {
 		var freight kargoapi.FreightList
 		if err := r.listFreightFn(
@@ -837,52 +900,87 @@ func (r *reconciler) getAllFreightQualifiedForUpstreamStages(
 			&client.ListOptions{
 				Namespace: namespace,
 				FieldSelector: fields.OneTermEqualSelector(
-					kubeclient.FreightByQualifiedStagesIndexField,
+					kubeclient.FreightByVerifiedStagesIndexField,
 					stageSub.Name,
 				),
 			},
 		); err != nil {
 			return nil, errors.Wrapf(
 				err,
-				"error listing Freight qualified for Stage %q in namespace %q",
+				"error listing Freight verified in Stage %q in namespace %q",
 				stageSub.Name,
 				namespace,
 			)
 		}
 		for _, freight := range freight.Items {
-			qualifiedFreight[freight.Name] = freight
+			verifiedFreight[freight.Name] = freight
 		}
 	}
-	if len(qualifiedFreight) == 0 {
+	if len(verifiedFreight) == 0 {
 		return nil, nil
 	}
 	// Turn the map to a list
-	qualifiedFreightList := make([]kargoapi.Freight, len(qualifiedFreight))
+	verifiedFreightList := make([]kargoapi.Freight, len(verifiedFreight))
 	i := 0
-	for _, freight := range qualifiedFreight {
-		qualifiedFreightList[i] = freight
+	for _, freight := range verifiedFreight {
+		verifiedFreightList[i] = freight
 		i++
 	}
-	// Sort the list by creation timestamp, descending
-	sort.SliceStable(qualifiedFreightList, func(i, j int) bool {
-		return qualifiedFreightList[j].CreationTimestamp.
-			Before(&qualifiedFreightList[i].CreationTimestamp)
-	})
-	return qualifiedFreightList, nil
+	return verifiedFreightList, nil
 }
 
-func (r *reconciler) getLatestFreightQualifiedForUpstreamStages(
+func (r *reconciler) getLatestVerifiedFreight(
 	ctx context.Context,
 	namespace string,
 	stageSubs []kargoapi.StageSubscription,
 ) (*kargoapi.Freight, error) {
-	qualifiedFreight, err :=
-		r.getAllFreightQualifiedForUpstreamStagesFn(ctx, namespace, stageSubs)
+	verifiedFreight, err :=
+		r.getAllVerifiedFreightFn(ctx, namespace, stageSubs)
 	if err != nil {
 		return nil, err
 	}
-	if len(qualifiedFreight) == 0 {
+	if len(verifiedFreight) == 0 {
+		return nil, nil
+	}
+	// Sort the list by creation timestamp, descending
+	sort.SliceStable(verifiedFreight, func(i, j int) bool {
+		return verifiedFreight[j].CreationTimestamp.
+			Before(&verifiedFreight[i].CreationTimestamp)
+	})
+	return &verifiedFreight[0], nil
+}
+
+func (r *reconciler) getLatestApprovedFreight(
+	ctx context.Context,
+	namespace string,
+	stage string,
+) (*kargoapi.Freight, error) {
+	var freight kargoapi.FreightList
+	if err := r.listFreightFn(
+		ctx,
+		&freight,
+		&client.ListOptions{
+			Namespace: namespace,
+			FieldSelector: fields.OneTermEqualSelector(
+				kubeclient.FreightApprovedForStagesIndexField,
+				stage,
+			),
+		},
+	); err != nil {
+		return nil, errors.Wrapf(
+			err,
+			"error listing Freight verified in Stage %q in namespace %q",
+			stage,
+			namespace,
+		)
+	}
+	if len(freight.Items) == 0 {
 		return nil, nil
 	}
-	return &qualifiedFreight[0], nil
+	// Sort the list by creation timestamp, descending
+	sort.SliceStable(freight.Items, func(i, j int) bool {
+		return freight.Items[j].CreationTimestamp.
+			Before(&freight.Items[i].CreationTimestamp)
+	})
+	return &freight.Items[0], nil
 }
diff --git a/internal/controller/stages/watches.go b/internal/controller/stages/watches.go
index 75f1a155..c418aac1 100644
--- a/internal/controller/stages/watches.go
+++ b/internal/controller/stages/watches.go
@@ -15,16 +15,16 @@ import (
 	"github.com/akuity/kargo/internal/kubeclient"
 )
 
-// EnqueueDownstreamStagesHandler is an event handler that enqueues downstream
-// Stages when a Freight is qualified for a Stage, so that those Stages can
-// reconcile and possibly create a Promotion if auto-promotion is enabled.
-type EnqueueDownstreamStagesHandler struct {
+// verifiedFreightEventHandler is an event handler that enqueues downstream
+// Stages when Freight is marked as verified in a Stage, so that those Stages
+// can reconcile and possibly create a Promotion if auto-promotion is enabled.
+type verifiedFreightEventHandler struct {
 	logger      *log.Entry
 	kargoClient client.Client
 }
 
 // Create implements EventHandler.
-func (e *EnqueueDownstreamStagesHandler) Create(
+func (v *verifiedFreightEventHandler) Create(
 	event.CreateEvent,
 	workqueue.RateLimitingInterface,
 ) {
@@ -32,7 +32,7 @@ func (e *EnqueueDownstreamStagesHandler) Create(
 }
 
 // Delete implements EventHandler.
-func (e *EnqueueDownstreamStagesHandler) Delete(
+func (v *verifiedFreightEventHandler) Delete(
 	event.DeleteEvent,
 	workqueue.RateLimitingInterface,
 ) {
@@ -40,7 +40,7 @@ func (e *EnqueueDownstreamStagesHandler) Delete(
 }
 
 // Generic implements EventHandler.
-func (e *EnqueueDownstreamStagesHandler) Generic(
+func (v *verifiedFreightEventHandler) Generic(
 	event.GenericEvent,
 	workqueue.RateLimitingInterface,
 ) {
@@ -48,40 +48,40 @@ func (e *EnqueueDownstreamStagesHandler) Generic(
 }
 
 // Update implements EventHandler.
-func (e *EnqueueDownstreamStagesHandler) Update(
+func (v *verifiedFreightEventHandler) Update(
 	evt event.UpdateEvent,
 	wq workqueue.RateLimitingInterface,
 ) {
 	if evt.ObjectOld == nil || evt.ObjectNew == nil {
-		e.logger.Errorf("Update event has no old or new object to update: %v", evt)
+		v.logger.Errorf("Update event has no old or new object to update: %v", evt)
 		return
 	}
 	oldFreight, ok := evt.ObjectOld.(*kargoapi.Freight)
 	if !ok {
-		e.logger.Errorf("Failed to convert old Freight: %v", evt.ObjectOld)
+		v.logger.Errorf("Failed to convert old Freight: %v", evt.ObjectOld)
 		return
 	}
 	newFreight, ok := evt.ObjectNew.(*kargoapi.Freight)
 	if !ok {
-		e.logger.Errorf("Failed to convert new Freight: %v", evt.ObjectNew)
+		v.logger.Errorf("Failed to convert new Freight: %v", evt.ObjectNew)
 		return
 	}
-	newlyQualifiedStages := getNewlyQualifiedStages(oldFreight, newFreight)
+	newlyVerifiedStages := getNewlyVerifiedStages(oldFreight, newFreight)
 	downstreamStages := map[string]struct{}{}
-	for _, newlyQualifiedStage := range newlyQualifiedStages {
+	for _, newlyVerifiedStage := range newlyVerifiedStages {
 		stages := kargoapi.StageList{}
-		if err := e.kargoClient.List(
+		if err := v.kargoClient.List(
 			context.TODO(),
 			&stages,
 			&client.ListOptions{
 				Namespace: newFreight.Namespace,
 				FieldSelector: fields.OneTermEqualSelector(
 					kubeclient.StagesByUpstreamStagesIndexField,
-					newlyQualifiedStage,
+					newlyVerifiedStage,
 				),
 			},
 		); err != nil {
-			e.logger.Errorf(
+			v.logger.Errorf(
 				"Failed list Stages downstream from Stage %v in namespace %q",
 				evt.ObjectOld,
 				newFreight.Namespace,
@@ -101,19 +101,181 @@ func (e *EnqueueDownstreamStagesHandler) Update(
 				},
 			},
 		)
-		e.logger.WithFields(log.Fields{
+		v.logger.WithFields(log.Fields{
 			"namespace": newFreight.Namespace,
 			"stage":     downStreamStage,
-		}).Debug("enqueued downstream stage")
+		}).Debug("enqueued downstream Stage for reconciliation")
 	}
 }
 
-func getNewlyQualifiedStages(old, new *kargoapi.Freight) []string {
+func getNewlyVerifiedStages(old, new *kargoapi.Freight) []string {
 	var stages []string
-	for stage := range new.Status.Qualifications {
-		if _, ok := old.Status.Qualifications[stage]; !ok {
+	for stage := range new.Status.VerifiedIn {
+		if _, ok := old.Status.VerifiedIn[stage]; !ok {
 			stages = append(stages, stage)
 		}
 	}
 	return stages
 }
+
+// approvedFreightEventHandler is an event handler that enqueues Stages when
+// Freight is marked as approved for them, so that those Stages can reconcile
+// and possibly create a Promotion if auto-promotion is enabled.
+type approvedFreightEventHandler struct {
+	logger      *log.Entry
+	kargoClient client.Client
+}
+
+// Create implements EventHandler.
+func (a *approvedFreightEventHandler) Create(
+	event.CreateEvent,
+	workqueue.RateLimitingInterface,
+) {
+	// No-op
+}
+
+// Delete implements EventHandler.
+func (a *approvedFreightEventHandler) Delete(
+	event.DeleteEvent,
+	workqueue.RateLimitingInterface,
+) {
+	// No-op
+}
+
+// Generic implements EventHandler.
+func (a *approvedFreightEventHandler) Generic(
+	event.GenericEvent,
+	workqueue.RateLimitingInterface,
+) {
+	// No-op
+}
+
+// Update implements EventHandler.
+func (a *approvedFreightEventHandler) Update(
+	evt event.UpdateEvent,
+	wq workqueue.RateLimitingInterface,
+) {
+	if evt.ObjectOld == nil || evt.ObjectNew == nil {
+		a.logger.Errorf("Update event has no old or new object to update: %v", evt)
+		return
+	}
+	oldFreight, ok := evt.ObjectOld.(*kargoapi.Freight)
+	if !ok {
+		a.logger.Errorf("Failed to convert old Freight: %v", evt.ObjectOld)
+		return
+	}
+	newFreight, ok := evt.ObjectNew.(*kargoapi.Freight)
+	if !ok {
+		a.logger.Errorf("Failed to convert new Freight: %v", evt.ObjectNew)
+		return
+	}
+	newlyApprovedStages := getNewlyApprovedStages(oldFreight, newFreight)
+	for _, stage := range newlyApprovedStages {
+		wq.Add(
+			reconcile.Request{
+				NamespacedName: types.NamespacedName{
+					Namespace: newFreight.Namespace,
+					Name:      stage,
+				},
+			},
+		)
+		a.logger.WithFields(log.Fields{
+			"namespace": newFreight.Namespace,
+			"stage":     stage,
+		}).Debug("enqueued Stage fir reconciliation")
+	}
+}
+
+func getNewlyApprovedStages(old, new *kargoapi.Freight) []string {
+	var stages []string
+	for stage := range new.Status.ApprovedFor {
+		if _, ok := old.Status.ApprovedFor[stage]; !ok {
+			stages = append(stages, stage)
+		}
+	}
+	return stages
+}
+
+// createdFreightEventHandler is an event handler that enqueues Stages
+// subscribed to a Freight's Warehouse whenever new Freight is created, so that
+// those Stages can reconcile and possibly create a Promotion if auto-promotion
+// is enabled.
+type createdFreightEventHandler struct {
+	logger      *log.Entry
+	kargoClient client.Client
+}
+
+// Create implements EventHandler.
+func (c *createdFreightEventHandler) Create(
+	evt event.CreateEvent,
+	wq workqueue.RateLimitingInterface,
+) {
+	freight := evt.Object.(*kargoapi.Freight) // nolint: forcetypeassert
+	// TODO: Get warehouse name freight.OwnerReferences
+	if len(freight.OwnerReferences) != 1 {
+		c.logger.Warnf(
+			"Expected Freight %q to have exactly 1 OwnerReference, got %d",
+			freight.Name,
+			len(freight.OwnerReferences),
+		)
+		return
+	}
+	warehouse := freight.OwnerReferences[0].Name
+	stages := kargoapi.StageList{}
+	if err := c.kargoClient.List(
+		context.TODO(),
+		&stages,
+		&client.ListOptions{
+			Namespace: freight.Namespace,
+			FieldSelector: fields.OneTermEqualSelector(
+				kubeclient.StagesByWarehouseIndexField,
+				warehouse,
+			),
+		},
+	); err != nil {
+		c.logger.Errorf(
+			"Failed list Stages subscribed to Warehouse %q in namespace %q",
+			warehouse,
+			freight.Namespace,
+		)
+		return
+	}
+	for _, stage := range stages.Items {
+		wq.Add(
+			reconcile.Request{
+				NamespacedName: types.NamespacedName{
+					Namespace: freight.Namespace,
+					Name:      stage.Name,
+				},
+			},
+		)
+		c.logger.WithFields(log.Fields{
+			"namespace": freight.Namespace,
+			"stage":     stage.Name,
+		}).Debug("enqueued Stage for reconciliation")
+	}
+}
+
+// Delete implements EventHandler.
+func (c *createdFreightEventHandler) Delete(
+	event.DeleteEvent,
+	workqueue.RateLimitingInterface,
+) {
+	// No-op
+}
+
+// Generic implements EventHandler.
+func (c *createdFreightEventHandler) Generic(
+	event.GenericEvent,
+	workqueue.RateLimitingInterface,
+) {
+	// No-op
+}
+
+// Update implements EventHandler.
+func (c *createdFreightEventHandler) Update(
+	event.UpdateEvent,
+	workqueue.RateLimitingInterface,
+) {
+	// No-op
+}
diff --git a/internal/controller/warehouses/aliases.go b/internal/controller/warehouses/aliases.go
new file mode 100644
index 00000000..a7f1b70e
--- /dev/null
+++ b/internal/controller/warehouses/aliases.go
@@ -0,0 +1,35 @@
+package warehouses
+
+import (
+	"context"
+
+	"github.com/pkg/errors"
+	"sigs.k8s.io/controller-runtime/pkg/client"
+
+	kargoapi "github.com/akuity/kargo/api/v1alpha1"
+)
+
+const aliasLabelKey = "kargo.akuity.com/alias"
+
+func (r *reconciler) getAvailableFreightAlias(
+	ctx context.Context,
+) (string, error) {
+	for {
+		alias := r.freightAliasGenerator.NameSep("-")
+		freight := kargoapi.FreightList{}
+		if err := r.client.List(
+			ctx,
+			&freight,
+			client.MatchingLabels{aliasLabelKey: alias},
+		); err != nil {
+			return "", errors.Wrapf(
+				err,
+				"error checking for existence of Freight with alias %q",
+				alias,
+			)
+		}
+		if len(freight.Items) == 0 {
+			return alias, nil
+		}
+	}
+}
diff --git a/internal/controller/warehouses/warehouses.go b/internal/controller/warehouses/warehouses.go
index 33847623..7269a9bc 100644
--- a/internal/controller/warehouses/warehouses.go
+++ b/internal/controller/warehouses/warehouses.go
@@ -5,6 +5,7 @@ import (
 
 	"github.com/pkg/errors"
 	log "github.com/sirupsen/logrus"
+	"github.com/technosophos/moniker"
 	apierrors "k8s.io/apimachinery/pkg/api/errors"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	ctrl "sigs.k8s.io/controller-runtime"
@@ -28,6 +29,7 @@ type reconciler struct {
 	client                     client.Client
 	credentialsDB              credentials.Database
 	imageSourceURLFnsByBaseURL map[string]func(string, string) string
+	freightAliasGenerator      moniker.Namer
 
 	// The following behaviors are overridable for testing purposes:
 
@@ -79,6 +81,8 @@ type reconciler struct {
 		creds *git.RepoCredentials,
 	) (*gitMeta, error)
 
+	getAvailableFreightAliasFn func(context.Context) (string, error)
+
 	createFreightFn func(
 		context.Context,
 		client.Object,
@@ -133,6 +137,7 @@ func newReconciler(
 		imageSourceURLFnsByBaseURL: map[string]func(string, string) string{
 			githubURLPrefix: getGithubImageSourceURL,
 		},
+		freightAliasGenerator: moniker.New(),
 	}
 	r.getLatestFreightFromReposFn = r.getLatestFreightFromRepos
 	r.getLatestCommitsFn = r.getLatestCommits
@@ -141,6 +146,7 @@ func newReconciler(
 	r.getLatestChartsFn = r.getLatestCharts
 	r.getLatestChartVersionFn = helm.GetLatestChartVersion
 	r.getLatestCommitMetaFn = getLatestCommitMeta
+	r.getAvailableFreightAliasFn = r.getAvailableFreightAlias
 	r.createFreightFn = kubeClient.Create
 	return r
 }
@@ -234,6 +240,12 @@ func (r *reconciler) syncWarehouse(
 	}
 	logger.Debug("got latest Freight from repositories")
 
+	freight.Labels = map[string]string{}
+	if freight.Labels[aliasLabelKey], err =
+		r.getAvailableFreightAliasFn(ctx); err != nil {
+		return status, errors.Wrap(err, "error getting available Freight alias")
+	}
+
 	if err = r.createFreightFn(ctx, freight); err != nil {
 		if apierrors.IsAlreadyExists(err) {
 			logger.Debugf(
diff --git a/internal/kubeclient/indexer.go b/internal/kubeclient/indexer.go
index 6a3a2f52..c2713ada 100644
--- a/internal/kubeclient/indexer.go
+++ b/internal/kubeclient/indexer.go
@@ -12,7 +12,8 @@ import (
 )
 
 const (
-	FreightByQualifiedStagesIndexField    = "qualifiedStages"
+	FreightByVerifiedStagesIndexField     = "verifiedIn"
+	FreightApprovedForStagesIndexField    = "approvedFor"
 	FreightByWarehouseIndexField          = "warehouse"
 	PromotionsByStageAndFreightIndexField = "stageAndFreight"
 
@@ -24,6 +25,7 @@ const (
 	PromotionPoliciesByStageIndexField   = "stage"
 	StagesByArgoCDApplicationsIndexField = "applications"
 	StagesByUpstreamStagesIndexField     = "upstreamStages"
+	StagesByWarehouseIndexField          = "warehouse"
 )
 
 func IndexStagesByArgoCDApplications(ctx context.Context, mgr ctrl.Manager, shardName string) error {
@@ -164,27 +166,54 @@ func indexFreightByWarehouse(obj client.Object) []string {
 	return nil
 }
 
-func IndexFreightByQualifiedStages(
+// IndexFreightByVerifiedStages indexes Freight by the Stages in which it has
+// been verified.
+func IndexFreightByVerifiedStages(
 	ctx context.Context,
 	mgr ctrl.Manager,
 ) error {
 	return mgr.GetFieldIndexer().IndexField(
 		ctx,
 		&kargoapi.Freight{},
-		FreightByQualifiedStagesIndexField,
-		indexFreightByQualifiedStages,
+		FreightByVerifiedStagesIndexField,
+		indexFreightByVerifiedStages,
 	)
 }
 
-func indexFreightByQualifiedStages(obj client.Object) []string {
+func indexFreightByVerifiedStages(obj client.Object) []string {
 	freight := obj.(*kargoapi.Freight) // nolint: forcetypeassert
-	qualifiedStages := make([]string, len(freight.Status.Qualifications))
+	verifiedStages := make([]string, len(freight.Status.VerifiedIn))
 	var i int
-	for qualifiedStage := range freight.Status.Qualifications {
-		qualifiedStages[i] = qualifiedStage
+	for stage := range freight.Status.VerifiedIn {
+		verifiedStages[i] = stage
 		i++
 	}
-	return qualifiedStages
+	return verifiedStages
+}
+
+// IndexFreightByApprovedStages indexes Freight by the Stages for which it has
+// been (manually) approved.
+func IndexFreightByApprovedStages(
+	ctx context.Context,
+	mgr ctrl.Manager,
+) error {
+	return mgr.GetFieldIndexer().IndexField(
+		ctx,
+		&kargoapi.Freight{},
+		FreightApprovedForStagesIndexField,
+		indexFreightByApprovedStages,
+	)
+}
+
+func indexFreightByApprovedStages(obj client.Object) []string {
+	freight := obj.(*kargoapi.Freight) // nolint: forcetypeassert
+	approvedStages := make([]string, len(freight.Status.ApprovedFor))
+	var i int
+	for stages := range freight.Status.ApprovedFor {
+		approvedStages[i] = stages
+		i++
+	}
+	return approvedStages
 }
 
 func IndexStagesByUpstreamStages(ctx context.Context, mgr ctrl.Manager) error {
@@ -207,3 +236,20 @@ func indexStagesByUpstreamStages(obj client.Object) []string {
 	}
 	return upstreamStages
 }
+
+func IndexStagesByWarehouse(ctx context.Context, mgr ctrl.Manager) error {
+	return mgr.GetFieldIndexer().IndexField(
+		ctx,
+		&kargoapi.Stage{},
+		StagesByWarehouseIndexField,
+		indexStagesByWarehouse,
+	)
+}
+
+func indexStagesByWarehouse(obj client.Object) []string {
+	stage := obj.(*kargoapi.Stage) // nolint: forcetypeassert
+	if stage.Spec.Subscriptions.Warehouse != "" {
+		return []string{stage.Spec.Subscriptions.Warehouse}
+	}
+	return nil
+}
diff --git a/pkg/api/v1alpha1/types.pb.go b/pkg/api/v1alpha1/types.pb.go
index 617a9ff3..5692e035 100644
--- a/pkg/api/v1alpha1/types.pb.go
+++ b/pkg/api/v1alpha1/types.pb.go
@@ -2247,7 +2247,8 @@ type FreightStatus struct {
 	sizeCache     protoimpl.SizeCache
 	unknownFields protoimpl.UnknownFields
 
-	Qualifications map[string]*Qualification `protobuf:"bytes,1,rep,name=qualifications,proto3" json:"qualifications,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"`
+	VerifiedIn  map[string]*VerifiedStage `protobuf:"bytes,1,rep,name=verified_in,json=verifiedIn,proto3" json:"verified_in,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"`
+	ApprovedFor map[string]*ApprovedStage `protobuf:"bytes,2,rep,name=approved_for,json=approvedFor,proto3" json:"approved_for,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"`
 }
 
 func (x *FreightStatus) Reset() {
@@ -2282,21 +2283,28 @@ func (*FreightStatus) Descriptor() ([]byte, []int) {
 	return file_v1alpha1_types_proto_rawDescGZIP(), []int{35}
 }
 
-func (x *FreightStatus) GetQualifications() map[string]*Qualification {
+func (x *FreightStatus) GetVerifiedIn() map[string]*VerifiedStage {
 	if x != nil {
-		return x.Qualifications
+		return x.VerifiedIn
 	}
 	return nil
 }
 
-type Qualification struct {
+func (x *FreightStatus) GetApprovedFor() map[string]*ApprovedStage {
+	if x != nil {
+		return x.ApprovedFor
+	}
+	return nil
+}
+
+type VerifiedStage struct {
 	state         protoimpl.MessageState
 	sizeCache     protoimpl.SizeCache
 	unknownFields protoimpl.UnknownFields
 }
 
-func (x *Qualification) Reset() {
-	*x = Qualification{}
+func (x *VerifiedStage) Reset() {
+	*x = VerifiedStage{}
 	if protoimpl.UnsafeEnabled {
 		mi := &file_v1alpha1_types_proto_msgTypes[36]
 		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
@@ -2304,13 +2312,13 @@ func (x *Qualification) Reset() {
 	}
 }
 
-func (x *Qualification) String() string {
+func (x *VerifiedStage) String() string {
 	return protoimpl.X.MessageStringOf(x)
 }
 
-func (*Qualification) ProtoMessage() {}
+func (*VerifiedStage) ProtoMessage() {}
 
-func (x *Qualification) ProtoReflect() protoreflect.Message {
+func (x *VerifiedStage) ProtoReflect() protoreflect.Message {
 	mi := &file_v1alpha1_types_proto_msgTypes[36]
 	if protoimpl.UnsafeEnabled && x != nil {
 		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
@@ -2322,11 +2330,49 @@ func (x *Qualification) ProtoReflect() protoreflect.Message {
 	return mi.MessageOf(x)
 }
 
-// Deprecated: Use Qualification.ProtoReflect.Descriptor instead.
-func (*Qualification) Descriptor() ([]byte, []int) {
+// Deprecated: Use VerifiedStage.ProtoReflect.Descriptor instead.
+func (*VerifiedStage) Descriptor() ([]byte, []int) {
 	return file_v1alpha1_types_proto_rawDescGZIP(), []int{36}
 }
 
+type ApprovedStage struct {
+	state         protoimpl.MessageState
+	sizeCache     protoimpl.SizeCache
+	unknownFields protoimpl.UnknownFields
+}
+
+func (x *ApprovedStage) Reset() {
+	*x = ApprovedStage{}
+	if protoimpl.UnsafeEnabled {
+		mi := &file_v1alpha1_types_proto_msgTypes[37]
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		ms.StoreMessageInfo(mi)
+	}
+}
+
+func (x *ApprovedStage) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*ApprovedStage) ProtoMessage() {}
+
+func (x *ApprovedStage) ProtoReflect() protoreflect.Message {
+	mi := &file_v1alpha1_types_proto_msgTypes[37]
+	if protoimpl.UnsafeEnabled && x != nil {
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		if ms.LoadMessageInfo() == nil {
+			ms.StoreMessageInfo(mi)
+		}
+		return ms
+	}
+	return mi.MessageOf(x)
+}
+
+// Deprecated: Use ApprovedStage.ProtoReflect.Descriptor instead.
+func (*ApprovedStage) Descriptor() ([]byte, []int) {
+	return file_v1alpha1_types_proto_rawDescGZIP(), []int{37}
+}
+
 type SimpleFreight struct {
 	state         protoimpl.MessageState
 	sizeCache     protoimpl.SizeCache
@@ -2342,7 +2388,7 @@ type SimpleFreight struct {
 func (x *SimpleFreight) Reset() {
 	*x = SimpleFreight{}
 	if protoimpl.UnsafeEnabled {
-		mi := &file_v1alpha1_types_proto_msgTypes[37]
+		mi := &file_v1alpha1_types_proto_msgTypes[38]
 		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
 		ms.StoreMessageInfo(mi)
 	}
@@ -2355,7 +2401,7 @@ func (x *SimpleFreight) String() string {
 func (*SimpleFreight) ProtoMessage() {}
 
 func (x *SimpleFreight) ProtoReflect() protoreflect.Message {
-	mi := &file_v1alpha1_types_proto_msgTypes[37]
+	mi := &file_v1alpha1_types_proto_msgTypes[38]
 	if protoimpl.UnsafeEnabled && x != nil {
 		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
 		if ms.LoadMessageInfo() == nil {
@@ -2368,7 +2414,7 @@ func (x *SimpleFreight) ProtoReflect() protoreflect.Message {
 
 // Deprecated: Use SimpleFreight.ProtoReflect.Descriptor instead.
 func (*SimpleFreight) Descriptor() ([]byte, []int) {
-	return file_v1alpha1_types_proto_rawDescGZIP(), []int{37}
+	return file_v1alpha1_types_proto_rawDescGZIP(), []int{38}
 }
 
 func (x *SimpleFreight) GetId() string {
@@ -2421,7 +2467,7 @@ type StageStatus struct {
 func (x *StageStatus) Reset() {
 	*x = StageStatus{}
 	if protoimpl.UnsafeEnabled {
-		mi := &file_v1alpha1_types_proto_msgTypes[38]
+		mi := &file_v1alpha1_types_proto_msgTypes[39]
 		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
 		ms.StoreMessageInfo(mi)
 	}
@@ -2434,7 +2480,7 @@ func (x *StageStatus) String() string {
 func (*StageStatus) ProtoMessage() {}
 
 func (x *StageStatus) ProtoReflect() protoreflect.Message {
-	mi := &file_v1alpha1_types_proto_msgTypes[38]
+	mi := &file_v1alpha1_types_proto_msgTypes[39]
 	if protoimpl.UnsafeEnabled && x != nil {
 		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
 		if ms.LoadMessageInfo() == nil {
@@ -2447,7 +2493,7 @@ func (x *StageStatus) ProtoReflect() protoreflect.Message {
 
 // Deprecated: Use StageStatus.ProtoReflect.Descriptor instead.
 func (*StageStatus) Descriptor() ([]byte, []int) {
-	return file_v1alpha1_types_proto_rawDescGZIP(), []int{38}
+	return file_v1alpha1_types_proto_rawDescGZIP(), []int{39}
 }
 
 func (x *StageStatus) GetCurrentFreight() *SimpleFreight {
@@ -2496,7 +2542,7 @@ type StageSubscription struct {
 func (x *StageSubscription) Reset() {
 	*x = StageSubscription{}
 	if protoimpl.UnsafeEnabled {
-		mi := &file_v1alpha1_types_proto_msgTypes[39]
+		mi := &file_v1alpha1_types_proto_msgTypes[40]
 		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
 		ms.StoreMessageInfo(mi)
 	}
@@ -2509,7 +2555,7 @@ func (x *StageSubscription) String() string {
 func (*StageSubscription) ProtoMessage() {}
 
 func (x *StageSubscription) ProtoReflect() protoreflect.Message {
-	mi := &file_v1alpha1_types_proto_msgTypes[39]
+	mi := &file_v1alpha1_types_proto_msgTypes[40]
 	if protoimpl.UnsafeEnabled && x != nil {
 		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
 		if ms.LoadMessageInfo() == nil {
@@ -2522,7 +2568,7 @@ func (x *StageSubscription) ProtoReflect() protoreflect.Message {
 
 // Deprecated: Use StageSubscription.ProtoReflect.Descriptor instead.
 func (*StageSubscription) Descriptor() ([]byte, []int) {
-	return file_v1alpha1_types_proto_rawDescGZIP(), []int{39}
+	return file_v1alpha1_types_proto_rawDescGZIP(), []int{40}
 }
 
 func (x *StageSubscription) GetName() string {
@@ -2544,7 +2590,7 @@ type Subscriptions struct {
 func (x *Subscriptions) Reset() {
 	*x = Subscriptions{}
 	if protoimpl.UnsafeEnabled {
-		mi := &file_v1alpha1_types_proto_msgTypes[40]
+		mi := &file_v1alpha1_types_proto_msgTypes[41]
 		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
 		ms.StoreMessageInfo(mi)
 	}
@@ -2557,7 +2603,7 @@ func (x *Subscriptions) String() string {
 func (*Subscriptions) ProtoMessage() {}
 
 func (x *Subscriptions) ProtoReflect() protoreflect.Message {
-	mi := &file_v1alpha1_types_proto_msgTypes[40]
+	mi := &file_v1alpha1_types_proto_msgTypes[41]
 	if protoimpl.UnsafeEnabled && x != nil {
 		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
 		if ms.LoadMessageInfo() == nil {
@@ -2570,7 +2616,7 @@ func (x *Subscriptions) ProtoReflect() protoreflect.Message {
 
 // Deprecated: Use Subscriptions.ProtoReflect.Descriptor instead.
 func (*Subscriptions) Descriptor() ([]byte, []int) {
-	return file_v1alpha1_types_proto_rawDescGZIP(), []int{40}
+	return file_v1alpha1_types_proto_rawDescGZIP(), []int{41}
 }
 
 func (x *Subscriptions) GetUpstreamStages() []*StageSubscription {
@@ -2602,7 +2648,7 @@ type Warehouse struct {
 func (x *Warehouse) Reset() {
 	*x = Warehouse{}
 	if protoimpl.UnsafeEnabled {
-		mi := &file_v1alpha1_types_proto_msgTypes[41]
+		mi := &file_v1alpha1_types_proto_msgTypes[42]
 		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
 		ms.StoreMessageInfo(mi)
 	}
@@ -2615,7 +2661,7 @@ func (x *Warehouse) String() string {
 func (*Warehouse) ProtoMessage() {}
 
 func (x *Warehouse) ProtoReflect() protoreflect.Message {
-	mi := &file_v1alpha1_types_proto_msgTypes[41]
+	mi := &file_v1alpha1_types_proto_msgTypes[42]
 	if protoimpl.UnsafeEnabled && x != nil {
 		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
 		if ms.LoadMessageInfo() == nil {
@@ -2628,7 +2674,7 @@ func (x *Warehouse) ProtoReflect() protoreflect.Message {
 
 // Deprecated: Use Warehouse.ProtoReflect.Descriptor instead.
 func (*Warehouse) Descriptor() ([]byte, []int) {
-	return file_v1alpha1_types_proto_rawDescGZIP(), []int{41}
+	return file_v1alpha1_types_proto_rawDescGZIP(), []int{42}
 }
 
 func (x *Warehouse) GetApiVersion() string {
@@ -2677,7 +2723,7 @@ type WarehouseSpec struct {
 func (x *WarehouseSpec) Reset() {
 	*x = WarehouseSpec{}
 	if protoimpl.UnsafeEnabled {
-		mi := &file_v1alpha1_types_proto_msgTypes[42]
+		mi := &file_v1alpha1_types_proto_msgTypes[43]
 		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
 		ms.StoreMessageInfo(mi)
 	}
@@ -2690,7 +2736,7 @@ func (x *WarehouseSpec) String() string {
 func (*WarehouseSpec) ProtoMessage() {}
 
 func (x *WarehouseSpec) ProtoReflect() protoreflect.Message {
-	mi := &file_v1alpha1_types_proto_msgTypes[42]
+	mi := &file_v1alpha1_types_proto_msgTypes[43]
 	if protoimpl.UnsafeEnabled && x != nil {
 		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
 		if ms.LoadMessageInfo() == nil {
@@ -2703,7 +2749,7 @@ func (x *WarehouseSpec) ProtoReflect() protoreflect.Message {
 
 // Deprecated: Use WarehouseSpec.ProtoReflect.Descriptor instead.
 func (*WarehouseSpec) Descriptor() ([]byte, []int) {
-	return file_v1alpha1_types_proto_rawDescGZIP(), []int{42}
+	return file_v1alpha1_types_proto_rawDescGZIP(), []int{43}
 }
 
 func (x *WarehouseSpec) GetSubscriptions() []*RepoSubscription {
@@ -2725,7 +2771,7 @@ type WarehouseStatus struct {
 func (x *WarehouseStatus) Reset() {
 	*x = WarehouseStatus{}
 	if protoimpl.UnsafeEnabled {
-		mi := &file_v1alpha1_types_proto_msgTypes[43]
+		mi := &file_v1alpha1_types_proto_msgTypes[44]
 		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
 		ms.StoreMessageInfo(mi)
 	}
@@ -2738,7 +2784,7 @@ func (x *WarehouseStatus) String() string {
 func (*WarehouseStatus) ProtoMessage() {}
 
 func (x *WarehouseStatus) ProtoReflect() protoreflect.Message {
-	mi := &file_v1alpha1_types_proto_msgTypes[43]
+	mi := &file_v1alpha1_types_proto_msgTypes[44]
 	if protoimpl.UnsafeEnabled && x != nil {
 		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
 		if ms.LoadMessageInfo() == nil {
@@ -2751,7 +2797,7 @@ func (x *WarehouseStatus) ProtoReflect() protoreflect.Message {
 
 // Deprecated: Use WarehouseStatus.ProtoReflect.Descriptor instead.
 func (*WarehouseStatus) Descriptor() ([]byte, []int) {
-	return file_v1alpha1_types_proto_rawDescGZIP(), []int{43}
+	return file_v1alpha1_types_proto_rawDescGZIP(), []int{44}
 }
 
 func (x *WarehouseStatus) GetError() string {
@@ -3169,138 +3215,153 @@ var file_v1alpha1_types_proto_rawDesc = []byte{
 	0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x6b, 0x75, 0x69, 0x74,
 	0x79, 0x2e, 0x6b, 0x61, 0x72, 0x67, 0x6f, 0x2e, 0x70, 0x6b, 0x67, 0x2e, 0x61, 0x70, 0x69, 0x2e,
 	0x76, 0x31, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x31, 0x2e, 0x46, 0x72, 0x65, 0x69, 0x67, 0x68, 0x74,
-	0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x22, 0x80,
-	0x02, 0x0a, 0x0d, 0x46, 0x72, 0x65, 0x69, 0x67, 0x68, 0x74, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73,
-	0x12, 0x73, 0x0a, 0x0e, 0x71, 0x75, 0x61, 0x6c, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f,
-	0x6e, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x4b, 0x2e, 0x67, 0x69, 0x74, 0x68, 0x75,
-	0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x6b, 0x75, 0x69, 0x74, 0x79, 0x2e, 0x6b, 0x61, 0x72,
-	0x67, 0x6f, 0x2e, 0x70, 0x6b, 0x67, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x31, 0x61, 0x6c, 0x70,
-	0x68, 0x61, 0x31, 0x2e, 0x46, 0x72, 0x65, 0x69, 0x67, 0x68, 0x74, 0x53, 0x74, 0x61, 0x74, 0x75,
-	0x73, 0x2e, 0x51, 0x75, 0x61, 0x6c, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73,
-	0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x0e, 0x71, 0x75, 0x61, 0x6c, 0x69, 0x66, 0x69, 0x63, 0x61,
-	0x74, 0x69, 0x6f, 0x6e, 0x73, 0x1a, 0x7a, 0x0a, 0x13, 0x51, 0x75, 0x61, 0x6c, 0x69, 0x66, 0x69,
-	0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03,
-	0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x4d,
-	0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x37, 0x2e,
-	0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x6b, 0x75, 0x69, 0x74,
-	0x79, 0x2e, 0x6b, 0x61, 0x72, 0x67, 0x6f, 0x2e, 0x70, 0x6b, 0x67, 0x2e, 0x61, 0x70, 0x69, 0x2e,
-	0x76, 0x31, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x31, 0x2e, 0x51, 0x75, 0x61, 0x6c, 0x69, 0x66, 0x69,
-	0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38,
-	0x01, 0x22, 0x0f, 0x0a, 0x0d, 0x51, 0x75, 0x61, 0x6c, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69,
-	0x6f, 0x6e, 0x22, 0xcf, 0x02, 0x0a, 0x0d, 0x53, 0x69, 0x6d, 0x70, 0x6c, 0x65, 0x46, 0x72, 0x65,
-	0x69, 0x67, 0x68, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09,
-	0x52, 0x02, 0x69, 0x64, 0x12, 0x3e, 0x0a, 0x0a, 0x66, 0x69, 0x72, 0x73, 0x74, 0x5f, 0x73, 0x65,
-	0x65, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c,
-	0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73,
-	0x74, 0x61, 0x6d, 0x70, 0x48, 0x00, 0x52, 0x09, 0x66, 0x69, 0x72, 0x73, 0x74, 0x53, 0x65, 0x65,
-	0x6e, 0x88, 0x01, 0x01, 0x12, 0x4d, 0x0a, 0x07, 0x63, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x73, 0x18,
-	0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x33, 0x2e, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63,
+	0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x22, 0xd7,
+	0x03, 0x0a, 0x0d, 0x46, 0x72, 0x65, 0x69, 0x67, 0x68, 0x74, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73,
+	0x12, 0x68, 0x0a, 0x0b, 0x76, 0x65, 0x72, 0x69, 0x66, 0x69, 0x65, 0x64, 0x5f, 0x69, 0x6e, 0x18,
+	0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x47, 0x2e, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63,
 	0x6f, 0x6d, 0x2e, 0x61, 0x6b, 0x75, 0x69, 0x74, 0x79, 0x2e, 0x6b, 0x61, 0x72, 0x67, 0x6f, 0x2e,
 	0x70, 0x6b, 0x67, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x31, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x31,
-	0x2e, 0x47, 0x69, 0x74, 0x43, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x52, 0x07, 0x63, 0x6f, 0x6d, 0x6d,
-	0x69, 0x74, 0x73, 0x12, 0x47, 0x0a, 0x06, 0x69, 0x6d, 0x61, 0x67, 0x65, 0x73, 0x18, 0x05, 0x20,
-	0x03, 0x28, 0x0b, 0x32, 0x2f, 0x2e, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d,
-	0x2e, 0x61, 0x6b, 0x75, 0x69, 0x74, 0x79, 0x2e, 0x6b, 0x61, 0x72, 0x67, 0x6f, 0x2e, 0x70, 0x6b,
-	0x67, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x31, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x31, 0x2e, 0x49,
-	0x6d, 0x61, 0x67, 0x65, 0x52, 0x06, 0x69, 0x6d, 0x61, 0x67, 0x65, 0x73, 0x12, 0x47, 0x0a, 0x06,
-	0x63, 0x68, 0x61, 0x72, 0x74, 0x73, 0x18, 0x06, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x2f, 0x2e, 0x67,
-	0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x6b, 0x75, 0x69, 0x74, 0x79,
-	0x2e, 0x6b, 0x61, 0x72, 0x67, 0x6f, 0x2e, 0x70, 0x6b, 0x67, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76,
-	0x31, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x31, 0x2e, 0x43, 0x68, 0x61, 0x72, 0x74, 0x52, 0x06, 0x63,
-	0x68, 0x61, 0x72, 0x74, 0x73, 0x42, 0x0d, 0x0a, 0x0b, 0x5f, 0x66, 0x69, 0x72, 0x73, 0x74, 0x5f,
-	0x73, 0x65, 0x65, 0x6e, 0x22, 0xcc, 0x03, 0x0a, 0x0b, 0x53, 0x74, 0x61, 0x67, 0x65, 0x53, 0x74,
-	0x61, 0x74, 0x75, 0x73, 0x12, 0x65, 0x0a, 0x0f, 0x63, 0x75, 0x72, 0x72, 0x65, 0x6e, 0x74, 0x5f,
-	0x66, 0x72, 0x65, 0x69, 0x67, 0x68, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x37, 0x2e,
-	0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x6b, 0x75, 0x69, 0x74,
-	0x79, 0x2e, 0x6b, 0x61, 0x72, 0x67, 0x6f, 0x2e, 0x70, 0x6b, 0x67, 0x2e, 0x61, 0x70, 0x69, 0x2e,
-	0x76, 0x31, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x31, 0x2e, 0x53, 0x69, 0x6d, 0x70, 0x6c, 0x65, 0x46,
-	0x72, 0x65, 0x69, 0x67, 0x68, 0x74, 0x48, 0x00, 0x52, 0x0e, 0x63, 0x75, 0x72, 0x72, 0x65, 0x6e,
-	0x74, 0x46, 0x72, 0x65, 0x69, 0x67, 0x68, 0x74, 0x88, 0x01, 0x01, 0x12, 0x51, 0x0a, 0x07, 0x68,
-	0x69, 0x73, 0x74, 0x6f, 0x72, 0x79, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x37, 0x2e, 0x67,
-	0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x6b, 0x75, 0x69, 0x74, 0x79,
-	0x2e, 0x6b, 0x61, 0x72, 0x67, 0x6f, 0x2e, 0x70, 0x6b, 0x67, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76,
-	0x31, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x31, 0x2e, 0x53, 0x69, 0x6d, 0x70, 0x6c, 0x65, 0x46, 0x72,
-	0x65, 0x69, 0x67, 0x68, 0x74, 0x52, 0x07, 0x68, 0x69, 0x73, 0x74, 0x6f, 0x72, 0x79, 0x12, 0x14,
-	0x0a, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x65,
-	0x72, 0x72, 0x6f, 0x72, 0x12, 0x4d, 0x0a, 0x06, 0x68, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x18, 0x05,
-	0x20, 0x01, 0x28, 0x0b, 0x32, 0x30, 0x2e, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f,
+	0x2e, 0x46, 0x72, 0x65, 0x69, 0x67, 0x68, 0x74, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x2e, 0x56,
+	0x65, 0x72, 0x69, 0x66, 0x69, 0x65, 0x64, 0x49, 0x6e, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x0a,
+	0x76, 0x65, 0x72, 0x69, 0x66, 0x69, 0x65, 0x64, 0x49, 0x6e, 0x12, 0x6b, 0x0a, 0x0c, 0x61, 0x70,
+	0x70, 0x72, 0x6f, 0x76, 0x65, 0x64, 0x5f, 0x66, 0x6f, 0x72, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b,
+	0x32, 0x48, 0x2e, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x6b,
+	0x75, 0x69, 0x74, 0x79, 0x2e, 0x6b, 0x61, 0x72, 0x67, 0x6f, 0x2e, 0x70, 0x6b, 0x67, 0x2e, 0x61,
+	0x70, 0x69, 0x2e, 0x76, 0x31, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x31, 0x2e, 0x46, 0x72, 0x65, 0x69,
+	0x67, 0x68, 0x74, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x2e, 0x41, 0x70, 0x70, 0x72, 0x6f, 0x76,
+	0x65, 0x64, 0x46, 0x6f, 0x72, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x0b, 0x61, 0x70, 0x70, 0x72,
+	0x6f, 0x76, 0x65, 0x64, 0x46, 0x6f, 0x72, 0x1a, 0x76, 0x0a, 0x0f, 0x56, 0x65, 0x72, 0x69, 0x66,
+	0x69, 0x65, 0x64, 0x49, 0x6e, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65,
+	0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x4d, 0x0a, 0x05,
+	0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x37, 0x2e, 0x67, 0x69,
+	0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x6b, 0x75, 0x69, 0x74, 0x79, 0x2e,
+	0x6b, 0x61, 0x72, 0x67, 0x6f, 0x2e, 0x70, 0x6b, 0x67, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x31,
+	0x61, 0x6c, 0x70, 0x68, 0x61, 0x31, 0x2e, 0x56, 0x65, 0x72, 0x69, 0x66, 0x69, 0x65, 0x64, 0x53,
+	0x74, 0x61, 0x67, 0x65, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x1a,
+	0x77, 0x0a, 0x10, 0x41, 0x70, 0x70, 0x72, 0x6f, 0x76, 0x65, 0x64, 0x46, 0x6f, 0x72, 0x45, 0x6e,
+	0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09,
+	0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x4d, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02,
+	0x20, 0x01, 0x28, 0x0b, 0x32, 0x37, 0x2e, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f,
 	0x6d, 0x2e, 0x61, 0x6b, 0x75, 0x69, 0x74, 0x79, 0x2e, 0x6b, 0x61, 0x72, 0x67, 0x6f, 0x2e, 0x70,
 	0x6b, 0x67, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x31, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x31, 0x2e,
-	0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x48, 0x01, 0x52, 0x06, 0x68, 0x65, 0x61, 0x6c, 0x74, 0x68,
-	0x88, 0x01, 0x01, 0x12, 0x69, 0x0a, 0x11, 0x63, 0x75, 0x72, 0x72, 0x65, 0x6e, 0x74, 0x5f, 0x70,
-	0x72, 0x6f, 0x6d, 0x6f, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x37,
-	0x2e, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x6b, 0x75, 0x69,
-	0x74, 0x79, 0x2e, 0x6b, 0x61, 0x72, 0x67, 0x6f, 0x2e, 0x70, 0x6b, 0x67, 0x2e, 0x61, 0x70, 0x69,
-	0x2e, 0x76, 0x31, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x31, 0x2e, 0x50, 0x72, 0x6f, 0x6d, 0x6f, 0x74,
-	0x69, 0x6f, 0x6e, 0x49, 0x6e, 0x66, 0x6f, 0x48, 0x02, 0x52, 0x10, 0x63, 0x75, 0x72, 0x72, 0x65,
-	0x6e, 0x74, 0x50, 0x72, 0x6f, 0x6d, 0x6f, 0x74, 0x69, 0x6f, 0x6e, 0x88, 0x01, 0x01, 0x42, 0x12,
-	0x0a, 0x10, 0x5f, 0x63, 0x75, 0x72, 0x72, 0x65, 0x6e, 0x74, 0x5f, 0x66, 0x72, 0x65, 0x69, 0x67,
-	0x68, 0x74, 0x42, 0x09, 0x0a, 0x07, 0x5f, 0x68, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x42, 0x14, 0x0a,
-	0x12, 0x5f, 0x63, 0x75, 0x72, 0x72, 0x65, 0x6e, 0x74, 0x5f, 0x70, 0x72, 0x6f, 0x6d, 0x6f, 0x74,
-	0x69, 0x6f, 0x6e, 0x22, 0x27, 0x0a, 0x11, 0x53, 0x74, 0x61, 0x67, 0x65, 0x53, 0x75, 0x62, 0x73,
-	0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65,
-	0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x22, 0x93, 0x01, 0x0a,
-	0x0d, 0x53, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x64,
-	0x0a, 0x0f, 0x75, 0x70, 0x73, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x5f, 0x73, 0x74, 0x61, 0x67, 0x65,
-	0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x3b, 0x2e, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62,
-	0x2e, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x6b, 0x75, 0x69, 0x74, 0x79, 0x2e, 0x6b, 0x61, 0x72, 0x67,
-	0x6f, 0x2e, 0x70, 0x6b, 0x67, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x31, 0x61, 0x6c, 0x70, 0x68,
-	0x61, 0x31, 0x2e, 0x53, 0x74, 0x61, 0x67, 0x65, 0x53, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x70,
-	0x74, 0x69, 0x6f, 0x6e, 0x52, 0x0e, 0x75, 0x70, 0x73, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x53, 0x74,
-	0x61, 0x67, 0x65, 0x73, 0x12, 0x1c, 0x0a, 0x09, 0x77, 0x61, 0x72, 0x65, 0x68, 0x6f, 0x75, 0x73,
-	0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x77, 0x61, 0x72, 0x65, 0x68, 0x6f, 0x75,
-	0x73, 0x65, 0x22, 0xb0, 0x02, 0x0a, 0x09, 0x57, 0x61, 0x72, 0x65, 0x68, 0x6f, 0x75, 0x73, 0x65,
-	0x12, 0x1f, 0x0a, 0x0b, 0x61, 0x70, 0x69, 0x5f, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18,
-	0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x61, 0x70, 0x69, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f,
-	0x6e, 0x12, 0x12, 0x0a, 0x04, 0x6b, 0x69, 0x6e, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52,
-	0x04, 0x6b, 0x69, 0x6e, 0x64, 0x12, 0x4e, 0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74,
-	0x61, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x32, 0x2e, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62,
-	0x2e, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x6b, 0x75, 0x69, 0x74, 0x79, 0x2e, 0x6b, 0x61, 0x72, 0x67,
-	0x6f, 0x2e, 0x70, 0x6b, 0x67, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x6d, 0x65, 0x74, 0x61, 0x76, 0x31,
-	0x2e, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x4d, 0x65, 0x74, 0x61, 0x52, 0x08, 0x6d, 0x65, 0x74,
-	0x61, 0x64, 0x61, 0x74, 0x61, 0x12, 0x4b, 0x0a, 0x04, 0x73, 0x70, 0x65, 0x63, 0x18, 0x04, 0x20,
-	0x01, 0x28, 0x0b, 0x32, 0x37, 0x2e, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d,
-	0x2e, 0x61, 0x6b, 0x75, 0x69, 0x74, 0x79, 0x2e, 0x6b, 0x61, 0x72, 0x67, 0x6f, 0x2e, 0x70, 0x6b,
-	0x67, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x31, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x31, 0x2e, 0x57,
-	0x61, 0x72, 0x65, 0x68, 0x6f, 0x75, 0x73, 0x65, 0x53, 0x70, 0x65, 0x63, 0x52, 0x04, 0x73, 0x70,
-	0x65, 0x63, 0x12, 0x51, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x05, 0x20, 0x01,
-	0x28, 0x0b, 0x32, 0x39, 0x2e, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2e,
-	0x61, 0x6b, 0x75, 0x69, 0x74, 0x79, 0x2e, 0x6b, 0x61, 0x72, 0x67, 0x6f, 0x2e, 0x70, 0x6b, 0x67,
-	0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x31, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x31, 0x2e, 0x57, 0x61,
-	0x72, 0x65, 0x68, 0x6f, 0x75, 0x73, 0x65, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x06, 0x73,
-	0x74, 0x61, 0x74, 0x75, 0x73, 0x22, 0x71, 0x0a, 0x0d, 0x57, 0x61, 0x72, 0x65, 0x68, 0x6f, 0x75,
-	0x73, 0x65, 0x53, 0x70, 0x65, 0x63, 0x12, 0x60, 0x0a, 0x0d, 0x73, 0x75, 0x62, 0x73, 0x63, 0x72,
-	0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x3a, 0x2e,
+	0x41, 0x70, 0x70, 0x72, 0x6f, 0x76, 0x65, 0x64, 0x53, 0x74, 0x61, 0x67, 0x65, 0x52, 0x05, 0x76,
+	0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0x0f, 0x0a, 0x0d, 0x56, 0x65, 0x72, 0x69,
+	0x66, 0x69, 0x65, 0x64, 0x53, 0x74, 0x61, 0x67, 0x65, 0x22, 0x0f, 0x0a, 0x0d, 0x41, 0x70, 0x70,
+	0x72, 0x6f, 0x76, 0x65, 0x64, 0x53, 0x74, 0x61, 0x67, 0x65, 0x22, 0xcf, 0x02, 0x0a, 0x0d, 0x53,
+	0x69, 0x6d, 0x70, 0x6c, 0x65, 0x46, 0x72, 0x65, 0x69, 0x67, 0x68, 0x74, 0x12, 0x0e, 0x0a, 0x02,
+	0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x3e, 0x0a, 0x0a,
+	0x66, 0x69, 0x72, 0x73, 0x74, 0x5f, 0x73, 0x65, 0x65, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b,
+	0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62,
+	0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x48, 0x00, 0x52, 0x09,
+	0x66, 0x69, 0x72, 0x73, 0x74, 0x53, 0x65, 0x65, 0x6e, 0x88, 0x01, 0x01, 0x12, 0x4d, 0x0a, 0x07,
+	0x63, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x33, 0x2e,
 	0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x6b, 0x75, 0x69, 0x74,
 	0x79, 0x2e, 0x6b, 0x61, 0x72, 0x67, 0x6f, 0x2e, 0x70, 0x6b, 0x67, 0x2e, 0x61, 0x70, 0x69, 0x2e,
-	0x76, 0x31, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x31, 0x2e, 0x52, 0x65, 0x70, 0x6f, 0x53, 0x75, 0x62,
-	0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x0d, 0x73, 0x75, 0x62, 0x73, 0x63,
-	0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x22, 0x58, 0x0a, 0x0f, 0x57, 0x61, 0x72, 0x65,
-	0x68, 0x6f, 0x75, 0x73, 0x65, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x14, 0x0a, 0x05, 0x65,
-	0x72, 0x72, 0x6f, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f,
-	0x72, 0x12, 0x2f, 0x0a, 0x13, 0x6f, 0x62, 0x73, 0x65, 0x72, 0x76, 0x65, 0x64, 0x5f, 0x67, 0x65,
-	0x6e, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x12,
-	0x6f, 0x62, 0x73, 0x65, 0x72, 0x76, 0x65, 0x64, 0x47, 0x65, 0x6e, 0x65, 0x72, 0x61, 0x74, 0x69,
-	0x6f, 0x6e, 0x42, 0xad, 0x02, 0x0a, 0x2c, 0x63, 0x6f, 0x6d, 0x2e, 0x67, 0x69, 0x74, 0x68, 0x75,
-	0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x6b, 0x75, 0x69, 0x74, 0x79, 0x2e, 0x6b, 0x61, 0x72,
-	0x67, 0x6f, 0x2e, 0x70, 0x6b, 0x67, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x31, 0x61, 0x6c, 0x70,
-	0x68, 0x61, 0x31, 0x42, 0x0a, 0x54, 0x79, 0x70, 0x65, 0x73, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x50,
-	0x01, 0x5a, 0x28, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x61, 0x6b,
-	0x75, 0x69, 0x74, 0x79, 0x2f, 0x6b, 0x61, 0x72, 0x67, 0x6f, 0x2f, 0x70, 0x6b, 0x67, 0x2f, 0x61,
-	0x70, 0x69, 0x2f, 0x76, 0x31, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x31, 0xa2, 0x02, 0x06, 0x47, 0x43,
-	0x41, 0x4b, 0x50, 0x41, 0xaa, 0x02, 0x28, 0x47, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x43, 0x6f,
-	0x6d, 0x2e, 0x41, 0x6b, 0x75, 0x69, 0x74, 0x79, 0x2e, 0x4b, 0x61, 0x72, 0x67, 0x6f, 0x2e, 0x50,
-	0x6b, 0x67, 0x2e, 0x41, 0x70, 0x69, 0x2e, 0x56, 0x31, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x31, 0xca,
-	0x02, 0x28, 0x47, 0x69, 0x74, 0x68, 0x75, 0x62, 0x5c, 0x43, 0x6f, 0x6d, 0x5c, 0x41, 0x6b, 0x75,
-	0x69, 0x74, 0x79, 0x5c, 0x4b, 0x61, 0x72, 0x67, 0x6f, 0x5c, 0x50, 0x6b, 0x67, 0x5c, 0x41, 0x70,
-	0x69, 0x5c, 0x56, 0x31, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x31, 0xe2, 0x02, 0x34, 0x47, 0x69, 0x74,
-	0x68, 0x75, 0x62, 0x5c, 0x43, 0x6f, 0x6d, 0x5c, 0x41, 0x6b, 0x75, 0x69, 0x74, 0x79, 0x5c, 0x4b,
-	0x61, 0x72, 0x67, 0x6f, 0x5c, 0x50, 0x6b, 0x67, 0x5c, 0x41, 0x70, 0x69, 0x5c, 0x56, 0x31, 0x61,
-	0x6c, 0x70, 0x68, 0x61, 0x31, 0x5c, 0x47, 0x50, 0x42, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74,
-	0x61, 0xea, 0x02, 0x2e, 0x47, 0x69, 0x74, 0x68, 0x75, 0x62, 0x3a, 0x3a, 0x43, 0x6f, 0x6d, 0x3a,
-	0x3a, 0x41, 0x6b, 0x75, 0x69, 0x74, 0x79, 0x3a, 0x3a, 0x4b, 0x61, 0x72, 0x67, 0x6f, 0x3a, 0x3a,
-	0x50, 0x6b, 0x67, 0x3a, 0x3a, 0x41, 0x70, 0x69, 0x3a, 0x3a, 0x56, 0x31, 0x61, 0x6c, 0x70, 0x68,
-	0x61, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
+	0x76, 0x31, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x31, 0x2e, 0x47, 0x69, 0x74, 0x43, 0x6f, 0x6d, 0x6d,
+	0x69, 0x74, 0x52, 0x07, 0x63, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x73, 0x12, 0x47, 0x0a, 0x06, 0x69,
+	0x6d, 0x61, 0x67, 0x65, 0x73, 0x18, 0x05, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x2f, 0x2e, 0x67, 0x69,
+	0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x6b, 0x75, 0x69, 0x74, 0x79, 0x2e,
+	0x6b, 0x61, 0x72, 0x67, 0x6f, 0x2e, 0x70, 0x6b, 0x67, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x31,
+	0x61, 0x6c, 0x70, 0x68, 0x61, 0x31, 0x2e, 0x49, 0x6d, 0x61, 0x67, 0x65, 0x52, 0x06, 0x69, 0x6d,
+	0x61, 0x67, 0x65, 0x73, 0x12, 0x47, 0x0a, 0x06, 0x63, 0x68, 0x61, 0x72, 0x74, 0x73, 0x18, 0x06,
+	0x20, 0x03, 0x28, 0x0b, 0x32, 0x2f, 0x2e, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f,
+	0x6d, 0x2e, 0x61, 0x6b, 0x75, 0x69, 0x74, 0x79, 0x2e, 0x6b, 0x61, 0x72, 0x67, 0x6f, 0x2e, 0x70,
+	0x6b, 0x67, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x31, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x31, 0x2e,
+	0x43, 0x68, 0x61, 0x72, 0x74, 0x52, 0x06, 0x63, 0x68, 0x61, 0x72, 0x74, 0x73, 0x42, 0x0d, 0x0a,
+	0x0b, 0x5f, 0x66, 0x69, 0x72, 0x73, 0x74, 0x5f, 0x73, 0x65, 0x65, 0x6e, 0x22, 0xcc, 0x03, 0x0a,
+	0x0b, 0x53, 0x74, 0x61, 0x67, 0x65, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x65, 0x0a, 0x0f,
+	0x63, 0x75, 0x72, 0x72, 0x65, 0x6e, 0x74, 0x5f, 0x66, 0x72, 0x65, 0x69, 0x67, 0x68, 0x74, 0x18,
+	0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x37, 0x2e, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63,
+	0x6f, 0x6d, 0x2e, 0x61, 0x6b, 0x75, 0x69, 0x74, 0x79, 0x2e, 0x6b, 0x61, 0x72, 0x67, 0x6f, 0x2e,
+	0x70, 0x6b, 0x67, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x31, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x31,
+	0x2e, 0x53, 0x69, 0x6d, 0x70, 0x6c, 0x65, 0x46, 0x72, 0x65, 0x69, 0x67, 0x68, 0x74, 0x48, 0x00,
+	0x52, 0x0e, 0x63, 0x75, 0x72, 0x72, 0x65, 0x6e, 0x74, 0x46, 0x72, 0x65, 0x69, 0x67, 0x68, 0x74,
+	0x88, 0x01, 0x01, 0x12, 0x51, 0x0a, 0x07, 0x68, 0x69, 0x73, 0x74, 0x6f, 0x72, 0x79, 0x18, 0x03,
+	0x20, 0x03, 0x28, 0x0b, 0x32, 0x37, 0x2e, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f,
+	0x6d, 0x2e, 0x61, 0x6b, 0x75, 0x69, 0x74, 0x79, 0x2e, 0x6b, 0x61, 0x72, 0x67, 0x6f, 0x2e, 0x70,
+	0x6b, 0x67, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x31, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x31, 0x2e,
+	0x53, 0x69, 0x6d, 0x70, 0x6c, 0x65, 0x46, 0x72, 0x65, 0x69, 0x67, 0x68, 0x74, 0x52, 0x07, 0x68,
+	0x69, 0x73, 0x74, 0x6f, 0x72, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18,
+	0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x12, 0x4d, 0x0a, 0x06,
+	0x68, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x30, 0x2e, 0x67,
+	0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x6b, 0x75, 0x69, 0x74, 0x79,
+	0x2e, 0x6b, 0x61, 0x72, 0x67, 0x6f, 0x2e, 0x70, 0x6b, 0x67, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76,
+	0x31, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x31, 0x2e, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x48, 0x01,
+	0x52, 0x06, 0x68, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x88, 0x01, 0x01, 0x12, 0x69, 0x0a, 0x11, 0x63,
+	0x75, 0x72, 0x72, 0x65, 0x6e, 0x74, 0x5f, 0x70, 0x72, 0x6f, 0x6d, 0x6f, 0x74, 0x69, 0x6f, 0x6e,
+	0x18, 0x06, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x37, 0x2e, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e,
+	0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x6b, 0x75, 0x69, 0x74, 0x79, 0x2e, 0x6b, 0x61, 0x72, 0x67, 0x6f,
+	0x2e, 0x70, 0x6b, 0x67, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x31, 0x61, 0x6c, 0x70, 0x68, 0x61,
+	0x31, 0x2e, 0x50, 0x72, 0x6f, 0x6d, 0x6f, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x6e, 0x66, 0x6f, 0x48,
+	0x02, 0x52, 0x10, 0x63, 0x75, 0x72, 0x72, 0x65, 0x6e, 0x74, 0x50, 0x72, 0x6f, 0x6d, 0x6f, 0x74,
+	0x69, 0x6f, 0x6e, 0x88, 0x01, 0x01, 0x42, 0x12, 0x0a, 0x10, 0x5f, 0x63, 0x75, 0x72, 0x72, 0x65,
+	0x6e, 0x74, 0x5f, 0x66, 0x72, 0x65, 0x69, 0x67, 0x68, 0x74, 0x42, 0x09, 0x0a, 0x07, 0x5f, 0x68,
+	0x65, 0x61, 0x6c, 0x74, 0x68, 0x42, 0x14, 0x0a, 0x12, 0x5f, 0x63, 0x75, 0x72, 0x72, 0x65, 0x6e,
+	0x74, 0x5f, 0x70, 0x72, 0x6f, 0x6d, 0x6f, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x27, 0x0a, 0x11, 0x53,
+	0x74, 0x61, 0x67, 0x65, 0x53, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e,
+	0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04,
+	0x6e, 0x61, 0x6d, 0x65, 0x22, 0x93, 0x01, 0x0a, 0x0d, 0x53, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69,
+	0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x64, 0x0a, 0x0f, 0x75, 0x70, 0x73, 0x74, 0x72, 0x65,
+	0x61, 0x6d, 0x5f, 0x73, 0x74, 0x61, 0x67, 0x65, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32,
+	0x3b, 0x2e, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x6b, 0x75,
+	0x69, 0x74, 0x79, 0x2e, 0x6b, 0x61, 0x72, 0x67, 0x6f, 0x2e, 0x70, 0x6b, 0x67, 0x2e, 0x61, 0x70,
+	0x69, 0x2e, 0x76, 0x31, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x31, 0x2e, 0x53, 0x74, 0x61, 0x67, 0x65,
+	0x53, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x0e, 0x75, 0x70,
+	0x73, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x53, 0x74, 0x61, 0x67, 0x65, 0x73, 0x12, 0x1c, 0x0a, 0x09,
+	0x77, 0x61, 0x72, 0x65, 0x68, 0x6f, 0x75, 0x73, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52,
+	0x09, 0x77, 0x61, 0x72, 0x65, 0x68, 0x6f, 0x75, 0x73, 0x65, 0x22, 0xb0, 0x02, 0x0a, 0x09, 0x57,
+	0x61, 0x72, 0x65, 0x68, 0x6f, 0x75, 0x73, 0x65, 0x12, 0x1f, 0x0a, 0x0b, 0x61, 0x70, 0x69, 0x5f,
+	0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x61,
+	0x70, 0x69, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x12, 0x0a, 0x04, 0x6b, 0x69, 0x6e,
+	0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6b, 0x69, 0x6e, 0x64, 0x12, 0x4e, 0x0a,
+	0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32,
+	0x32, 0x2e, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x6b, 0x75,
+	0x69, 0x74, 0x79, 0x2e, 0x6b, 0x61, 0x72, 0x67, 0x6f, 0x2e, 0x70, 0x6b, 0x67, 0x2e, 0x61, 0x70,
+	0x69, 0x2e, 0x6d, 0x65, 0x74, 0x61, 0x76, 0x31, 0x2e, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x4d,
+	0x65, 0x74, 0x61, 0x52, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x12, 0x4b, 0x0a,
+	0x04, 0x73, 0x70, 0x65, 0x63, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x37, 0x2e, 0x67, 0x69,
+	0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x6b, 0x75, 0x69, 0x74, 0x79, 0x2e,
+	0x6b, 0x61, 0x72, 0x67, 0x6f, 0x2e, 0x70, 0x6b, 0x67, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x31,
+	0x61, 0x6c, 0x70, 0x68, 0x61, 0x31, 0x2e, 0x57, 0x61, 0x72, 0x65, 0x68, 0x6f, 0x75, 0x73, 0x65,
+	0x53, 0x70, 0x65, 0x63, 0x52, 0x04, 0x73, 0x70, 0x65, 0x63, 0x12, 0x51, 0x0a, 0x06, 0x73, 0x74,
+	0x61, 0x74, 0x75, 0x73, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x39, 0x2e, 0x67, 0x69, 0x74,
+	0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x6b, 0x75, 0x69, 0x74, 0x79, 0x2e, 0x6b,
+	0x61, 0x72, 0x67, 0x6f, 0x2e, 0x70, 0x6b, 0x67, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x31, 0x61,
+	0x6c, 0x70, 0x68, 0x61, 0x31, 0x2e, 0x57, 0x61, 0x72, 0x65, 0x68, 0x6f, 0x75, 0x73, 0x65, 0x53,
+	0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x22, 0x71, 0x0a,
+	0x0d, 0x57, 0x61, 0x72, 0x65, 0x68, 0x6f, 0x75, 0x73, 0x65, 0x53, 0x70, 0x65, 0x63, 0x12, 0x60,
+	0x0a, 0x0d, 0x73, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18,
+	0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x3a, 0x2e, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63,
+	0x6f, 0x6d, 0x2e, 0x61, 0x6b, 0x75, 0x69, 0x74, 0x79, 0x2e, 0x6b, 0x61, 0x72, 0x67, 0x6f, 0x2e,
+	0x70, 0x6b, 0x67, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x31, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x31,
+	0x2e, 0x52, 0x65, 0x70, 0x6f, 0x53, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f,
+	0x6e, 0x52, 0x0d, 0x73, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73,
+	0x22, 0x58, 0x0a, 0x0f, 0x57, 0x61, 0x72, 0x65, 0x68, 0x6f, 0x75, 0x73, 0x65, 0x53, 0x74, 0x61,
+	0x74, 0x75, 0x73, 0x12, 0x14, 0x0a, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x01, 0x20, 0x01,
+	0x28, 0x09, 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x12, 0x2f, 0x0a, 0x13, 0x6f, 0x62, 0x73,
+	0x65, 0x72, 0x76, 0x65, 0x64, 0x5f, 0x67, 0x65, 0x6e, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e,
+	0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x12, 0x6f, 0x62, 0x73, 0x65, 0x72, 0x76, 0x65, 0x64,
+	0x47, 0x65, 0x6e, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x42, 0xad, 0x02, 0x0a, 0x2c, 0x63,
+	0x6f, 0x6d, 0x2e, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x6b,
+	0x75, 0x69, 0x74, 0x79, 0x2e, 0x6b, 0x61, 0x72, 0x67, 0x6f, 0x2e, 0x70, 0x6b, 0x67, 0x2e, 0x61,
+	0x70, 0x69, 0x2e, 0x76, 0x31, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x31, 0x42, 0x0a, 0x54, 0x79, 0x70,
+	0x65, 0x73, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x50, 0x01, 0x5a, 0x28, 0x67, 0x69, 0x74, 0x68, 0x75,
+	0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x61, 0x6b, 0x75, 0x69, 0x74, 0x79, 0x2f, 0x6b, 0x61, 0x72,
+	0x67, 0x6f, 0x2f, 0x70, 0x6b, 0x67, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x31, 0x61, 0x6c, 0x70,
+	0x68, 0x61, 0x31, 0xa2, 0x02, 0x06, 0x47, 0x43, 0x41, 0x4b, 0x50, 0x41, 0xaa, 0x02, 0x28, 0x47,
+	0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x43, 0x6f, 0x6d, 0x2e, 0x41, 0x6b, 0x75, 0x69, 0x74, 0x79,
+	0x2e, 0x4b, 0x61, 0x72, 0x67, 0x6f, 0x2e, 0x50, 0x6b, 0x67, 0x2e, 0x41, 0x70, 0x69, 0x2e, 0x56,
+	0x31, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x31, 0xca, 0x02, 0x28, 0x47, 0x69, 0x74, 0x68, 0x75, 0x62,
+	0x5c, 0x43, 0x6f, 0x6d, 0x5c, 0x41, 0x6b, 0x75, 0x69, 0x74, 0x79, 0x5c, 0x4b, 0x61, 0x72, 0x67,
+	0x6f, 0x5c, 0x50, 0x6b, 0x67, 0x5c, 0x41, 0x70, 0x69, 0x5c, 0x56, 0x31, 0x61, 0x6c, 0x70, 0x68,
+	0x61, 0x31, 0xe2, 0x02, 0x34, 0x47, 0x69, 0x74, 0x68, 0x75, 0x62, 0x5c, 0x43, 0x6f, 0x6d, 0x5c,
+	0x41, 0x6b, 0x75, 0x69, 0x74, 0x79, 0x5c, 0x4b, 0x61, 0x72, 0x67, 0x6f, 0x5c, 0x50, 0x6b, 0x67,
+	0x5c, 0x41, 0x70, 0x69, 0x5c, 0x56, 0x31, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x31, 0x5c, 0x47, 0x50,
+	0x42, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0xea, 0x02, 0x2e, 0x47, 0x69, 0x74, 0x68,
+	0x75, 0x62, 0x3a, 0x3a, 0x43, 0x6f, 0x6d, 0x3a, 0x3a, 0x41, 0x6b, 0x75, 0x69, 0x74, 0x79, 0x3a,
+	0x3a, 0x4b, 0x61, 0x72, 0x67, 0x6f, 0x3a, 0x3a, 0x50, 0x6b, 0x67, 0x3a, 0x3a, 0x41, 0x70, 0x69,
+	0x3a, 0x3a, 0x56, 0x31, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74,
+	0x6f, 0x33,
 }
 
 var (
@@ -3315,7 +3376,7 @@ func file_v1alpha1_types_proto_rawDescGZIP() []byte {
 	return file_v1alpha1_types_proto_rawDescData
 }
 
-var file_v1alpha1_types_proto_msgTypes = make([]protoimpl.MessageInfo, 45)
+var file_v1alpha1_types_proto_msgTypes = make([]protoimpl.MessageInfo, 47)
 var file_v1alpha1_types_proto_goTypes = []interface{}{
 	(*ArgoCDAppUpdate)(nil),               // 0: github.com.akuity.kargo.pkg.api.v1alpha1.ArgoCDAppUpdate
 	(*ArgoCDHelm)(nil),                    // 1: github.com.akuity.kargo.pkg.api.v1alpha1.ArgoCDHelm
@@ -3353,18 +3414,20 @@ var file_v1alpha1_types_proto_goTypes = []interface{}{
 	(*StageSpec)(nil),                     // 33: github.com.akuity.kargo.pkg.api.v1alpha1.StageSpec
 	(*Freight)(nil),                       // 34: github.com.akuity.kargo.pkg.api.v1alpha1.Freight
 	(*FreightStatus)(nil),                 // 35: github.com.akuity.kargo.pkg.api.v1alpha1.FreightStatus
-	(*Qualification)(nil),                 // 36: github.com.akuity.kargo.pkg.api.v1alpha1.Qualification
-	(*SimpleFreight)(nil),                 // 37: github.com.akuity.kargo.pkg.api.v1alpha1.SimpleFreight
-	(*StageStatus)(nil),                   // 38: github.com.akuity.kargo.pkg.api.v1alpha1.StageStatus
-	(*StageSubscription)(nil),             // 39: github.com.akuity.kargo.pkg.api.v1alpha1.StageSubscription
-	(*Subscriptions)(nil),                 // 40: github.com.akuity.kargo.pkg.api.v1alpha1.Subscriptions
-	(*Warehouse)(nil),                     // 41: github.com.akuity.kargo.pkg.api.v1alpha1.Warehouse
-	(*WarehouseSpec)(nil),                 // 42: github.com.akuity.kargo.pkg.api.v1alpha1.WarehouseSpec
-	(*WarehouseStatus)(nil),               // 43: github.com.akuity.kargo.pkg.api.v1alpha1.WarehouseStatus
-	nil,                                   // 44: github.com.akuity.kargo.pkg.api.v1alpha1.FreightStatus.QualificationsEntry
-	(*metav1.ObjectMeta)(nil),             // 45: github.com.akuity.kargo.pkg.api.metav1.ObjectMeta
-	(*metav1.ListMeta)(nil),               // 46: github.com.akuity.kargo.pkg.api.metav1.ListMeta
-	(*timestamppb.Timestamp)(nil),         // 47: google.protobuf.Timestamp
+	(*VerifiedStage)(nil),                 // 36: github.com.akuity.kargo.pkg.api.v1alpha1.VerifiedStage
+	(*ApprovedStage)(nil),                 // 37: github.com.akuity.kargo.pkg.api.v1alpha1.ApprovedStage
+	(*SimpleFreight)(nil),                 // 38: github.com.akuity.kargo.pkg.api.v1alpha1.SimpleFreight
+	(*StageStatus)(nil),                   // 39: github.com.akuity.kargo.pkg.api.v1alpha1.StageStatus
+	(*StageSubscription)(nil),             // 40: github.com.akuity.kargo.pkg.api.v1alpha1.StageSubscription
+	(*Subscriptions)(nil),                 // 41: github.com.akuity.kargo.pkg.api.v1alpha1.Subscriptions
+	(*Warehouse)(nil),                     // 42: github.com.akuity.kargo.pkg.api.v1alpha1.Warehouse
+	(*WarehouseSpec)(nil),                 // 43: github.com.akuity.kargo.pkg.api.v1alpha1.WarehouseSpec
+	(*WarehouseStatus)(nil),               // 44: github.com.akuity.kargo.pkg.api.v1alpha1.WarehouseStatus
+	nil,                                   // 45: github.com.akuity.kargo.pkg.api.v1alpha1.FreightStatus.VerifiedInEntry
+	nil,                                   // 46: github.com.akuity.kargo.pkg.api.v1alpha1.FreightStatus.ApprovedForEntry
+	(*metav1.ObjectMeta)(nil),             // 47: github.com.akuity.kargo.pkg.api.metav1.ObjectMeta
+	(*metav1.ListMeta)(nil),               // 48: github.com.akuity.kargo.pkg.api.metav1.ListMeta
+	(*timestamppb.Timestamp)(nil),         // 49: google.protobuf.Timestamp
 }
 var file_v1alpha1_types_proto_depIdxs = []int32{
 	4,  // 0: github.com.akuity.kargo.pkg.api.v1alpha1.ArgoCDAppUpdate.source_updates:type_name -> github.com.akuity.kargo.pkg.api.v1alpha1.ArgoCDSourceUpdate
@@ -3380,52 +3443,54 @@ var file_v1alpha1_types_proto_depIdxs = []int32{
 	16, // 10: github.com.akuity.kargo.pkg.api.v1alpha1.HelmPromotionMechanism.images:type_name -> github.com.akuity.kargo.pkg.api.v1alpha1.HelmImageUpdate
 	15, // 11: github.com.akuity.kargo.pkg.api.v1alpha1.HelmPromotionMechanism.charts:type_name -> github.com.akuity.kargo.pkg.api.v1alpha1.HelmChartDependencyUpdate
 	20, // 12: github.com.akuity.kargo.pkg.api.v1alpha1.KustomizePromotionMechanism.images:type_name -> github.com.akuity.kargo.pkg.api.v1alpha1.KustomizeImageUpdate
-	45, // 13: github.com.akuity.kargo.pkg.api.v1alpha1.Promotion.metadata:type_name -> github.com.akuity.kargo.pkg.api.metav1.ObjectMeta
+	47, // 13: github.com.akuity.kargo.pkg.api.v1alpha1.Promotion.metadata:type_name -> github.com.akuity.kargo.pkg.api.metav1.ObjectMeta
 	28, // 14: github.com.akuity.kargo.pkg.api.v1alpha1.Promotion.spec:type_name -> github.com.akuity.kargo.pkg.api.v1alpha1.PromotionSpec
 	29, // 15: github.com.akuity.kargo.pkg.api.v1alpha1.Promotion.status:type_name -> github.com.akuity.kargo.pkg.api.v1alpha1.PromotionStatus
-	37, // 16: github.com.akuity.kargo.pkg.api.v1alpha1.PromotionInfo.freight:type_name -> github.com.akuity.kargo.pkg.api.v1alpha1.SimpleFreight
-	46, // 17: github.com.akuity.kargo.pkg.api.v1alpha1.PromotionList.metadata:type_name -> github.com.akuity.kargo.pkg.api.metav1.ListMeta
+	38, // 16: github.com.akuity.kargo.pkg.api.v1alpha1.PromotionInfo.freight:type_name -> github.com.akuity.kargo.pkg.api.v1alpha1.SimpleFreight
+	48, // 17: github.com.akuity.kargo.pkg.api.v1alpha1.PromotionList.metadata:type_name -> github.com.akuity.kargo.pkg.api.metav1.ListMeta
 	22, // 18: github.com.akuity.kargo.pkg.api.v1alpha1.PromotionList.items:type_name -> github.com.akuity.kargo.pkg.api.v1alpha1.Promotion
 	9,  // 19: github.com.akuity.kargo.pkg.api.v1alpha1.PromotionMechanisms.git_repo_updates:type_name -> github.com.akuity.kargo.pkg.api.v1alpha1.GitRepoUpdate
 	0,  // 20: github.com.akuity.kargo.pkg.api.v1alpha1.PromotionMechanisms.argocd_app_updates:type_name -> github.com.akuity.kargo.pkg.api.v1alpha1.ArgoCDAppUpdate
-	45, // 21: github.com.akuity.kargo.pkg.api.v1alpha1.PromotionPolicy.metadata:type_name -> github.com.akuity.kargo.pkg.api.metav1.ObjectMeta
-	46, // 22: github.com.akuity.kargo.pkg.api.v1alpha1.PromotionPolicyList.metadata:type_name -> github.com.akuity.kargo.pkg.api.metav1.ListMeta
+	47, // 21: github.com.akuity.kargo.pkg.api.v1alpha1.PromotionPolicy.metadata:type_name -> github.com.akuity.kargo.pkg.api.metav1.ObjectMeta
+	48, // 22: github.com.akuity.kargo.pkg.api.v1alpha1.PromotionPolicyList.metadata:type_name -> github.com.akuity.kargo.pkg.api.metav1.ListMeta
 	26, // 23: github.com.akuity.kargo.pkg.api.v1alpha1.PromotionPolicyList.items:type_name -> github.com.akuity.kargo.pkg.api.v1alpha1.PromotionPolicy
 	10, // 24: github.com.akuity.kargo.pkg.api.v1alpha1.RepoSubscription.git:type_name -> github.com.akuity.kargo.pkg.api.v1alpha1.GitSubscription
 	19, // 25: github.com.akuity.kargo.pkg.api.v1alpha1.RepoSubscription.image:type_name -> github.com.akuity.kargo.pkg.api.v1alpha1.ImageSubscription
 	7,  // 26: github.com.akuity.kargo.pkg.api.v1alpha1.RepoSubscription.chart:type_name -> github.com.akuity.kargo.pkg.api.v1alpha1.ChartSubscription
-	45, // 27: github.com.akuity.kargo.pkg.api.v1alpha1.Stage.metadata:type_name -> github.com.akuity.kargo.pkg.api.metav1.ObjectMeta
+	47, // 27: github.com.akuity.kargo.pkg.api.v1alpha1.Stage.metadata:type_name -> github.com.akuity.kargo.pkg.api.metav1.ObjectMeta
 	33, // 28: github.com.akuity.kargo.pkg.api.v1alpha1.Stage.spec:type_name -> github.com.akuity.kargo.pkg.api.v1alpha1.StageSpec
-	38, // 29: github.com.akuity.kargo.pkg.api.v1alpha1.Stage.status:type_name -> github.com.akuity.kargo.pkg.api.v1alpha1.StageStatus
-	46, // 30: github.com.akuity.kargo.pkg.api.v1alpha1.StageList.metadata:type_name -> github.com.akuity.kargo.pkg.api.metav1.ListMeta
+	39, // 29: github.com.akuity.kargo.pkg.api.v1alpha1.Stage.status:type_name -> github.com.akuity.kargo.pkg.api.v1alpha1.StageStatus
+	48, // 30: github.com.akuity.kargo.pkg.api.v1alpha1.StageList.metadata:type_name -> github.com.akuity.kargo.pkg.api.metav1.ListMeta
 	31, // 31: github.com.akuity.kargo.pkg.api.v1alpha1.StageList.items:type_name -> github.com.akuity.kargo.pkg.api.v1alpha1.Stage
-	40, // 32: github.com.akuity.kargo.pkg.api.v1alpha1.StageSpec.subscriptions:type_name -> github.com.akuity.kargo.pkg.api.v1alpha1.Subscriptions
+	41, // 32: github.com.akuity.kargo.pkg.api.v1alpha1.StageSpec.subscriptions:type_name -> github.com.akuity.kargo.pkg.api.v1alpha1.Subscriptions
 	25, // 33: github.com.akuity.kargo.pkg.api.v1alpha1.StageSpec.promotion_mechanisms:type_name -> github.com.akuity.kargo.pkg.api.v1alpha1.PromotionMechanisms
-	45, // 34: github.com.akuity.kargo.pkg.api.v1alpha1.Freight.metadata:type_name -> github.com.akuity.kargo.pkg.api.metav1.ObjectMeta
+	47, // 34: github.com.akuity.kargo.pkg.api.v1alpha1.Freight.metadata:type_name -> github.com.akuity.kargo.pkg.api.metav1.ObjectMeta
 	8,  // 35: github.com.akuity.kargo.pkg.api.v1alpha1.Freight.commits:type_name -> github.com.akuity.kargo.pkg.api.v1alpha1.GitCommit
 	18, // 36: github.com.akuity.kargo.pkg.api.v1alpha1.Freight.images:type_name -> github.com.akuity.kargo.pkg.api.v1alpha1.Image
 	6,  // 37: github.com.akuity.kargo.pkg.api.v1alpha1.Freight.charts:type_name -> github.com.akuity.kargo.pkg.api.v1alpha1.Chart
 	35, // 38: github.com.akuity.kargo.pkg.api.v1alpha1.Freight.status:type_name -> github.com.akuity.kargo.pkg.api.v1alpha1.FreightStatus
-	44, // 39: github.com.akuity.kargo.pkg.api.v1alpha1.FreightStatus.qualifications:type_name -> github.com.akuity.kargo.pkg.api.v1alpha1.FreightStatus.QualificationsEntry
-	47, // 40: github.com.akuity.kargo.pkg.api.v1alpha1.SimpleFreight.first_seen:type_name -> google.protobuf.Timestamp
-	8,  // 41: github.com.akuity.kargo.pkg.api.v1alpha1.SimpleFreight.commits:type_name -> github.com.akuity.kargo.pkg.api.v1alpha1.GitCommit
-	18, // 42: github.com.akuity.kargo.pkg.api.v1alpha1.SimpleFreight.images:type_name -> github.com.akuity.kargo.pkg.api.v1alpha1.Image
-	6,  // 43: github.com.akuity.kargo.pkg.api.v1alpha1.SimpleFreight.charts:type_name -> github.com.akuity.kargo.pkg.api.v1alpha1.Chart
-	37, // 44: github.com.akuity.kargo.pkg.api.v1alpha1.StageStatus.current_freight:type_name -> github.com.akuity.kargo.pkg.api.v1alpha1.SimpleFreight
-	37, // 45: github.com.akuity.kargo.pkg.api.v1alpha1.StageStatus.history:type_name -> github.com.akuity.kargo.pkg.api.v1alpha1.SimpleFreight
-	11, // 46: github.com.akuity.kargo.pkg.api.v1alpha1.StageStatus.health:type_name -> github.com.akuity.kargo.pkg.api.v1alpha1.Health
-	23, // 47: github.com.akuity.kargo.pkg.api.v1alpha1.StageStatus.current_promotion:type_name -> github.com.akuity.kargo.pkg.api.v1alpha1.PromotionInfo
-	39, // 48: github.com.akuity.kargo.pkg.api.v1alpha1.Subscriptions.upstream_stages:type_name -> github.com.akuity.kargo.pkg.api.v1alpha1.StageSubscription
-	45, // 49: github.com.akuity.kargo.pkg.api.v1alpha1.Warehouse.metadata:type_name -> github.com.akuity.kargo.pkg.api.metav1.ObjectMeta
-	42, // 50: github.com.akuity.kargo.pkg.api.v1alpha1.Warehouse.spec:type_name -> github.com.akuity.kargo.pkg.api.v1alpha1.WarehouseSpec
-	43, // 51: github.com.akuity.kargo.pkg.api.v1alpha1.Warehouse.status:type_name -> github.com.akuity.kargo.pkg.api.v1alpha1.WarehouseStatus
-	30, // 52: github.com.akuity.kargo.pkg.api.v1alpha1.WarehouseSpec.subscriptions:type_name -> github.com.akuity.kargo.pkg.api.v1alpha1.RepoSubscription
-	36, // 53: github.com.akuity.kargo.pkg.api.v1alpha1.FreightStatus.QualificationsEntry.value:type_name -> github.com.akuity.kargo.pkg.api.v1alpha1.Qualification
-	54, // [54:54] is the sub-list for method output_type
-	54, // [54:54] is the sub-list for method input_type
-	54, // [54:54] is the sub-list for extension type_name
-	54, // [54:54] is the sub-list for extension extendee
-	0,  // [0:54] is the sub-list for field type_name
+	45, // 39: github.com.akuity.kargo.pkg.api.v1alpha1.FreightStatus.verified_in:type_name -> github.com.akuity.kargo.pkg.api.v1alpha1.FreightStatus.VerifiedInEntry
+	46, // 40: github.com.akuity.kargo.pkg.api.v1alpha1.FreightStatus.approved_for:type_name -> github.com.akuity.kargo.pkg.api.v1alpha1.FreightStatus.ApprovedForEntry
+	49, // 41: github.com.akuity.kargo.pkg.api.v1alpha1.SimpleFreight.first_seen:type_name -> google.protobuf.Timestamp
+	8,  // 42: github.com.akuity.kargo.pkg.api.v1alpha1.SimpleFreight.commits:type_name -> github.com.akuity.kargo.pkg.api.v1alpha1.GitCommit
+	18, // 43: github.com.akuity.kargo.pkg.api.v1alpha1.SimpleFreight.images:type_name -> github.com.akuity.kargo.pkg.api.v1alpha1.Image
+	6,  // 44: github.com.akuity.kargo.pkg.api.v1alpha1.SimpleFreight.charts:type_name -> github.com.akuity.kargo.pkg.api.v1alpha1.Chart
+	38, // 45: github.com.akuity.kargo.pkg.api.v1alpha1.StageStatus.current_freight:type_name -> github.com.akuity.kargo.pkg.api.v1alpha1.SimpleFreight
+	38, // 46: github.com.akuity.kargo.pkg.api.v1alpha1.StageStatus.history:type_name -> github.com.akuity.kargo.pkg.api.v1alpha1.SimpleFreight
+	11, // 47: github.com.akuity.kargo.pkg.api.v1alpha1.StageStatus.health:type_name -> github.com.akuity.kargo.pkg.api.v1alpha1.Health
+	23, // 48: github.com.akuity.kargo.pkg.api.v1alpha1.StageStatus.current_promotion:type_name -> github.com.akuity.kargo.pkg.api.v1alpha1.PromotionInfo
+	40, // 49: github.com.akuity.kargo.pkg.api.v1alpha1.Subscriptions.upstream_stages:type_name -> github.com.akuity.kargo.pkg.api.v1alpha1.StageSubscription
+	47, // 50: github.com.akuity.kargo.pkg.api.v1alpha1.Warehouse.metadata:type_name -> github.com.akuity.kargo.pkg.api.metav1.ObjectMeta
+	43, // 51: github.com.akuity.kargo.pkg.api.v1alpha1.Warehouse.spec:type_name -> github.com.akuity.kargo.pkg.api.v1alpha1.WarehouseSpec
+	44, // 52: github.com.akuity.kargo.pkg.api.v1alpha1.Warehouse.status:type_name -> github.com.akuity.kargo.pkg.api.v1alpha1.WarehouseStatus
+	30, // 53: github.com.akuity.kargo.pkg.api.v1alpha1.WarehouseSpec.subscriptions:type_name -> github.com.akuity.kargo.pkg.api.v1alpha1.RepoSubscription
+	36, // 54: github.com.akuity.kargo.pkg.api.v1alpha1.FreightStatus.VerifiedInEntry.value:type_name -> github.com.akuity.kargo.pkg.api.v1alpha1.VerifiedStage
+	37, // 55: github.com.akuity.kargo.pkg.api.v1alpha1.FreightStatus.ApprovedForEntry.value:type_name -> github.com.akuity.kargo.pkg.api.v1alpha1.ApprovedStage
+	56, // [56:56] is the sub-list for method output_type
+	56, // [56:56] is the sub-list for method input_type
+	56, // [56:56] is the sub-list for extension type_name
+	56, // [56:56] is the sub-list for extension extendee
+	0,  // [0:56] is the sub-list for field type_name
 }
 
 func init() { file_v1alpha1_types_proto_init() }
@@ -3867,7 +3932,7 @@ func file_v1alpha1_types_proto_init() {
 			}
 		}
 		file_v1alpha1_types_proto_msgTypes[36].Exporter = func(v interface{}, i int) interface{} {
-			switch v := v.(*Qualification); i {
+			switch v := v.(*VerifiedStage); i {
 			case 0:
 				return &v.state
 			case 1:
@@ -3879,7 +3944,7 @@ func file_v1alpha1_types_proto_init() {
 			}
 		}
 		file_v1alpha1_types_proto_msgTypes[37].Exporter = func(v interface{}, i int) interface{} {
-			switch v := v.(*SimpleFreight); i {
+			switch v := v.(*ApprovedStage); i {
 			case 0:
 				return &v.state
 			case 1:
@@ -3891,7 +3956,7 @@ func file_v1alpha1_types_proto_init() {
 			}
 		}
 		file_v1alpha1_types_proto_msgTypes[38].Exporter = func(v interface{}, i int) interface{} {
-			switch v := v.(*StageStatus); i {
+			switch v := v.(*SimpleFreight); i {
 			case 0:
 				return &v.state
 			case 1:
@@ -3903,7 +3968,7 @@ func file_v1alpha1_types_proto_init() {
 			}
 		}
 		file_v1alpha1_types_proto_msgTypes[39].Exporter = func(v interface{}, i int) interface{} {
-			switch v := v.(*StageSubscription); i {
+			switch v := v.(*StageStatus); i {
 			case 0:
 				return &v.state
 			case 1:
@@ -3915,7 +3980,7 @@ func file_v1alpha1_types_proto_init() {
 			}
 		}
 		file_v1alpha1_types_proto_msgTypes[40].Exporter = func(v interface{}, i int) interface{} {
-			switch v := v.(*Subscriptions); i {
+			switch v := v.(*StageSubscription); i {
 			case 0:
 				return &v.state
 			case 1:
@@ -3927,7 +3992,7 @@ func file_v1alpha1_types_proto_init() {
 			}
 		}
 		file_v1alpha1_types_proto_msgTypes[41].Exporter = func(v interface{}, i int) interface{} {
-			switch v := v.(*Warehouse); i {
+			switch v := v.(*Subscriptions); i {
 			case 0:
 				return &v.state
 			case 1:
@@ -3939,7 +4004,7 @@ func file_v1alpha1_types_proto_init() {
 			}
 		}
 		file_v1alpha1_types_proto_msgTypes[42].Exporter = func(v interface{}, i int) interface{} {
-			switch v := v.(*WarehouseSpec); i {
+			switch v := v.(*Warehouse); i {
 			case 0:
 				return &v.state
 			case 1:
@@ -3951,6 +4016,18 @@ func file_v1alpha1_types_proto_init() {
 			}
 		}
 		file_v1alpha1_types_proto_msgTypes[43].Exporter = func(v interface{}, i int) interface{} {
+			switch v := v.(*WarehouseSpec); i {
+			case 0:
+				return &v.state
+			case 1:
+				return &v.sizeCache
+			case 2:
+				return &v.unknownFields
+			default:
+				return nil
+			}
+		}
+		file_v1alpha1_types_proto_msgTypes[44].Exporter = func(v interface{}, i int) interface{} {
 			switch v := v.(*WarehouseStatus); i {
 			case 0:
 				return &v.state
@@ -3972,15 +4049,15 @@ func file_v1alpha1_types_proto_init() {
 	file_v1alpha1_types_proto_msgTypes[22].OneofWrappers = []interface{}{}
 	file_v1alpha1_types_proto_msgTypes[24].OneofWrappers = []interface{}{}
 	file_v1alpha1_types_proto_msgTypes[30].OneofWrappers = []interface{}{}
-	file_v1alpha1_types_proto_msgTypes[37].OneofWrappers = []interface{}{}
 	file_v1alpha1_types_proto_msgTypes[38].OneofWrappers = []interface{}{}
+	file_v1alpha1_types_proto_msgTypes[39].OneofWrappers = []interface{}{}
 	type x struct{}
 	out := protoimpl.TypeBuilder{
 		File: protoimpl.DescBuilder{
 			GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
 			RawDescriptor: file_v1alpha1_types_proto_rawDesc,
 			NumEnums:      0,
-			NumMessages:   45,
+			NumMessages:   47,
 			NumExtensions: 0,
 			NumServices:   0,
 		},
diff --git a/ui/package.json b/ui/package.json
index 6e04cb2e..4cb97535 100644
--- a/ui/package.json
+++ b/ui/package.json
@@ -16,15 +16,15 @@
   "devDependencies": {
     "@openapi-contrib/openapi-schema-to-json-schema": "^5.1.0",
     "@types/json-schema": "^7.0.15",
-    "@types/node": "^20.9.0",
+    "@types/node": "^20.9.2",
     "@types/react": "^18.2.37",
     "@types/react-dom": "^18.2.15",
     "@typescript-eslint/eslint-plugin": "^6.11.0",
     "@typescript-eslint/parser": "^6.11.0",
-    "@vitejs/plugin-react": "^4.1.1",
+    "@vitejs/plugin-react": "^4.2.0",
     "autoprefixer": "^10.4.16",
     "cssnano": "^6.0.1",
-    "eslint": "^8.53.0",
+    "eslint": "^8.54.0",
     "eslint-config-prettier": "^9.0.0",
     "eslint-plugin-import": "^2.29.0",
     "eslint-plugin-prettier": "^5.0.1",
@@ -36,7 +36,7 @@
     "prettier": "^3.1.0",
     "tailwindcss": "^3.3.5",
     "typescript": "^5.2.2",
-    "vite": "^4.5.0",
+    "vite": "^5.0.0",
     "vite-tsconfig-paths": "^4.2.1",
     "zx": "^7.2.3"
   },
@@ -54,7 +54,7 @@
     "@monaco-editor/react": "^4.6.0",
     "@tanstack/react-query": "^5.8.4",
     "@types/dagre": "^0.7.52",
-    "antd": "^5.11.1",
+    "antd": "^5.11.2",
     "classnames": "^2.3.2",
     "dagre": "^0.8.5",
     "date-fns": "^2.30.0",
@@ -64,7 +64,7 @@
     "react": "^18.2.0",
     "react-dom": "^18.2.0",
     "react-hook-form": "^7.48.2",
-    "react-router-dom": "^6.18.0",
+    "react-router-dom": "^6.19.0",
     "vite-plugin-monaco-editor": "^1.1.0",
     "vitest": "^0.34.6",
     "yaml": "^2.3.4",
diff --git a/ui/pnpm-lock.yaml b/ui/pnpm-lock.yaml
index b984605e..9ad64df3 100644
--- a/ui/pnpm-lock.yaml
+++ b/ui/pnpm-lock.yaml
@@ -38,8 +38,8 @@ dependencies:
     specifier: ^0.7.52
     version: 0.7.52
   antd:
-    specifier: ^5.11.1
-    version: 5.11.1(date-fns@2.30.0)(react-dom@18.2.0)(react@18.2.0)
+    specifier: ^5.11.2
+    version: 5.11.2(date-fns@2.30.0)(react-dom@18.2.0)(react@18.2.0)
   classnames:
     specifier: ^2.3.2
     version: 2.3.2
@@ -68,8 +68,8 @@ dependencies:
     specifier: ^7.48.2
     version: 7.48.2(react@18.2.0)
   react-router-dom:
-    specifier: ^6.18.0
-    version: 6.18.0(react-dom@18.2.0)(react@18.2.0)
+    specifier: ^6.19.0
+    version: 6.19.0(react-dom@18.2.0)(react@18.2.0)
   vite-plugin-monaco-editor:
     specifier: ^1.1.0
     version: 1.1.0(monaco-editor@0.44.0)
@@ -91,8 +91,8 @@ devDependencies:
     specifier: ^7.0.15
     version: 7.0.15
   '@types/node':
-    specifier: ^20.9.0
-    version: 20.9.0
+    specifier: ^20.9.2
+    version: 20.9.2
   '@types/react':
     specifier: ^18.2.37
     version: 18.2.37
@@ -101,13 +101,13 @@ devDependencies:
     version: 18.2.15
   '@typescript-eslint/eslint-plugin':
     specifier: ^6.11.0
-    version: 6.11.0(@typescript-eslint/parser@6.11.0)(eslint@8.53.0)(typescript@5.2.2)
+    version: 6.11.0(@typescript-eslint/parser@6.11.0)(eslint@8.54.0)(typescript@5.2.2)
   '@typescript-eslint/parser':
     specifier: ^6.11.0
-    version: 6.11.0(eslint@8.53.0)(typescript@5.2.2)
+    version: 6.11.0(eslint@8.54.0)(typescript@5.2.2)
   '@vitejs/plugin-react':
-    specifier: ^4.1.1
-    version: 4.1.1(vite@4.5.0)
+    specifier: ^4.2.0
+    version: 4.2.0(vite@5.0.0)
   autoprefixer:
     specifier: ^10.4.16
     version: 10.4.16(postcss@8.4.31)
@@ -115,23 +115,23 @@ devDependencies:
     specifier: ^6.0.1
     version: 6.0.1(postcss@8.4.31)
   eslint:
-    specifier: ^8.53.0
-    version: 8.53.0
+    specifier: ^8.54.0
+    version: 8.54.0
   eslint-config-prettier:
     specifier: ^9.0.0
-    version: 9.0.0(eslint@8.53.0)
+    version: 9.0.0(eslint@8.54.0)
   eslint-plugin-import:
     specifier: ^2.29.0
-    version: 2.29.0(@typescript-eslint/parser@6.11.0)(eslint@8.53.0)
+    version: 2.29.0(@typescript-eslint/parser@6.11.0)(eslint@8.54.0)
   eslint-plugin-prettier:
     specifier: ^5.0.1
-    version: 5.0.1(eslint-config-prettier@9.0.0)(eslint@8.53.0)(prettier@3.1.0)
+    version: 5.0.1(eslint-config-prettier@9.0.0)(eslint@8.54.0)(prettier@3.1.0)
   eslint-plugin-react:
     specifier: ^7.33.2
-    version: 7.33.2(eslint@8.53.0)
+    version: 7.33.2(eslint@8.54.0)
   eslint-plugin-react-refresh:
     specifier: ^0.4.4
-    version: 0.4.4(eslint@8.53.0)
+    version: 0.4.4(eslint@8.54.0)
   json-stable-stringify:
     specifier: ^1.1.0
     version: 1.1.0
@@ -151,11 +151,11 @@ devDependencies:
     specifier: ^5.2.2
     version: 5.2.2
   vite:
-    specifier: ^4.5.0
-    version: 4.5.0(@types/node@20.9.0)(less@4.2.0)
+    specifier: ^5.0.0
+    version: 5.0.0(@types/node@20.9.2)(less@4.2.0)
   vite-tsconfig-paths:
     specifier: ^4.2.1
-    version: 4.2.1(typescript@5.2.2)(vite@4.5.0)
+    version: 4.2.1(typescript@5.2.2)(vite@5.0.0)
   zx:
     specifier: ^7.2.3
     version: 7.2.3
@@ -520,189 +520,189 @@ packages:
     resolution: {integrity: sha512-OWORNpfjMsSSUBVrRBVGECkhWcULOAJz9ZW8uK9qgxD+87M7jHRcvh/A96XXNhXTLmKcoYSQtBEX7lHMO7YRwg==}
     dev: false
 
-  /@esbuild/android-arm64@0.18.20:
-    resolution: {integrity: sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==}
+  /@esbuild/android-arm64@0.19.7:
+    resolution: {integrity: sha512-YEDcw5IT7hW3sFKZBkCAQaOCJQLONVcD4bOyTXMZz5fr66pTHnAet46XAtbXAkJRfIn2YVhdC6R9g4xa27jQ1w==}
     engines: {node: '>=12'}
     cpu: [arm64]
     os: [android]
     requiresBuild: true
     optional: true
 
-  /@esbuild/android-arm@0.18.20:
-    resolution: {integrity: sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==}
+  /@esbuild/android-arm@0.19.7:
+    resolution: {integrity: sha512-YGSPnndkcLo4PmVl2tKatEn+0mlVMr3yEpOOT0BeMria87PhvoJb5dg5f5Ft9fbCVgtAz4pWMzZVgSEGpDAlww==}
     engines: {node: '>=12'}
     cpu: [arm]
     os: [android]
     requiresBuild: true
     optional: true
 
-  /@esbuild/android-x64@0.18.20:
-    resolution: {integrity: sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==}
+  /@esbuild/android-x64@0.19.7:
+    resolution: {integrity: sha512-jhINx8DEjz68cChFvM72YzrqfwJuFbfvSxZAk4bebpngGfNNRm+zRl4rtT9oAX6N9b6gBcFaJHFew5Blf6CvUw==}
     engines: {node: '>=12'}
     cpu: [x64]
     os: [android]
     requiresBuild: true
     optional: true
 
-  /@esbuild/darwin-arm64@0.18.20:
-    resolution: {integrity: sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==}
+  /@esbuild/darwin-arm64@0.19.7:
+    resolution: {integrity: sha512-dr81gbmWN//3ZnBIm6YNCl4p3pjnabg1/ZVOgz2fJoUO1a3mq9WQ/1iuEluMs7mCL+Zwv7AY5e3g1hjXqQZ9Iw==}
     engines: {node: '>=12'}
     cpu: [arm64]
     os: [darwin]
     requiresBuild: true
     optional: true
 
-  /@esbuild/darwin-x64@0.18.20:
-    resolution: {integrity: sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==}
+  /@esbuild/darwin-x64@0.19.7:
+    resolution: {integrity: sha512-Lc0q5HouGlzQEwLkgEKnWcSazqr9l9OdV2HhVasWJzLKeOt0PLhHaUHuzb8s/UIya38DJDoUm74GToZ6Wc7NGQ==}
     engines: {node: '>=12'}
     cpu: [x64]
     os: [darwin]
     requiresBuild: true
     optional: true
 
-  /@esbuild/freebsd-arm64@0.18.20:
-    resolution: {integrity: sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==}
+  /@esbuild/freebsd-arm64@0.19.7:
+    resolution: {integrity: sha512-+y2YsUr0CxDFF7GWiegWjGtTUF6gac2zFasfFkRJPkMAuMy9O7+2EH550VlqVdpEEchWMynkdhC9ZjtnMiHImQ==}
     engines: {node: '>=12'}
     cpu: [arm64]
     os: [freebsd]
     requiresBuild: true
     optional: true
 
-  /@esbuild/freebsd-x64@0.18.20:
-    resolution: {integrity: sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==}
+  /@esbuild/freebsd-x64@0.19.7:
+    resolution: {integrity: sha512-CdXOxIbIzPJmJhrpmJTLx+o35NoiKBIgOvmvT+jeSadYiWJn0vFKsl+0bSG/5lwjNHoIDEyMYc/GAPR9jxusTA==}
     engines: {node: '>=12'}
     cpu: [x64]
     os: [freebsd]
     requiresBuild: true
     optional: true
 
-  /@esbuild/linux-arm64@0.18.20:
-    resolution: {integrity: sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==}
+  /@esbuild/linux-arm64@0.19.7:
+    resolution: {integrity: sha512-inHqdOVCkUhHNvuQPT1oCB7cWz9qQ/Cz46xmVe0b7UXcuIJU3166aqSunsqkgSGMtUCWOZw3+KMwI6otINuC9g==}
     engines: {node: '>=12'}
     cpu: [arm64]
     os: [linux]
     requiresBuild: true
     optional: true
 
-  /@esbuild/linux-arm@0.18.20:
-    resolution: {integrity: sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==}
+  /@esbuild/linux-arm@0.19.7:
+    resolution: {integrity: sha512-Y+SCmWxsJOdQtjcBxoacn/pGW9HDZpwsoof0ttL+2vGcHokFlfqV666JpfLCSP2xLxFpF1lj7T3Ox3sr95YXww==}
     engines: {node: '>=12'}
     cpu: [arm]
     os: [linux]
     requiresBuild: true
     optional: true
 
-  /@esbuild/linux-ia32@0.18.20:
-    resolution: {integrity: sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==}
+  /@esbuild/linux-ia32@0.19.7:
+    resolution: {integrity: sha512-2BbiL7nLS5ZO96bxTQkdO0euGZIUQEUXMTrqLxKUmk/Y5pmrWU84f+CMJpM8+EHaBPfFSPnomEaQiG/+Gmh61g==}
     engines: {node: '>=12'}
     cpu: [ia32]
     os: [linux]
     requiresBuild: true
     optional: true
 
-  /@esbuild/linux-loong64@0.18.20:
-    resolution: {integrity: sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==}
+  /@esbuild/linux-loong64@0.19.7:
+    resolution: {integrity: sha512-BVFQla72KXv3yyTFCQXF7MORvpTo4uTA8FVFgmwVrqbB/4DsBFWilUm1i2Oq6zN36DOZKSVUTb16jbjedhfSHw==}
     engines: {node: '>=12'}
     cpu: [loong64]
     os: [linux]
     requiresBuild: true
     optional: true
 
-  /@esbuild/linux-mips64el@0.18.20:
-    resolution: {integrity: sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==}
+  /@esbuild/linux-mips64el@0.19.7:
+    resolution: {integrity: sha512-DzAYckIaK+pS31Q/rGpvUKu7M+5/t+jI+cdleDgUwbU7KdG2eC3SUbZHlo6Q4P1CfVKZ1lUERRFP8+q0ob9i2w==}
     engines: {node: '>=12'}
     cpu: [mips64el]
     os: [linux]
     requiresBuild: true
     optional: true
 
-  /@esbuild/linux-ppc64@0.18.20:
-    resolution: {integrity: sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==}
+  /@esbuild/linux-ppc64@0.19.7:
+    resolution: {integrity: sha512-JQ1p0SmUteNdUaaiRtyS59GkkfTW0Edo+e0O2sihnY4FoZLz5glpWUQEKMSzMhA430ctkylkS7+vn8ziuhUugQ==}
     engines: {node: '>=12'}
     cpu: [ppc64]
     os: [linux]
     requiresBuild: true
     optional: true
 
-  /@esbuild/linux-riscv64@0.18.20:
-    resolution: {integrity: sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==}
+  /@esbuild/linux-riscv64@0.19.7:
+    resolution: {integrity: sha512-xGwVJ7eGhkprY/nB7L7MXysHduqjpzUl40+XoYDGC4UPLbnG+gsyS1wQPJ9lFPcxYAaDXbdRXd1ACs9AE9lxuw==}
     engines: {node: '>=12'}
     cpu: [riscv64]
     os: [linux]
     requiresBuild: true
     optional: true
 
-  /@esbuild/linux-s390x@0.18.20:
-    resolution: {integrity: sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==}
+  /@esbuild/linux-s390x@0.19.7:
+    resolution: {integrity: sha512-U8Rhki5PVU0L0nvk+E8FjkV8r4Lh4hVEb9duR6Zl21eIEYEwXz8RScj4LZWA2i3V70V4UHVgiqMpszXvG0Yqhg==}
     engines: {node: '>=12'}
     cpu: [s390x]
     os: [linux]
     requiresBuild: true
     optional: true
 
-  /@esbuild/linux-x64@0.18.20:
-    resolution: {integrity: sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==}
+  /@esbuild/linux-x64@0.19.7:
+    resolution: {integrity: sha512-ZYZopyLhm4mcoZXjFt25itRlocKlcazDVkB4AhioiL9hOWhDldU9n38g62fhOI4Pth6vp+Mrd5rFKxD0/S+7aQ==}
     engines: {node: '>=12'}
     cpu: [x64]
     os: [linux]
     requiresBuild: true
     optional: true
 
-  /@esbuild/netbsd-x64@0.18.20:
-    resolution: {integrity: sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==}
+  /@esbuild/netbsd-x64@0.19.7:
+    resolution: {integrity: sha512-/yfjlsYmT1O3cum3J6cmGG16Fd5tqKMcg5D+sBYLaOQExheAJhqr8xOAEIuLo8JYkevmjM5zFD9rVs3VBcsjtQ==}
     engines: {node: '>=12'}
     cpu: [x64]
     os: [netbsd]
     requiresBuild: true
     optional: true
 
-  /@esbuild/openbsd-x64@0.18.20:
-    resolution: {integrity: sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==}
+  /@esbuild/openbsd-x64@0.19.7:
+    resolution: {integrity: sha512-MYDFyV0EW1cTP46IgUJ38OnEY5TaXxjoDmwiTXPjezahQgZd+j3T55Ht8/Q9YXBM0+T9HJygrSRGV5QNF/YVDQ==}
     engines: {node: '>=12'}
     cpu: [x64]
     os: [openbsd]
     requiresBuild: true
     optional: true
 
-  /@esbuild/sunos-x64@0.18.20:
-    resolution: {integrity: sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==}
+  /@esbuild/sunos-x64@0.19.7:
+    resolution: {integrity: sha512-JcPvgzf2NN/y6X3UUSqP6jSS06V0DZAV/8q0PjsZyGSXsIGcG110XsdmuWiHM+pno7/mJF6fjH5/vhUz/vA9fw==}
     engines: {node: '>=12'}
     cpu: [x64]
     os: [sunos]
     requiresBuild: true
     optional: true
 
-  /@esbuild/win32-arm64@0.18.20:
-    resolution: {integrity: sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==}
+  /@esbuild/win32-arm64@0.19.7:
+    resolution: {integrity: sha512-ZA0KSYti5w5toax5FpmfcAgu3ZNJxYSRm0AW/Dao5up0YV1hDVof1NvwLomjEN+3/GMtaWDI+CIyJOMTRSTdMw==}
     engines: {node: '>=12'}
     cpu: [arm64]
     os: [win32]
     requiresBuild: true
     optional: true
 
-  /@esbuild/win32-ia32@0.18.20:
-    resolution: {integrity: sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==}
+  /@esbuild/win32-ia32@0.19.7:
+    resolution: {integrity: sha512-CTOnijBKc5Jpk6/W9hQMMvJnsSYRYgveN6O75DTACCY18RA2nqka8dTZR+x/JqXCRiKk84+5+bRKXUSbbwsS0A==}
     engines: {node: '>=12'}
     cpu: [ia32]
     os: [win32]
     requiresBuild: true
     optional: true
 
-  /@esbuild/win32-x64@0.18.20:
-    resolution: {integrity: sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==}
+  /@esbuild/win32-x64@0.19.7:
+    resolution: {integrity: sha512-gRaP2sk6hc98N734luX4VpF318l3w+ofrtTu9j5L8EQXF+FzQKV6alCOHMVoJJHvVK/mGbwBXfOL1HETQu9IGQ==}
     engines: {node: '>=12'}
     cpu: [x64]
     os: [win32]
     requiresBuild: true
     optional: true
 
-  /@eslint-community/eslint-utils@4.4.0(eslint@8.53.0):
+  /@eslint-community/eslint-utils@4.4.0(eslint@8.54.0):
     resolution: {integrity: sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==}
     engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
     peerDependencies:
       eslint: ^6.0.0 || ^7.0.0 || >=8.0.0
     dependencies:
-      eslint: 8.53.0
+      eslint: 8.54.0
       eslint-visitor-keys: 3.4.3
     dev: true
 
@@ -719,7 +719,7 @@ packages:
       debug: 4.3.4
       espree: 9.6.1
       globals: 13.23.0
-      ignore: 5.2.4
+      ignore: 5.3.0
       import-fresh: 3.3.0
       js-yaml: 4.1.0
       minimatch: 3.1.2
@@ -728,8 +728,8 @@ packages:
       - supports-color
     dev: true
 
-  /@eslint/js@8.53.0:
-    resolution: {integrity: sha512-Kn7K8dx/5U6+cT1yEhpX1w4PCSg0M+XyRILPgvwcEBjerFWCwQj5sbr3/VmxqV0JGHCBCzyd6LxypEuehypY1w==}
+  /@eslint/js@8.54.0:
+    resolution: {integrity: sha512-ut5V+D+fOoWPgGGNj83GGjnntO39xDy6DWxO0wb7Jp3DcMX0TfIqdzHF85VTQkerdyGmuuMD9AKAo5KiNlf/AQ==}
     engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
     dev: true
 
@@ -893,7 +893,7 @@ packages:
     dependencies:
       '@types/json-schema': 7.0.15
       '@types/lodash': 4.14.195
-      '@types/node': 20.9.0
+      '@types/node': 20.9.2
       fast-deep-equal: 3.1.3
       lodash: 4.17.21
       openapi-typescript: 5.4.1
@@ -1006,11 +1006,95 @@ packages:
       react-dom: 18.2.0(react@18.2.0)
     dev: false
 
-  /@remix-run/router@1.11.0:
-    resolution: {integrity: sha512-BHdhcWgeiudl91HvVa2wxqZjSHbheSgIiDvxrF1VjFzBzpTtuDPkOdOi3Iqvc08kXtFkLjhbS+ML9aM8mJS+wQ==}
+  /@remix-run/router@1.12.0:
+    resolution: {integrity: sha512-2hXv036Bux90e1GXTWSMfNzfDDK8LA8JYEWfyHxzvwdp6GyoWEovKc9cotb3KCKmkdwsIBuFGX7ScTWyiHv7Eg==}
     engines: {node: '>=14.0.0'}
     dev: false
 
+  /@rollup/rollup-android-arm-eabi@4.5.0:
+    resolution: {integrity: sha512-OINaBGY+Wc++U0rdr7BLuFClxcoWaVW3vQYqmQq6B3bqQ/2olkaoz+K8+af/Mmka/C2yN5j+L9scBkv4BtKsDA==}
+    cpu: [arm]
+    os: [android]
+    requiresBuild: true
+    optional: true
+
+  /@rollup/rollup-android-arm64@4.5.0:
+    resolution: {integrity: sha512-UdMf1pOQc4ZmUA/NTmKhgJTBimbSKnhPS2zJqucqFyBRFPnPDtwA8MzrGNTjDeQbIAWfpJVAlxejw+/lQyBK/w==}
+    cpu: [arm64]
+    os: [android]
+    requiresBuild: true
+    optional: true
+
+  /@rollup/rollup-darwin-arm64@4.5.0:
+    resolution: {integrity: sha512-L0/CA5p/idVKI+c9PcAPGorH6CwXn6+J0Ys7Gg1axCbTPgI8MeMlhA6fLM9fK+ssFhqogMHFC8HDvZuetOii7w==}
+    cpu: [arm64]
+    os: [darwin]
+    requiresBuild: true
+    optional: true
+
+  /@rollup/rollup-darwin-x64@4.5.0:
+    resolution: {integrity: sha512-QZCbVqU26mNlLn8zi/XDDquNmvcr4ON5FYAHQQsyhrHx8q+sQi/6xduoznYXwk/KmKIXG5dLfR0CvY+NAWpFYQ==}
+    cpu: [x64]
+    os: [darwin]
+    requiresBuild: true
+    optional: true
+
+  /@rollup/rollup-linux-arm-gnueabihf@4.5.0:
+    resolution: {integrity: sha512-VpSQ+xm93AeV33QbYslgf44wc5eJGYfYitlQzAi3OObu9iwrGXEnmu5S3ilkqE3Pr/FkgOiJKV/2p0ewf4Hrtg==}
+    cpu: [arm]
+    os: [linux]
+    requiresBuild: true
+    optional: true
+
+  /@rollup/rollup-linux-arm64-gnu@4.5.0:
+    resolution: {integrity: sha512-OrEyIfpxSsMal44JpEVx9AEcGpdBQG1ZuWISAanaQTSMeStBW+oHWwOkoqR54bw3x8heP8gBOyoJiGg+fLY8qQ==}
+    cpu: [arm64]
+    os: [linux]
+    requiresBuild: true
+    optional: true
+
+  /@rollup/rollup-linux-arm64-musl@4.5.0:
+    resolution: {integrity: sha512-1H7wBbQuE6igQdxMSTjtFfD+DGAudcYWhp106z/9zBA8OQhsJRnemO4XGavdzHpGhRtRxbgmUGdO3YQgrWf2RA==}
+    cpu: [arm64]
+    os: [linux]
+    requiresBuild: true
+    optional: true
+
+  /@rollup/rollup-linux-x64-gnu@4.5.0:
+    resolution: {integrity: sha512-FVyFI13tXw5aE65sZdBpNjPVIi4Q5mARnL/39UIkxvSgRAIqCo5sCpCELk0JtXHGee2owZz5aNLbWNfBHzr71Q==}
+    cpu: [x64]
+    os: [linux]
+    requiresBuild: true
+    optional: true
+
+  /@rollup/rollup-linux-x64-musl@4.5.0:
+    resolution: {integrity: sha512-eBPYl2sLpH/o8qbSz6vPwWlDyThnQjJfcDOGFbNjmjb44XKC1F5dQfakOsADRVrXCNzM6ZsSIPDG5dc6HHLNFg==}
+    cpu: [x64]
+    os: [linux]
+    requiresBuild: true
+    optional: true
+
+  /@rollup/rollup-win32-arm64-msvc@4.5.0:
+    resolution: {integrity: sha512-xaOHIfLOZypoQ5U2I6rEaugS4IYtTgP030xzvrBf5js7p9WI9wik07iHmsKaej8Z83ZDxN5GyypfoyKV5O5TJA==}
+    cpu: [arm64]
+    os: [win32]
+    requiresBuild: true
+    optional: true
+
+  /@rollup/rollup-win32-ia32-msvc@4.5.0:
+    resolution: {integrity: sha512-Al6quztQUrHwcOoU2TuFblUQ5L+/AmPBXFR6dUvyo4nRj2yQRK0WIUaGMF/uwKulvRcXkpHe3k9A8Vf93VDktA==}
+    cpu: [ia32]
+    os: [win32]
+    requiresBuild: true
+    optional: true
+
+  /@rollup/rollup-win32-x64-msvc@4.5.0:
+    resolution: {integrity: sha512-8kdW+brNhI/NzJ4fxDufuJUjepzINqJKLGHuxyAtpPG9bMbn8P5mtaCcbOm0EzLJ+atg+kF9dwg8jpclkVqx5w==}
+    cpu: [x64]
+    os: [win32]
+    requiresBuild: true
+    optional: true
+
   /@sinclair/typebox@0.27.8:
     resolution: {integrity: sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==}
     dev: false
@@ -1088,7 +1172,7 @@ packages:
     resolution: {integrity: sha512-MxObHvNl4A69ofaTRU8DFqvgzzv8s9yRtaPPm5gud9HDNvpB3GPQFvNuTWAI59B9huVGV5jXYJwbCsmBsOGYWA==}
     dependencies:
       '@types/jsonfile': 6.1.1
-      '@types/node': 20.9.0
+      '@types/node': 20.9.2
     dev: true
 
   /@types/json-schema@7.0.15:
@@ -1101,7 +1185,7 @@ packages:
   /@types/jsonfile@6.1.1:
     resolution: {integrity: sha512-GSgiRCVeapDN+3pqA35IkQwasaCh/0YFH5dEF6S88iDvEn901DjOeH3/QPY+XYP1DFzDZPvIvfeEgk+7br5png==}
     dependencies:
-      '@types/node': 20.9.0
+      '@types/node': 20.9.2
     dev: true
 
   /@types/lodash@4.14.195:
@@ -1112,14 +1196,14 @@ packages:
     resolution: {integrity: sha512-jhuKLIRrhvCPLqwPcx6INqmKeiA5EWrsCOPhrlFSrbrmU4ZMPjj5Ul/oLCMDO98XRUIwVm78xICz4EPCektzeQ==}
     dev: true
 
-  /@types/node@18.18.9:
-    resolution: {integrity: sha512-0f5klcuImLnG4Qreu9hPj/rEfFq6YRc5n2mAjSsH+ec/mJL+3voBH0+8T7o8RpFjH7ovc+TRsL/c7OYIQsPTfQ==}
+  /@types/node@18.18.10:
+    resolution: {integrity: sha512-luANqZxPmjTll8bduz4ACs/lNTCLuWssCyjqTY9yLdsv1xnViQp3ISKwsEWOIecO13JWUqjVdig/Vjjc09o8uA==}
     dependencies:
       undici-types: 5.26.5
     dev: true
 
-  /@types/node@20.9.0:
-    resolution: {integrity: sha512-nekiGu2NDb1BcVofVcEKMIwzlx4NjHlcjhoxxKBNLtz15Y1z7MYf549DFvkHSId02Ax6kGwWntIBPC3l/JZcmw==}
+  /@types/node@20.9.2:
+    resolution: {integrity: sha512-WHZXKFCEyIUJzAwh3NyyTHYSR35SevJ6mZ1nWwJafKtiQbqRTIKSRcw3Ma3acqgsent3RRDqeVwpHntMk+9irg==}
     dependencies:
       undici-types: 5.26.5
 
@@ -1157,7 +1241,7 @@ packages:
     resolution: {integrity: sha512-ASCxdbsrwNfSMXALlC3Decif9rwDMu+80KGp5zI2RLRotfMsTv7fHL8W8VDp24wymzDyIFudhUeSCugrgRFfHQ==}
     dev: true
 
-  /@typescript-eslint/eslint-plugin@6.11.0(@typescript-eslint/parser@6.11.0)(eslint@8.53.0)(typescript@5.2.2):
+  /@typescript-eslint/eslint-plugin@6.11.0(@typescript-eslint/parser@6.11.0)(eslint@8.54.0)(typescript@5.2.2):
     resolution: {integrity: sha512-uXnpZDc4VRjY4iuypDBKzW1rz9T5YBBK0snMn8MaTSNd2kMlj50LnLBABELjJiOL5YHk7ZD8hbSpI9ubzqYI0w==}
     engines: {node: ^16.0.0 || >=18.0.0}
     peerDependencies:
@@ -1169,13 +1253,13 @@ packages:
         optional: true
     dependencies:
       '@eslint-community/regexpp': 4.10.0
-      '@typescript-eslint/parser': 6.11.0(eslint@8.53.0)(typescript@5.2.2)
+      '@typescript-eslint/parser': 6.11.0(eslint@8.54.0)(typescript@5.2.2)
       '@typescript-eslint/scope-manager': 6.11.0
-      '@typescript-eslint/type-utils': 6.11.0(eslint@8.53.0)(typescript@5.2.2)
-      '@typescript-eslint/utils': 6.11.0(eslint@8.53.0)(typescript@5.2.2)
+      '@typescript-eslint/type-utils': 6.11.0(eslint@8.54.0)(typescript@5.2.2)
+      '@typescript-eslint/utils': 6.11.0(eslint@8.54.0)(typescript@5.2.2)
       '@typescript-eslint/visitor-keys': 6.11.0
       debug: 4.3.4
-      eslint: 8.53.0
+      eslint: 8.54.0
       graphemer: 1.4.0
       ignore: 5.2.4
       natural-compare: 1.4.0
@@ -1186,7 +1270,7 @@ packages:
       - supports-color
     dev: true
 
-  /@typescript-eslint/parser@6.11.0(eslint@8.53.0)(typescript@5.2.2):
+  /@typescript-eslint/parser@6.11.0(eslint@8.54.0)(typescript@5.2.2):
     resolution: {integrity: sha512-+whEdjk+d5do5nxfxx73oanLL9ghKO3EwM9kBCkUtWMRwWuPaFv9ScuqlYfQ6pAD6ZiJhky7TZ2ZYhrMsfMxVQ==}
     engines: {node: ^16.0.0 || >=18.0.0}
     peerDependencies:
@@ -1201,7 +1285,7 @@ packages:
       '@typescript-eslint/typescript-estree': 6.11.0(typescript@5.2.2)
       '@typescript-eslint/visitor-keys': 6.11.0
       debug: 4.3.4
-      eslint: 8.53.0
+      eslint: 8.54.0
       typescript: 5.2.2
     transitivePeerDependencies:
       - supports-color
@@ -1215,7 +1299,7 @@ packages:
       '@typescript-eslint/visitor-keys': 6.11.0
     dev: true
 
-  /@typescript-eslint/type-utils@6.11.0(eslint@8.53.0)(typescript@5.2.2):
+  /@typescript-eslint/type-utils@6.11.0(eslint@8.54.0)(typescript@5.2.2):
     resolution: {integrity: sha512-nA4IOXwZtqBjIoYrJcYxLRO+F9ri+leVGoJcMW1uqr4r1Hq7vW5cyWrA43lFbpRvQ9XgNrnfLpIkO3i1emDBIA==}
     engines: {node: ^16.0.0 || >=18.0.0}
     peerDependencies:
@@ -1226,9 +1310,9 @@ packages:
         optional: true
     dependencies:
       '@typescript-eslint/typescript-estree': 6.11.0(typescript@5.2.2)
-      '@typescript-eslint/utils': 6.11.0(eslint@8.53.0)(typescript@5.2.2)
+      '@typescript-eslint/utils': 6.11.0(eslint@8.54.0)(typescript@5.2.2)
       debug: 4.3.4
-      eslint: 8.53.0
+      eslint: 8.54.0
       ts-api-utils: 1.0.3(typescript@5.2.2)
       typescript: 5.2.2
     transitivePeerDependencies:
@@ -1261,19 +1345,19 @@ packages:
       - supports-color
     dev: true
 
-  /@typescript-eslint/utils@6.11.0(eslint@8.53.0)(typescript@5.2.2):
+  /@typescript-eslint/utils@6.11.0(eslint@8.54.0)(typescript@5.2.2):
     resolution: {integrity: sha512-p23ibf68fxoZy605dc0dQAEoUsoiNoP3MD9WQGiHLDuTSOuqoTsa4oAy+h3KDkTcxbbfOtUjb9h3Ta0gT4ug2g==}
     engines: {node: ^16.0.0 || >=18.0.0}
     peerDependencies:
       eslint: ^7.0.0 || ^8.0.0
     dependencies:
-      '@eslint-community/eslint-utils': 4.4.0(eslint@8.53.0)
+      '@eslint-community/eslint-utils': 4.4.0(eslint@8.54.0)
       '@types/json-schema': 7.0.15
       '@types/semver': 7.5.5
       '@typescript-eslint/scope-manager': 6.11.0
       '@typescript-eslint/types': 6.11.0
       '@typescript-eslint/typescript-estree': 6.11.0(typescript@5.2.2)
-      eslint: 8.53.0
+      eslint: 8.54.0
       semver: 7.5.4
     transitivePeerDependencies:
       - supports-color
@@ -1292,18 +1376,18 @@ packages:
     resolution: {integrity: sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==}
     dev: true
 
-  /@vitejs/plugin-react@4.1.1(vite@4.5.0):
-    resolution: {integrity: sha512-Jie2HERK+uh27e+ORXXwEP5h0Y2lS9T2PRGbfebiHGlwzDO0dEnd2aNtOR/qjBlPb1YgxwAONeblL1xqLikLag==}
+  /@vitejs/plugin-react@4.2.0(vite@5.0.0):
+    resolution: {integrity: sha512-+MHTH/e6H12kRp5HUkzOGqPMksezRMmW+TNzlh/QXfI8rRf6l2Z2yH/v12no1UvTwhZgEDMuQ7g7rrfMseU6FQ==}
     engines: {node: ^14.18.0 || >=16.0.0}
     peerDependencies:
-      vite: ^4.2.0
+      vite: ^4.2.0 || ^5.0.0
     dependencies:
       '@babel/core': 7.23.3
       '@babel/plugin-transform-react-jsx-self': 7.23.3(@babel/core@7.23.3)
       '@babel/plugin-transform-react-jsx-source': 7.23.3(@babel/core@7.23.3)
       '@types/babel__core': 7.20.4
       react-refresh: 0.14.0
-      vite: 4.5.0(@types/node@20.9.0)(less@4.2.0)
+      vite: 5.0.0(@types/node@20.9.2)(less@4.2.0)
     transitivePeerDependencies:
       - supports-color
     dev: true
@@ -1404,8 +1488,8 @@ packages:
     engines: {node: '>=10'}
     dev: false
 
-  /antd@5.11.1(date-fns@2.30.0)(react-dom@18.2.0)(react@18.2.0):
-    resolution: {integrity: sha512-X7Gk1TtJtM9goTrmTPVserqcP7crtV2TX/pbxgqyHpMZRrnBy0EI3h4ngrW+zsIAW8/iqpgrYsG/NVUel6xkBA==}
+  /antd@5.11.2(date-fns@2.30.0)(react-dom@18.2.0)(react@18.2.0):
+    resolution: {integrity: sha512-7/yqmfXpShHH0MJQOgv3vX9PUFwctyBm/G5L0i/S4AQy20ON6ZZ2UkjmWxgwg3vq2CEHKyVGTHozpH9WwDizgw==}
     peerDependencies:
       react: '>=16.9.0'
       react-dom: '>=16.9.0'
@@ -1448,7 +1532,7 @@ packages:
       rc-slider: 10.4.0(react-dom@18.2.0)(react@18.2.0)
       rc-steps: 6.0.1(react-dom@18.2.0)(react@18.2.0)
       rc-switch: 4.1.0(react-dom@18.2.0)(react@18.2.0)
-      rc-table: 7.35.2(react-dom@18.2.0)(react@18.2.0)
+      rc-table: 7.36.0(react-dom@18.2.0)(react@18.2.0)
       rc-tabs: 12.13.1(react-dom@18.2.0)(react@18.2.0)
       rc-textarea: 1.5.3(react-dom@18.2.0)(react@18.2.0)
       rc-tooltip: 6.1.2(react-dom@18.2.0)(react@18.2.0)
@@ -2215,34 +2299,34 @@ packages:
       is-symbol: 1.0.4
     dev: true
 
-  /esbuild@0.18.20:
-    resolution: {integrity: sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==}
+  /esbuild@0.19.7:
+    resolution: {integrity: sha512-6brbTZVqxhqgbpqBR5MzErImcpA0SQdoKOkcWK/U30HtQxnokIpG3TX2r0IJqbFUzqLjhU/zC1S5ndgakObVCQ==}
     engines: {node: '>=12'}
     hasBin: true
     requiresBuild: true
     optionalDependencies:
-      '@esbuild/android-arm': 0.18.20
-      '@esbuild/android-arm64': 0.18.20
-      '@esbuild/android-x64': 0.18.20
-      '@esbuild/darwin-arm64': 0.18.20
-      '@esbuild/darwin-x64': 0.18.20
-      '@esbuild/freebsd-arm64': 0.18.20
-      '@esbuild/freebsd-x64': 0.18.20
-      '@esbuild/linux-arm': 0.18.20
-      '@esbuild/linux-arm64': 0.18.20
-      '@esbuild/linux-ia32': 0.18.20
-      '@esbuild/linux-loong64': 0.18.20
-      '@esbuild/linux-mips64el': 0.18.20
-      '@esbuild/linux-ppc64': 0.18.20
-      '@esbuild/linux-riscv64': 0.18.20
-      '@esbuild/linux-s390x': 0.18.20
-      '@esbuild/linux-x64': 0.18.20
-      '@esbuild/netbsd-x64': 0.18.20
-      '@esbuild/openbsd-x64': 0.18.20
-      '@esbuild/sunos-x64': 0.18.20
-      '@esbuild/win32-arm64': 0.18.20
-      '@esbuild/win32-ia32': 0.18.20
-      '@esbuild/win32-x64': 0.18.20
+      '@esbuild/android-arm': 0.19.7
+      '@esbuild/android-arm64': 0.19.7
+      '@esbuild/android-x64': 0.19.7
+      '@esbuild/darwin-arm64': 0.19.7
+      '@esbuild/darwin-x64': 0.19.7
+      '@esbuild/freebsd-arm64': 0.19.7
+      '@esbuild/freebsd-x64': 0.19.7
+      '@esbuild/linux-arm': 0.19.7
+      '@esbuild/linux-arm64': 0.19.7
+      '@esbuild/linux-ia32': 0.19.7
+      '@esbuild/linux-loong64': 0.19.7
+      '@esbuild/linux-mips64el': 0.19.7
+      '@esbuild/linux-ppc64': 0.19.7
+      '@esbuild/linux-riscv64': 0.19.7
+      '@esbuild/linux-s390x': 0.19.7
+      '@esbuild/linux-x64': 0.19.7
+      '@esbuild/netbsd-x64': 0.19.7
+      '@esbuild/openbsd-x64': 0.19.7
+      '@esbuild/sunos-x64': 0.19.7
+      '@esbuild/win32-arm64': 0.19.7
+      '@esbuild/win32-ia32': 0.19.7
+      '@esbuild/win32-x64': 0.19.7
 
   /escalade@3.1.1:
     resolution: {integrity: sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==}
@@ -2259,13 +2343,13 @@ packages:
     engines: {node: '>=10'}
     dev: true
 
-  /eslint-config-prettier@9.0.0(eslint@8.53.0):
+  /eslint-config-prettier@9.0.0(eslint@8.54.0):
     resolution: {integrity: sha512-IcJsTkJae2S35pRsRAwoCE+925rJJStOdkKnLVgtE+tEpqU0EVVM7OqrwxqgptKdX29NUwC82I5pXsGFIgSevw==}
     hasBin: true
     peerDependencies:
       eslint: '>=7.0.0'
     dependencies:
-      eslint: 8.53.0
+      eslint: 8.54.0
     dev: true
 
   /eslint-import-resolver-node@0.3.9:
@@ -2278,7 +2362,7 @@ packages:
       - supports-color
     dev: true
 
-  /eslint-module-utils@2.8.0(@typescript-eslint/parser@6.11.0)(eslint-import-resolver-node@0.3.9)(eslint@8.53.0):
+  /eslint-module-utils@2.8.0(@typescript-eslint/parser@6.11.0)(eslint-import-resolver-node@0.3.9)(eslint@8.54.0):
     resolution: {integrity: sha512-aWajIYfsqCKRDgUfjEXNN/JlrzauMuSEy5sbd7WXbtW3EH6A6MpwEh42c7qD+MqQo9QMJ6fWLAeIJynx0g6OAw==}
     engines: {node: '>=4'}
     peerDependencies:
@@ -2299,15 +2383,15 @@ packages:
       eslint-import-resolver-webpack:
         optional: true
     dependencies:
-      '@typescript-eslint/parser': 6.11.0(eslint@8.53.0)(typescript@5.2.2)
+      '@typescript-eslint/parser': 6.11.0(eslint@8.54.0)(typescript@5.2.2)
       debug: 3.2.7
-      eslint: 8.53.0
+      eslint: 8.54.0
       eslint-import-resolver-node: 0.3.9
     transitivePeerDependencies:
       - supports-color
     dev: true
 
-  /eslint-plugin-import@2.29.0(@typescript-eslint/parser@6.11.0)(eslint@8.53.0):
+  /eslint-plugin-import@2.29.0(@typescript-eslint/parser@6.11.0)(eslint@8.54.0):
     resolution: {integrity: sha512-QPOO5NO6Odv5lpoTkddtutccQjysJuFxoPS7fAHO+9m9udNHvTCPSAMW9zGAYj8lAIdr40I8yPCdUYrncXtrwg==}
     engines: {node: '>=4'}
     peerDependencies:
@@ -2317,16 +2401,16 @@ packages:
       '@typescript-eslint/parser':
         optional: true
     dependencies:
-      '@typescript-eslint/parser': 6.11.0(eslint@8.53.0)(typescript@5.2.2)
+      '@typescript-eslint/parser': 6.11.0(eslint@8.54.0)(typescript@5.2.2)
       array-includes: 3.1.7
       array.prototype.findlastindex: 1.2.3
       array.prototype.flat: 1.3.2
       array.prototype.flatmap: 1.3.2
       debug: 3.2.7
       doctrine: 2.1.0
-      eslint: 8.53.0
+      eslint: 8.54.0
       eslint-import-resolver-node: 0.3.9
-      eslint-module-utils: 2.8.0(@typescript-eslint/parser@6.11.0)(eslint-import-resolver-node@0.3.9)(eslint@8.53.0)
+      eslint-module-utils: 2.8.0(@typescript-eslint/parser@6.11.0)(eslint-import-resolver-node@0.3.9)(eslint@8.54.0)
       hasown: 2.0.0
       is-core-module: 2.13.1
       is-glob: 4.0.3
@@ -2342,7 +2426,7 @@ packages:
       - supports-color
     dev: true
 
-  /eslint-plugin-prettier@5.0.1(eslint-config-prettier@9.0.0)(eslint@8.53.0)(prettier@3.1.0):
+  /eslint-plugin-prettier@5.0.1(eslint-config-prettier@9.0.0)(eslint@8.54.0)(prettier@3.1.0):
     resolution: {integrity: sha512-m3u5RnR56asrwV/lDC4GHorlW75DsFfmUcjfCYylTUs85dBRnB7VM6xG8eCMJdeDRnppzmxZVf1GEPJvl1JmNg==}
     engines: {node: ^14.18.0 || >=16.0.0}
     peerDependencies:
@@ -2356,22 +2440,22 @@ packages:
       eslint-config-prettier:
         optional: true
     dependencies:
-      eslint: 8.53.0
-      eslint-config-prettier: 9.0.0(eslint@8.53.0)
+      eslint: 8.54.0
+      eslint-config-prettier: 9.0.0(eslint@8.54.0)
       prettier: 3.1.0
       prettier-linter-helpers: 1.0.0
       synckit: 0.8.5
     dev: true
 
-  /eslint-plugin-react-refresh@0.4.4(eslint@8.53.0):
+  /eslint-plugin-react-refresh@0.4.4(eslint@8.54.0):
     resolution: {integrity: sha512-eD83+65e8YPVg6603Om2iCIwcQJf/y7++MWm4tACtEswFLYMwxwVWAfwN+e19f5Ad/FOyyNg9Dfi5lXhH3Y3rA==}
     peerDependencies:
       eslint: '>=7'
     dependencies:
-      eslint: 8.53.0
+      eslint: 8.54.0
     dev: true
 
-  /eslint-plugin-react@7.33.2(eslint@8.53.0):
+  /eslint-plugin-react@7.33.2(eslint@8.54.0):
     resolution: {integrity: sha512-73QQMKALArI8/7xGLNI/3LylrEYrlKZSb5C9+q3OtOewTnMQi5cT+aE9E41sLCmli3I9PGGmD1yiZydyo4FEPw==}
     engines: {node: '>=4'}
     peerDependencies:
@@ -2382,7 +2466,7 @@ packages:
       array.prototype.tosorted: 1.1.2
       doctrine: 2.1.0
       es-iterator-helpers: 1.0.15
-      eslint: 8.53.0
+      eslint: 8.54.0
       estraverse: 5.3.0
       jsx-ast-utils: 3.3.5
       minimatch: 3.1.2
@@ -2409,15 +2493,15 @@ packages:
     engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
     dev: true
 
-  /eslint@8.53.0:
-    resolution: {integrity: sha512-N4VuiPjXDUa4xVeV/GC/RV3hQW9Nw+Y463lkWaKKXKYMvmRiRDAtfpuPFLN+E1/6ZhyR8J2ig+eVREnYgUsiag==}
+  /eslint@8.54.0:
+    resolution: {integrity: sha512-NY0DfAkM8BIZDVl6PgSa1ttZbx3xHgJzSNJKYcQglem6CppHyMhRIQkBVSSMaSRnLhig3jsDbEzOjwCVt4AmmA==}
     engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
     hasBin: true
     dependencies:
-      '@eslint-community/eslint-utils': 4.4.0(eslint@8.53.0)
+      '@eslint-community/eslint-utils': 4.4.0(eslint@8.54.0)
       '@eslint-community/regexpp': 4.10.0
       '@eslint/eslintrc': 2.1.3
-      '@eslint/js': 8.53.0
+      '@eslint/js': 8.54.0
       '@humanwhocodes/config-array': 0.11.13
       '@humanwhocodes/module-importer': 1.0.1
       '@nodelib/fs.walk': 1.2.8
@@ -2439,7 +2523,7 @@ packages:
       glob-parent: 6.0.2
       globals: 13.23.0
       graphemer: 1.4.0
-      ignore: 5.2.4
+      ignore: 5.3.0
       imurmurhash: 0.1.4
       is-glob: 4.0.3
       is-path-inside: 3.0.3
@@ -2773,7 +2857,7 @@ packages:
       array-union: 2.1.0
       dir-glob: 3.0.1
       fast-glob: 3.3.2
-      ignore: 5.2.4
+      ignore: 5.3.0
       merge2: 1.4.1
       slash: 3.0.0
     dev: true
@@ -2784,7 +2868,7 @@ packages:
     dependencies:
       dir-glob: 3.0.1
       fast-glob: 3.3.2
-      ignore: 5.2.4
+      ignore: 5.3.0
       merge2: 1.4.1
       slash: 4.0.0
     dev: true
@@ -2880,6 +2964,11 @@ packages:
     engines: {node: '>= 4'}
     dev: true
 
+  /ignore@5.3.0:
+    resolution: {integrity: sha512-g7dmpshy+gD7mh88OC9NwSGTKoc3kyLAZQRU1mt53Aw/vnvfXnbC+F/7F7QoYVKbV+KNvJx8wArewKy1vXMtlg==}
+    engines: {node: '>= 4'}
+    dev: true
+
   /image-size@0.5.5:
     resolution: {integrity: sha512-6TDAlDPZxUFCv+fuOkIoXT/V/f3Qbq8e37p+YOiYrUv3v9cc3/6x78VdfPgFVaB9dZYeLUfKgHRebpkm/oP2VQ==}
     engines: {node: '>=0.10.0'}
@@ -4586,8 +4675,8 @@ packages:
       react-dom: 18.2.0(react@18.2.0)
     dev: false
 
-  /rc-table@7.35.2(react-dom@18.2.0)(react@18.2.0):
-    resolution: {integrity: sha512-ZLIZdAEdfen21FI21xt2LDg9chQ7gc5Lpy4nkjWKPDgmQMnH0KJ8JQQzrd3zrEN16xzjiVdHHvRmi1RU8BtgYg==}
+  /rc-table@7.36.0(react-dom@18.2.0)(react@18.2.0):
+    resolution: {integrity: sha512-3xVcdCC5OLeOOhaCg+5Lps2oPreM/GWXmUXWTSX4p6vF7F76ABM4dfPpMJ9Dnf5yGRyh+8pe7FRyhRVnWw2H/w==}
     engines: {node: '>=8.x'}
     peerDependencies:
       react: '>=16.9.0'
@@ -4751,26 +4840,26 @@ packages:
     engines: {node: '>=0.10.0'}
     dev: true
 
-  /react-router-dom@6.18.0(react-dom@18.2.0)(react@18.2.0):
-    resolution: {integrity: sha512-Ubrue4+Ercc/BoDkFQfc6og5zRQ4A8YxSO3Knsne+eRbZ+IepAsK249XBH/XaFuOYOYr3L3r13CXTLvYt5JDjw==}
+  /react-router-dom@6.19.0(react-dom@18.2.0)(react@18.2.0):
+    resolution: {integrity: sha512-N6dWlcgL2w0U5HZUUqU2wlmOrSb3ighJmtQ438SWbhB1yuLTXQ8yyTBMK3BSvVjp7gBtKurT554nCtMOgxCZmQ==}
     engines: {node: '>=14.0.0'}
     peerDependencies:
       react: '>=16.8'
       react-dom: '>=16.8'
     dependencies:
-      '@remix-run/router': 1.11.0
+      '@remix-run/router': 1.12.0
       react: 18.2.0
       react-dom: 18.2.0(react@18.2.0)
-      react-router: 6.18.0(react@18.2.0)
+      react-router: 6.19.0(react@18.2.0)
     dev: false
 
-  /react-router@6.18.0(react@18.2.0):
-    resolution: {integrity: sha512-vk2y7Dsy8wI02eRRaRmOs9g2o+aE72YCx5q9VasT1N9v+lrdB79tIqrjMfByHiY5+6aYkH2rUa5X839nwWGPDg==}
+  /react-router@6.19.0(react@18.2.0):
+    resolution: {integrity: sha512-0W63PKCZ7+OuQd7Tm+RbkI8kCLmn4GPjDbX61tWljPxWgqTKlEpeQUwPkT1DRjYhF8KSihK0hQpmhU4uxVMcdw==}
     engines: {node: '>=14.0.0'}
     peerDependencies:
       react: '>=16.8'
     dependencies:
-      '@remix-run/router': 1.11.0
+      '@remix-run/router': 1.12.0
       react: 18.2.0
     dev: false
 
@@ -4867,11 +4956,23 @@ packages:
       glob: 7.2.3
     dev: true
 
-  /rollup@3.29.4:
-    resolution: {integrity: sha512-oWzmBZwvYrU0iJHtDmhsm662rC15FRXmcjCk1xD771dFDx5jJ02ufAQQTn0etB2emNk4J9EZg/yWKpsn9BWGRw==}
-    engines: {node: '>=14.18.0', npm: '>=8.0.0'}
+  /rollup@4.5.0:
+    resolution: {integrity: sha512-41xsWhzxqjMDASCxH5ibw1mXk+3c4TNI2UjKbLxe6iEzrSQnqOzmmK8/3mufCPbzHNJ2e04Fc1ddI35hHy+8zg==}
+    engines: {node: '>=18.0.0', npm: '>=8.0.0'}
     hasBin: true
     optionalDependencies:
+      '@rollup/rollup-android-arm-eabi': 4.5.0
+      '@rollup/rollup-android-arm64': 4.5.0
+      '@rollup/rollup-darwin-arm64': 4.5.0
+      '@rollup/rollup-darwin-x64': 4.5.0
+      '@rollup/rollup-linux-arm-gnueabihf': 4.5.0
+      '@rollup/rollup-linux-arm64-gnu': 4.5.0
+      '@rollup/rollup-linux-arm64-musl': 4.5.0
+      '@rollup/rollup-linux-x64-gnu': 4.5.0
+      '@rollup/rollup-linux-x64-musl': 4.5.0
+      '@rollup/rollup-win32-arm64-msvc': 4.5.0
+      '@rollup/rollup-win32-ia32-msvc': 4.5.0
+      '@rollup/rollup-win32-x64-msvc': 4.5.0
       fsevents: 2.3.3
 
   /run-applescript@5.0.0:
@@ -5446,7 +5547,7 @@ packages:
     resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
     dev: true
 
-  /vite-node@0.34.6(@types/node@20.9.0)(less@4.2.0):
+  /vite-node@0.34.6(@types/node@20.9.2)(less@4.2.0):
     resolution: {integrity: sha512-nlBMJ9x6n7/Amaz6F3zJ97EBwR2FkzhBRxF5e+jE6LA3yi6Wtc2lyTij1OnDMIr34v5g/tVQtsVAzhT0jc5ygA==}
     engines: {node: '>=v14.18.0'}
     hasBin: true
@@ -5456,7 +5557,7 @@ packages:
       mlly: 1.4.2
       pathe: 1.1.1
       picocolors: 1.0.0
-      vite: 4.5.0(@types/node@20.9.0)(less@4.2.0)
+      vite: 5.0.0(@types/node@20.9.2)(less@4.2.0)
     transitivePeerDependencies:
       - '@types/node'
       - less
@@ -5476,7 +5577,7 @@ packages:
       monaco-editor: 0.44.0
     dev: false
 
-  /vite-tsconfig-paths@4.2.1(typescript@5.2.2)(vite@4.5.0):
+  /vite-tsconfig-paths@4.2.1(typescript@5.2.2)(vite@5.0.0):
     resolution: {integrity: sha512-GNUI6ZgPqT3oervkvzU+qtys83+75N/OuDaQl7HmOqFTb0pjZsuARrRipsyJhJ3enqV8beI1xhGbToR4o78nSQ==}
     peerDependencies:
       vite: '*'
@@ -5487,18 +5588,18 @@ packages:
       debug: 4.3.4
       globrex: 0.1.2
       tsconfck: 2.1.2(typescript@5.2.2)
-      vite: 4.5.0(@types/node@20.9.0)(less@4.2.0)
+      vite: 5.0.0(@types/node@20.9.2)(less@4.2.0)
     transitivePeerDependencies:
       - supports-color
       - typescript
     dev: true
 
-  /vite@4.5.0(@types/node@20.9.0)(less@4.2.0):
-    resolution: {integrity: sha512-ulr8rNLA6rkyFAlVWw2q5YJ91v098AFQ2R0PRFwPzREXOUJQPtFUG0t+/ZikhaOCDqFoDhN6/v8Sq0o4araFAw==}
-    engines: {node: ^14.18.0 || >=16.0.0}
+  /vite@5.0.0(@types/node@20.9.2)(less@4.2.0):
+    resolution: {integrity: sha512-ESJVM59mdyGpsiNAeHQOR/0fqNoOyWPYesFto8FFZugfmhdHx8Fzd8sF3Q/xkVhZsyOxHfdM7ieiVAorI9RjFw==}
+    engines: {node: ^18.0.0 || >=20.0.0}
     hasBin: true
     peerDependencies:
-      '@types/node': '>= 14'
+      '@types/node': ^18.0.0 || >=20.0.0
       less: '*'
       lightningcss: ^1.21.0
       sass: '*'
@@ -5521,11 +5622,11 @@ packages:
       terser:
         optional: true
     dependencies:
-      '@types/node': 20.9.0
-      esbuild: 0.18.20
+      '@types/node': 20.9.2
+      esbuild: 0.19.7
       less: 4.2.0
       postcss: 8.4.31
-      rollup: 3.29.4
+      rollup: 4.5.0
     optionalDependencies:
       fsevents: 2.3.3
 
@@ -5562,7 +5663,7 @@ packages:
     dependencies:
       '@types/chai': 4.3.6
       '@types/chai-subset': 1.3.3
-      '@types/node': 20.9.0
+      '@types/node': 20.9.2
       '@vitest/expect': 0.34.6
       '@vitest/runner': 0.34.6
       '@vitest/snapshot': 0.34.6
@@ -5581,8 +5682,8 @@ packages:
       strip-literal: 1.3.0
       tinybench: 2.5.1
       tinypool: 0.7.0
-      vite: 4.5.0(@types/node@20.9.0)(less@4.2.0)
-      vite-node: 0.34.6(@types/node@20.9.0)(less@4.2.0)
+      vite: 5.0.0(@types/node@20.9.2)(less@4.2.0)
+      vite-node: 0.34.6(@types/node@20.9.2)(less@4.2.0)
       why-is-node-running: 2.2.2
     transitivePeerDependencies:
       - less
@@ -5770,7 +5871,7 @@ packages:
     dependencies:
       '@types/fs-extra': 11.0.1
       '@types/minimist': 1.2.2
-      '@types/node': 18.18.9
+      '@types/node': 18.18.10
       '@types/ps-tree': 1.1.2
       '@types/which': 3.0.0
       chalk: 5.3.0
diff --git a/ui/src/config/transport.ts b/ui/src/config/transport.ts
index d422abbe..39a1773e 100644
--- a/ui/src/config/transport.ts
+++ b/ui/src/config/transport.ts
@@ -6,15 +6,39 @@ import { paths } from './paths';
 
 export const authTokenKey = 'auth_token';
 
-const errorHandler: Interceptor = (next) => async (req) => {
+const logout = () => {
+  localStorage.removeItem(authTokenKey);
+  window.location.replace(paths.login);
+};
+
+const authHandler: Interceptor = (next) => (req) => {
+  const token = localStorage.getItem(authTokenKey);
+  let isTokenExpired;
+
   try {
-    const token = localStorage.getItem(authTokenKey);
+    isTokenExpired = token && Date.now() >= JSON.parse(atob(token.split('.')[1])).exp * 1000;
+  } catch (err) {
+    logout();
 
-    if (token) {
-      req.header.append('Authorization', `Bearer ${token}`);
-    }
+    throw new ConnectError('Invalid token');
+  }
 
-    return await next(req);
+  if (isTokenExpired) {
+    logout();
+
+    throw new ConnectError('Token expired');
+  }
+
+  if (token) {
+    req.header.append('Authorization', `Bearer ${token}`);
+  }
+
+  return next(req);
+};
+
+const errorHandler: Interceptor = (next) => (req) => {
+  try {
+    return next(req);
   } catch (err) {
     if (req.signal.aborted) {
       throw err;
@@ -24,9 +48,7 @@ const errorHandler: Interceptor = (next) => async (req) => {
     notification.error({ message: errorMessage, placement: 'bottomRight' });
 
     if (err instanceof ConnectError && err.code === Code.Unauthenticated) {
-      localStorage.removeItem(authTokenKey);
-
-      setTimeout(() => window.location.replace(paths.login), 3000);
+      logout();
     }
 
     throw err;
@@ -35,5 +57,5 @@ const errorHandler: Interceptor = (next) => async (req) => {
 
 export const transport = createConnectTransport({
   baseUrl: '',
-  interceptors: [errorHandler]
+  interceptors: [authHandler, errorHandler]
 });
diff --git a/ui/src/features/project/project-details/nodes/repo-node.module.less b/ui/src/features/project/project-details/nodes/repo-node.module.less
index d76223fc..66af17a6 100644
--- a/ui/src/features/project/project-details/nodes/repo-node.module.less
+++ b/ui/src/features/project/project-details/nodes/repo-node.module.less
@@ -30,6 +30,9 @@
   color: #777;
   font-size: 11px;
   text-decoration: none;
+  white-space: nowrap;
+  overflow: hidden;
+  text-overflow: ellipsis;;
 
   &:hover {
     text-decoration: underline;
diff --git a/ui/src/features/project/project-details/nodes/repo-node.tsx b/ui/src/features/project/project-details/nodes/repo-node.tsx
index 1e8b32ee..1ee210ad 100644
--- a/ui/src/features/project/project-details/nodes/repo-node.tsx
+++ b/ui/src/features/project/project-details/nodes/repo-node.tsx
@@ -9,11 +9,8 @@ import { NodeType, NodesRepoType } from '../types';
 
 import * as styles from './repo-node.module.less';
 
-const MAX_CHARS = 19;
-
 type Props = {
   nodeData: NodesRepoType;
-  height: number;
   children?: React.ReactNode;
 };
 
@@ -31,13 +28,15 @@ const ico = {
   [NodeType.REPO_CHART]: faAnchor
 };
 
-export const RepoNode = ({ nodeData, height, children }: Props) => {
+export const RepoNode = ({ nodeData, children }: Props) => {
   const type = nodeData.type;
   const value = type === NodeType.REPO_CHART ? nodeData.data.registryUrl : nodeData.data.repoUrl;
   return (
-    <div style={{ height }} className={styles.node}>
-      <h3 className='flex justify-between'>
-        <span>{nodeData.warehouseName}</span>
+    <div className={styles.node}>
+      <h3 className='flex justify-between gap-2'>
+        <div className='text-ellipsis whitespace-nowrap overflow-hidden'>
+          {nodeData.warehouseName}
+        </div>
         {nodeData.type !== NodeType.REPO_CHART && <FontAwesomeIcon icon={faBuilding} />}
       </h3>
       <div className={styles.body}>
@@ -54,8 +53,7 @@ export const RepoNode = ({ nodeData, height, children }: Props) => {
               target='_blank'
               rel='noreferrer'
             >
-              {value.length > MAX_CHARS && '...'}
-              {value.substring(value.length - MAX_CHARS)}
+              {value}
             </a>
           </Tooltip>
         </div>
diff --git a/ui/src/features/project/project-details/nodes/stage-node.module.less b/ui/src/features/project/project-details/nodes/stage-node.module.less
index 50405797..199180de 100644
--- a/ui/src/features/project/project-details/nodes/stage-node.module.less
+++ b/ui/src/features/project/project-details/nodes/stage-node.module.less
@@ -3,10 +3,9 @@
   border-radius: 10px;
 
   h3 {
-    padding: 0.7em 0.5em;
+    padding: 0.7em 0.75em;
     color: white;
     font-weight: 600;
-    margin-left: 0.25em;
     font-size: 15px;
   }
 
diff --git a/ui/src/features/project/project-details/nodes/stage-node.tsx b/ui/src/features/project/project-details/nodes/stage-node.tsx
index 4de7c60d..6aa3b238 100644
--- a/ui/src/features/project/project-details/nodes/stage-node.tsx
+++ b/ui/src/features/project/project-details/nodes/stage-node.tsx
@@ -46,7 +46,7 @@ export const StageNode = ({
         }}
       >
         <h3 className='flex items-center text-white justify-between'>
-          <div className='text-ellipsis whitespace-nowrap overflow-hidden h-8'>
+          <div className='text-ellipsis whitespace-nowrap overflow-hidden'>
             {stage.metadata?.name}
           </div>
           {stage.status?.currentPromotion ? (
diff --git a/ui/src/features/project/project-details/project-details.tsx b/ui/src/features/project/project-details/project-details.tsx
index 2392d009..a2b52799 100644
--- a/ui/src/features/project/project-details/project-details.tsx
+++ b/ui/src/features/project/project-details/project-details.tsx
@@ -152,6 +152,7 @@ export const ProjectDetails = () => {
                 ? NodeType.REPO_IMAGE
                 : NodeType.REPO_GIT;
             n.push({
+              // eslint-disable-next-line @typescript-eslint/no-explicit-any
               data: sub.chart || sub.image || sub.git || ({} as any),
               stageName: stage.metadata?.name || '',
               warehouseName: cur.metadata?.name || '',
@@ -379,7 +380,7 @@ export const ProjectDetails = () => {
                         />
                       </>
                     ) : (
-                      <RepoNode nodeData={node} height={node.height}>
+                      <RepoNode nodeData={node}>
                         <div className='flex w-full'>
                           <Button
                             onClick={() =>
diff --git a/ui/src/features/stage/subscriptions.tsx b/ui/src/features/stage/subscriptions.tsx
index 4352cf14..7d8244df 100644
--- a/ui/src/features/stage/subscriptions.tsx
+++ b/ui/src/features/stage/subscriptions.tsx
@@ -3,7 +3,6 @@ import { Link, generatePath } from 'react-router-dom';
 
 import { paths } from '@ui/config/paths';
 import { Subscriptions as SubscriptionsType } from '@ui/gen/v1alpha1/types_pb';
-import { urlWithProtocol } from '@ui/utils/url';
 
 export const Subscriptions = (props: {
   subscriptions?: SubscriptionsType;
@@ -19,6 +18,12 @@ export const Subscriptions = (props: {
     <div>
       <Typography.Title level={3}>Subscriptions</Typography.Title>
 
+      {subscriptions.warehouse && (
+        <Descriptions bordered size='small' column={1} style={{ width: '50%' }}>
+          <Descriptions.Item label='Warehouse'>{subscriptions.warehouse}</Descriptions.Item>
+        </Descriptions>
+      )}
+
       {!!subscriptions.upstreamStages.length && (
         <>
           <Typography.Title level={5} style={{ marginTop: '.8em' }}>
@@ -42,90 +47,6 @@ export const Subscriptions = (props: {
           </Space>
         </>
       )}
-
-      {!!subscriptions.repos?.git.length && (
-        <>
-          <Typography.Title level={5} style={{ marginTop: '.8em' }}>
-            Git Repositories
-          </Typography.Title>
-          <Space direction='vertical' style={{ width: '100%' }}>
-            {subscriptions?.repos.git.map((gitRepo) => (
-              <Descriptions bordered size='small' key={gitRepo.repoUrl} column={1}>
-                <Descriptions.Item label='URL'>
-                  <Typography.Link
-                    href={urlWithProtocol(gitRepo.repoUrl)}
-                    target='_blank'
-                    rel='noreferrer'
-                  >
-                    {gitRepo.repoUrl}
-                  </Typography.Link>
-                </Descriptions.Item>
-                <Descriptions.Item label='Branch'>{gitRepo.branch}</Descriptions.Item>
-              </Descriptions>
-            ))}
-          </Space>
-        </>
-      )}
-
-      {!!subscriptions.repos?.images.length && (
-        <>
-          <Typography.Title level={5} style={{ marginTop: '.8em' }}>
-            Container images
-          </Typography.Title>
-          <Space direction='vertical' style={{ width: '100%' }}>
-            {subscriptions?.repos.images.map((image) => (
-              <Descriptions bordered size='small' key={image.repoUrl}>
-                <Descriptions.Item label='URL'>
-                  <Typography.Link
-                    href={urlWithProtocol(image.repoUrl)}
-                    target='_blank'
-                    rel='noreferrer'
-                  >
-                    {image.repoUrl}
-                  </Typography.Link>
-                </Descriptions.Item>
-                {image.semverConstraint && (
-                  <Descriptions.Item label='Semver Constraint'>
-                    {image.semverConstraint}
-                  </Descriptions.Item>
-                )}
-                <Descriptions.Item label='Update Strategy'>
-                  {image.updateStrategy}
-                </Descriptions.Item>
-              </Descriptions>
-            ))}
-          </Space>
-        </>
-      )}
-
-      {!!subscriptions.repos?.charts.length && (
-        <>
-          <Typography.Title level={5} style={{ marginTop: '.8em' }}>
-            Helm charts
-          </Typography.Title>
-          <Space direction='vertical' style={{ width: '100%' }}>
-            {subscriptions?.repos.charts.map((chart) => (
-              <Descriptions bordered size='small' key={chart.registryUrl}>
-                <Descriptions.Item label='Registry URL'>
-                  <Typography.Link
-                    href={urlWithProtocol(chart.registryUrl)}
-                    target='_blank'
-                    rel='noreferrer'
-                  >
-                    {chart.registryUrl}
-                  </Typography.Link>
-                </Descriptions.Item>
-                {chart.name && <Descriptions.Item label='Name'>{chart.name}</Descriptions.Item>}
-                {chart.semverConstraint && (
-                  <Descriptions.Item label='Semver Constraint'>
-                    {chart.semverConstraint}
-                  </Descriptions.Item>
-                )}
-              </Descriptions>
-            ))}
-          </Space>
-        </>
-      )}
     </div>
   );
 };
diff --git a/ui/src/gen/schema/freights.kargo.akuity.io_v1alpha1.json b/ui/src/gen/schema/freights.kargo.akuity.io_v1alpha1.json
index 6c7281e0..8ef1c88f 100644
--- a/ui/src/gen/schema/freights.kargo.akuity.io_v1alpha1.json
+++ b/ui/src/gen/schema/freights.kargo.akuity.io_v1alpha1.json
@@ -98,12 +98,20 @@
     "status": {
       "description": "Status describes the current status of this Freight.",
       "properties": {
-        "qualifications": {
+        "approvedFor": {
           "additionalProperties": {
-            "description": "Qualification describes a Freight's qualification for a Stage.",
+            "description": "ApprovedStage describes a Stage for which Freight has been (manually) approved.",
             "type": "object"
           },
-          "description": "Qualifications describes the Stages for which this Freight has been qualified.",
+          "description": "ApprovedFor describes the Stages for which this Freight has been approved preemptively/manually by a user. This is useful for hotfixes, where one might wish to promote a piece of Freight to a given Stage without transiting the entire pipeline.",
+          "type": "object"
+        },
+        "verifiedIn": {
+          "additionalProperties": {
+            "description": "VerifiedStage describes a Stage in which Freight has been verified.",
+            "type": "object"
+          },
+          "description": "VerifiedIn describes the Stages in which this Freight has been verified through promotion and subsequent health checks.",
           "type": "object"
         }
       },
diff --git a/ui/src/gen/v1alpha1/types_pb.ts b/ui/src/gen/v1alpha1/types_pb.ts
index 88d6e3b4..80c41476 100644
--- a/ui/src/gen/v1alpha1/types_pb.ts
+++ b/ui/src/gen/v1alpha1/types_pb.ts
@@ -1739,9 +1739,14 @@ export class Freight extends Message<Freight> {
  */
 export class FreightStatus extends Message<FreightStatus> {
   /**
-   * @generated from field: map<string, github.com.akuity.kargo.pkg.api.v1alpha1.Qualification> qualifications = 1;
+   * @generated from field: map<string, github.com.akuity.kargo.pkg.api.v1alpha1.VerifiedStage> verified_in = 1;
    */
-  qualifications: { [key: string]: Qualification } = {};
+  verifiedIn: { [key: string]: VerifiedStage } = {};
+
+  /**
+   * @generated from field: map<string, github.com.akuity.kargo.pkg.api.v1alpha1.ApprovedStage> approved_for = 2;
+   */
+  approvedFor: { [key: string]: ApprovedStage } = {};
 
   constructor(data?: PartialMessage<FreightStatus>) {
     super();
@@ -1751,7 +1756,8 @@ export class FreightStatus extends Message<FreightStatus> {
   static readonly runtime: typeof proto3 = proto3;
   static readonly typeName = "github.com.akuity.kargo.pkg.api.v1alpha1.FreightStatus";
   static readonly fields: FieldList = proto3.util.newFieldList(() => [
-    { no: 1, name: "qualifications", kind: "map", K: 9 /* ScalarType.STRING */, V: {kind: "message", T: Qualification} },
+    { no: 1, name: "verified_in", kind: "map", K: 9 /* ScalarType.STRING */, V: {kind: "message", T: VerifiedStage} },
+    { no: 2, name: "approved_for", kind: "map", K: 9 /* ScalarType.STRING */, V: {kind: "message", T: ApprovedStage} },
   ]);
 
   static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): FreightStatus {
@@ -1772,33 +1778,64 @@ export class FreightStatus extends Message<FreightStatus> {
 }
 
 /**
- * @generated from message github.com.akuity.kargo.pkg.api.v1alpha1.Qualification
+ * @generated from message github.com.akuity.kargo.pkg.api.v1alpha1.VerifiedStage
+ */
+export class VerifiedStage extends Message<VerifiedStage> {
+  constructor(data?: PartialMessage<VerifiedStage>) {
+    super();
+    proto3.util.initPartial(data, this);
+  }
+
+  static readonly runtime: typeof proto3 = proto3;
+  static readonly typeName = "github.com.akuity.kargo.pkg.api.v1alpha1.VerifiedStage";
+  static readonly fields: FieldList = proto3.util.newFieldList(() => [
+  ]);
+
+  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): VerifiedStage {
+    return new VerifiedStage().fromBinary(bytes, options);
+  }
+
+  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): VerifiedStage {
+    return new VerifiedStage().fromJson(jsonValue, options);
+  }
+
+  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): VerifiedStage {
+    return new VerifiedStage().fromJsonString(jsonString, options);
+  }
+
+  static equals(a: VerifiedStage | PlainMessage<VerifiedStage> | undefined, b: VerifiedStage | PlainMessage<VerifiedStage> | undefined): boolean {
+    return proto3.util.equals(VerifiedStage, a, b);
+  }
+}
+
+/**
+ * @generated from message github.com.akuity.kargo.pkg.api.v1alpha1.ApprovedStage
  */
-export class Qualification extends Message<Qualification> {
-  constructor(data?: PartialMessage<Qualification>) {
+export class ApprovedStage extends Message<ApprovedStage> {
+  constructor(data?: PartialMessage<ApprovedStage>) {
     super();
     proto3.util.initPartial(data, this);
   }
 
   static readonly runtime: typeof proto3 = proto3;
-  static readonly typeName = "github.com.akuity.kargo.pkg.api.v1alpha1.Qualification";
+  static readonly typeName = "github.com.akuity.kargo.pkg.api.v1alpha1.ApprovedStage";
   static readonly fields: FieldList = proto3.util.newFieldList(() => [
   ]);
 
-  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): Qualification {
-    return new Qualification().fromBinary(bytes, options);
+  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): ApprovedStage {
+    return new ApprovedStage().fromBinary(bytes, options);
   }
 
-  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): Qualification {
-    return new Qualification().fromJson(jsonValue, options);
+  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): ApprovedStage {
+    return new ApprovedStage().fromJson(jsonValue, options);
   }
 
-  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): Qualification {
-    return new Qualification().fromJsonString(jsonString, options);
+  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): ApprovedStage {
+    return new ApprovedStage().fromJsonString(jsonString, options);
   }
 
-  static equals(a: Qualification | PlainMessage<Qualification> | undefined, b: Qualification | PlainMessage<Qualification> | undefined): boolean {
-    return proto3.util.equals(Qualification, a, b);
+  static equals(a: ApprovedStage | PlainMessage<ApprovedStage> | undefined, b: ApprovedStage | PlainMessage<ApprovedStage> | undefined): boolean {
+    return proto3.util.equals(ApprovedStage, a, b);
   }
 }
 


Test patch:
diff --git a/internal/api/promote_stage_v1alpha1_test.go b/internal/api/promote_stage_v1alpha1_test.go
index e6ba940f..f4a782b0 100644
--- a/internal/api/promote_stage_v1alpha1_test.go
+++ b/internal/api/promote_stage_v1alpha1_test.go
@@ -117,7 +117,7 @@ func TestPromoteStage(t *testing.T) {
 			},
 		},
 		{
-			name: "error getting qualified Freight",
+			name: "error getting available Freight",
 			req: &svcv1alpha1.PromoteStageRequest{
 				Project: "fake-project",
 				Name:    "fake-stage",
@@ -144,11 +144,12 @@ func TestPromoteStage(t *testing.T) {
 						},
 					}, nil
 				},
-				getQualifiedFreightFn: func(
+				getAvailableFreightFn: func(
 					context.Context,
 					client.Client,
 					types.NamespacedName,
 					[]string,
+					string,
 				) (*kargoapi.Freight, error) {
 					return nil, errors.New("something went wrong")
 				},
@@ -165,7 +166,7 @@ func TestPromoteStage(t *testing.T) {
 			},
 		},
 		{
-			name: "Freight not found or is not qualified for any of the upstream Stages",
+			name: "available Freight not found",
 			req: &svcv1alpha1.PromoteStageRequest{
 				Project: "fake-project",
 				Name:    "fake-stage",
@@ -192,11 +193,12 @@ func TestPromoteStage(t *testing.T) {
 						},
 					}, nil
 				},
-				getQualifiedFreightFn: func(
+				getAvailableFreightFn: func(
 					context.Context,
 					client.Client,
 					types.NamespacedName,
 					[]string,
+					string,
 				) (*kargoapi.Freight, error) {
 					return nil, nil
 				},
@@ -209,7 +211,7 @@ func TestPromoteStage(t *testing.T) {
 				connErr, ok := err.(*connect.Error)
 				require.True(t, ok)
 				require.Equal(t, connect.CodeNotFound, connErr.Code())
-				require.Contains(t, connErr.Message(), "no qualified Freight")
+				require.Contains(t, connErr.Message(), "no available Freight")
 				require.Contains(t, connErr.Message(), "found in namespace")
 			},
 		},
@@ -241,11 +243,12 @@ func TestPromoteStage(t *testing.T) {
 						},
 					}, nil
 				},
-				getQualifiedFreightFn: func(
+				getAvailableFreightFn: func(
 					context.Context,
 					client.Client,
 					types.NamespacedName,
 					[]string,
+					string,
 				) (*kargoapi.Freight, error) {
 					return &kargoapi.Freight{}, nil
 				},
@@ -296,11 +299,12 @@ func TestPromoteStage(t *testing.T) {
 						},
 					}, nil
 				},
-				getQualifiedFreightFn: func(
+				getAvailableFreightFn: func(
 					context.Context,
 					client.Client,
 					types.NamespacedName,
 					[]string,
+					string,
 				) (*kargoapi.Freight, error) {
 					return &kargoapi.Freight{}, nil
 				},
diff --git a/internal/api/promote_subscribers_v1alpha1_test.go b/internal/api/promote_subscribers_v1alpha1_test.go
index cbf7f155..e72e8d5d 100644
--- a/internal/api/promote_subscribers_v1alpha1_test.go
+++ b/internal/api/promote_subscribers_v1alpha1_test.go
@@ -119,7 +119,7 @@ func TestPromoteSubscribers(t *testing.T) {
 			},
 		},
 		{
-			name: "error getting qualified Freight",
+			name: "error getting available Freight",
 			req: &svcv1alpha1.PromoteSubscribersRequest{
 				Project: "fake-project",
 				Stage:   "fake-stage",
@@ -146,11 +146,12 @@ func TestPromoteSubscribers(t *testing.T) {
 						},
 					}, nil
 				},
-				getQualifiedFreightFn: func(
+				getAvailableFreightFn: func(
 					context.Context,
 					client.Client,
 					types.NamespacedName,
 					[]string,
+					string,
 				) (*kargoapi.Freight, error) {
 					return nil, errors.New("something went wrong")
 				},
@@ -167,7 +168,7 @@ func TestPromoteSubscribers(t *testing.T) {
 			},
 		},
 		{
-			name: "Freight not found or is not qualified for the specified Stage",
+			name: "available Freight not found",
 			req: &svcv1alpha1.PromoteSubscribersRequest{
 				Project: "fake-project",
 				Stage:   "fake-stage",
@@ -194,11 +195,12 @@ func TestPromoteSubscribers(t *testing.T) {
 						},
 					}, nil
 				},
-				getQualifiedFreightFn: func(
+				getAvailableFreightFn: func(
 					context.Context,
 					client.Client,
 					types.NamespacedName,
 					[]string,
+					string,
 				) (*kargoapi.Freight, error) {
 					return nil, nil
 				},
@@ -211,7 +213,7 @@ func TestPromoteSubscribers(t *testing.T) {
 				connErr, ok := err.(*connect.Error)
 				require.True(t, ok)
 				require.Equal(t, connect.CodeNotFound, connErr.Code())
-				require.Contains(t, connErr.Message(), "no qualified Freight")
+				require.Contains(t, connErr.Message(), "no available Freight")
 				require.Contains(t, connErr.Message(), "found in namespace")
 			},
 		},
@@ -243,11 +245,12 @@ func TestPromoteSubscribers(t *testing.T) {
 						},
 					}, nil
 				},
-				getQualifiedFreightFn: func(
+				getAvailableFreightFn: func(
 					context.Context,
 					client.Client,
 					types.NamespacedName,
 					[]string,
+					string,
 				) (*kargoapi.Freight, error) {
 					return &kargoapi.Freight{}, nil
 				},
@@ -297,11 +300,12 @@ func TestPromoteSubscribers(t *testing.T) {
 						},
 					}, nil
 				},
-				getQualifiedFreightFn: func(
+				getAvailableFreightFn: func(
 					context.Context,
 					client.Client,
 					types.NamespacedName,
 					[]string,
+					string,
 				) (*kargoapi.Freight, error) {
 					return &kargoapi.Freight{}, nil
 				},
@@ -352,11 +356,12 @@ func TestPromoteSubscribers(t *testing.T) {
 						},
 					}, nil
 				},
-				getQualifiedFreightFn: func(
+				getAvailableFreightFn: func(
 					context.Context,
 					client.Client,
 					types.NamespacedName,
 					[]string,
+					string,
 				) (*kargoapi.Freight, error) {
 					return &kargoapi.Freight{}, nil
 				},
@@ -413,11 +418,12 @@ func TestPromoteSubscribers(t *testing.T) {
 						},
 					}, nil
 				},
-				getQualifiedFreightFn: func(
+				getAvailableFreightFn: func(
 					context.Context,
 					client.Client,
 					types.NamespacedName,
 					[]string,
+					string,
 				) (*kargoapi.Freight, error) {
 					return &kargoapi.Freight{}, nil
 				},
diff --git a/internal/api/query_freights_v1alpha1_test.go b/internal/api/query_freights_v1alpha1_test.go
index f00318a4..903bad1e 100644
--- a/internal/api/query_freights_v1alpha1_test.go
+++ b/internal/api/query_freights_v1alpha1_test.go
@@ -171,6 +171,7 @@ func TestQueryFreight(t *testing.T) {
 				getAvailableFreightForStageFn: func(
 					context.Context,
 					string,
+					string,
 					kargoapi.Subscriptions,
 				) ([]kargoapi.Freight, error) {
 					return nil, errors.New("something went wrong")
@@ -425,7 +426,7 @@ func TestGetAvailableFreightForStage(t *testing.T) {
 			},
 		},
 		{
-			name: "error getting Freight from upstream Stages",
+			name: "error getting Freight verified in upstream Stages",
 			subs: kargoapi.Subscriptions{
 				UpstreamStages: []kargoapi.StageSubscription{
 					{
@@ -434,7 +435,7 @@ func TestGetAvailableFreightForStage(t *testing.T) {
 				},
 			},
 			server: &server{
-				getFreightQualifiedForUpstreamStagesFn: func(
+				getVerifiedFreightFn: func(
 					context.Context,
 					string,
 					[]kargoapi.StageSubscription,
@@ -444,11 +445,51 @@ func TestGetAvailableFreightForStage(t *testing.T) {
 			},
 			assertions: func(f []kargoapi.Freight, err error) {
 				require.Error(t, err)
-				require.Equal(t, "something went wrong", err.Error())
+				require.Contains(
+					t,
+					err.Error(),
+					"error listing Freight verified in Stages upstream from Stage",
+				)
+				require.Contains(t, err.Error(), "something went wrong")
+			},
+		},
+		{
+			name: "error getting Freight approved for Stage",
+			subs: kargoapi.Subscriptions{
+				UpstreamStages: []kargoapi.StageSubscription{
+					{
+						Name: "fake-stage",
+					},
+				},
+			},
+			server: &server{
+				getVerifiedFreightFn: func(
+					context.Context,
+					string,
+					[]kargoapi.StageSubscription,
+				) ([]kargoapi.Freight, error) {
+					return nil, nil
+				},
+				listFreightFn: func(
+					context.Context,
+					client.ObjectList,
+					...client.ListOption,
+				) error {
+					return errors.New("something went wrong")
+				},
+			},
+			assertions: func(f []kargoapi.Freight, err error) {
+				require.Error(t, err)
+				require.Contains(
+					t,
+					err.Error(),
+					"error listing Freight approved for Stage",
+				)
+				require.Contains(t, err.Error(), "something went wrong")
 			},
 		},
 		{
-			name: "success getting Freight from upstream Stages",
+			name: "success getting available Freight",
 			subs: kargoapi.Subscriptions{
 				UpstreamStages: []kargoapi.StageSubscription{
 					{
@@ -457,7 +498,7 @@ func TestGetAvailableFreightForStage(t *testing.T) {
 				},
 			},
 			server: &server{
-				getFreightQualifiedForUpstreamStagesFn: func(
+				getVerifiedFreightFn: func(
 					context.Context,
 					string,
 					[]kargoapi.StageSubscription,
@@ -468,12 +509,23 @@ func TestGetAvailableFreightForStage(t *testing.T) {
 								Name: "fake-freight",
 							},
 						},
+					}, nil
+				},
+				listFreightFn: func(
+					_ context.Context,
+					objList client.ObjectList,
+					_ ...client.ListOption,
+				) error {
+					freight, ok := objList.(*kargoapi.FreightList)
+					require.True(t, ok)
+					freight.Items = []kargoapi.Freight{
 						{
 							ObjectMeta: metav1.ObjectMeta{
 								Name: "another fake-freight",
 							},
 						},
-					}, nil
+					}
+					return nil
 				},
 			},
 			assertions: func(freight []kargoapi.Freight, err error) {
@@ -488,6 +540,7 @@ func TestGetAvailableFreightForStage(t *testing.T) {
 				testCase.server.getAvailableFreightForStage(
 					context.Background(),
 					"fake-project",
+					"fake-stage",
 					testCase.subs,
 				),
 			)
@@ -562,7 +615,7 @@ func TestGetFreightFromWarehouse(t *testing.T) {
 	}
 }
 
-func TestGetFreightQualifiedForUpstreamStages(t *testing.T) {
+func TestGetVerifiedFreight(t *testing.T) {
 	testCases := []struct {
 		name       string
 		server     *server
@@ -585,7 +638,7 @@ func TestGetFreightQualifiedForUpstreamStages(t *testing.T) {
 				require.Contains(
 					t,
 					err.Error(),
-					"error listing Freight qualified for Stage",
+					"error listing Freight verified in Stage",
 				)
 			},
 		},
@@ -624,7 +677,7 @@ func TestGetFreightQualifiedForUpstreamStages(t *testing.T) {
 	for _, testCase := range testCases {
 		t.Run(testCase.name, func(t *testing.T) {
 			testCase.assertions(
-				testCase.server.getFreightQualifiedForUpstreamStages(
+				testCase.server.getVerifiedFreight(
 					context.Background(),
 					"fake-project",
 					[]kargoapi.StageSubscription{
diff --git a/internal/api/server_test.go b/internal/api/server_test.go
index ebf6bdba..41fe5cfe 100644
--- a/internal/api/server_test.go
+++ b/internal/api/server_test.go
@@ -38,12 +38,12 @@ func TestNewServer(t *testing.T) {
 	require.NotNil(t, s.validateProjectFn)
 	require.NotNil(t, s.externalValidateProjectFn)
 	require.NotNil(t, s.getStageFn)
-	require.NotNil(t, s.getQualifiedFreightFn)
+	require.NotNil(t, s.getAvailableFreightFn)
 	require.NotNil(t, s.createPromotionFn)
 	require.NotNil(t, s.findStageSubscribersFn)
 	require.NotNil(t, s.listFreightFn)
 	require.NotNil(t, s.getAvailableFreightForStageFn)
 	require.NotNil(t, s.getFreightFromWarehouseFn)
-	require.NotNil(t, s.getFreightQualifiedForUpstreamStagesFn)
+	require.NotNil(t, s.getVerifiedFreightFn)
 	require.NotNil(t, s.parseManifestFn)
 }
diff --git a/internal/cli/config/config_test.go b/internal/cli/config/config_test.go
index 7686043f..1a28deb5 100644
--- a/internal/cli/config/config_test.go
+++ b/internal/cli/config/config_test.go
@@ -5,8 +5,8 @@ import (
 	"path/filepath"
 	"testing"
 
-	"github.com/ghodss/yaml"
 	"github.com/stretchr/testify/require"
+	"sigs.k8s.io/yaml"
 )
 
 func TestLoadCLIConfig(t *testing.T) {
diff --git a/internal/controller/stages/stages_test.go b/internal/controller/stages/stages_test.go
index ac50419d..eaf81fae 100644
--- a/internal/controller/stages/stages_test.go
+++ b/internal/controller/stages/stages_test.go
@@ -30,9 +30,9 @@ func TestNewReconciler(t *testing.T) {
 	// Health checks:
 	require.NotNil(t, e.checkHealthFn)
 	require.NotNil(t, e.getArgoCDAppFn)
-	// Freight qualification:
+	// Freight verification:
 	require.NotNil(t, e.getFreightFn)
-	require.NotNil(t, e.qualifyFreightFn)
+	require.NotNil(t, e.verifyFreightInStageFn)
 	require.NotNil(t, e.patchFreightStatusFn)
 	// Auto-promotion:
 	require.NotNil(t, e.isAutoPromotionPermittedFn)
@@ -40,10 +40,10 @@ func TestNewReconciler(t *testing.T) {
 	require.NotNil(t, e.createPromotionFn)
 	// Discovering latest Freight:
 	require.NotNil(t, e.getLatestAvailableFreightFn)
-	require.NotNil(t, e.getAllFreightFromWarehouseFn)
 	require.NotNil(t, e.getLatestFreightFromWarehouseFn)
-	require.NotNil(t, e.getAllFreightQualifiedForUpstreamStagesFn)
-	require.NotNil(t, e.getLatestFreightQualifiedForUpstreamStagesFn)
+	require.NotNil(t, e.getAllVerifiedFreightFn)
+	require.NotNil(t, e.getLatestVerifiedFreightFn)
+	require.NotNil(t, e.getLatestApprovedFreightFn)
 	require.NotNil(t, e.listFreightFn)
 }
 
@@ -68,12 +68,12 @@ func TestSyncControlFlowStage(t *testing.T) {
 				},
 			},
 			reconciler: &reconciler{
-				getAllFreightFromWarehouseFn: func(
+				listFreightFn: func(
 					context.Context,
-					string,
-					string,
-				) ([]kargoapi.Freight, error) {
-					return nil, errors.New("something went wrong")
+					client.ObjectList,
+					...client.ListOption,
+				) error {
+					return errors.New("something went wrong")
 				},
 			},
 			assertions: func(
@@ -82,18 +82,14 @@ func TestSyncControlFlowStage(t *testing.T) {
 				err error,
 			) {
 				require.Error(t, err)
-				require.Contains(
-					t,
-					err.Error(),
-					"error finding all Freight from Warehouse",
-				)
+				require.Contains(t, err.Error(), "error listing Freight from Warehouse")
 				require.Contains(t, err.Error(), "something went wrong")
 				// Status should be returned unchanged
 				require.Equal(t, initialStatus, newStatus)
 			},
 		},
 		{
-			name: "error listing Freight qualified for upstream Stages",
+			name: "error listing Freight verified in upstream Stages",
 			stage: &kargoapi.Stage{
 				Spec: &kargoapi.StageSpec{
 					Subscriptions: &kargoapi.Subscriptions{
@@ -104,7 +100,7 @@ func TestSyncControlFlowStage(t *testing.T) {
 				},
 			},
 			reconciler: &reconciler{
-				getAllFreightQualifiedForUpstreamStagesFn: func(
+				getAllVerifiedFreightFn: func(
 					context.Context,
 					string,
 					[]kargoapi.StageSubscription,
@@ -121,7 +117,7 @@ func TestSyncControlFlowStage(t *testing.T) {
 				require.Contains(
 					t,
 					err.Error(),
-					"error finding available Freight for Stage",
+					"error getting all Freight verified in Stages upstream from Stage",
 				)
 				require.Contains(t, err.Error(), "something went wrong")
 				// Status should be returned unchanged
@@ -129,7 +125,7 @@ func TestSyncControlFlowStage(t *testing.T) {
 			},
 		},
 		{
-			name: "error qualifying Freight",
+			name: "error marking Freight as verified in Stage",
 			stage: &kargoapi.Stage{
 				Spec: &kargoapi.StageSpec{
 					Subscriptions: &kargoapi.Subscriptions{
@@ -138,12 +134,15 @@ func TestSyncControlFlowStage(t *testing.T) {
 				},
 			},
 			reconciler: &reconciler{
-				getAllFreightFromWarehouseFn: func(
-					context.Context,
-					string,
-					string,
-				) ([]kargoapi.Freight, error) {
-					return []kargoapi.Freight{{}}, nil
+				listFreightFn: func(
+					_ context.Context,
+					objList client.ObjectList,
+					_ ...client.ListOption,
+				) error {
+					freight, ok := objList.(*kargoapi.FreightList)
+					require.True(t, ok)
+					freight.Items = []kargoapi.Freight{{}}
+					return nil
 				},
 				patchFreightStatusFn: func(
 					context.Context,
@@ -159,7 +158,7 @@ func TestSyncControlFlowStage(t *testing.T) {
 				err error,
 			) {
 				require.Error(t, err)
-				require.Contains(t, err.Error(), "error qualifying Freight")
+				require.Contains(t, err.Error(), "error marking Freight")
 				require.Contains(t, err.Error(), "something went wrong")
 				// Status should be returned unchanged
 				require.Equal(t, initialStatus, newStatus)
@@ -182,12 +181,15 @@ func TestSyncControlFlowStage(t *testing.T) {
 				},
 			},
 			reconciler: &reconciler{
-				getAllFreightFromWarehouseFn: func(
-					context.Context,
-					string,
-					string,
-				) ([]kargoapi.Freight, error) {
-					return []kargoapi.Freight{{}}, nil
+				listFreightFn: func(
+					_ context.Context,
+					objList client.ObjectList,
+					_ ...client.ListOption,
+				) error {
+					freight, ok := objList.(*kargoapi.FreightList)
+					require.True(t, ok)
+					freight.Items = []kargoapi.Freight{{}}
+					return nil
 				},
 				patchFreightStatusFn: func(
 					context.Context,
@@ -287,7 +289,7 @@ func TestSyncNormalStage(t *testing.T) {
 		},
 
 		{
-			name: "error qualifying Freight",
+			name: "error marking Freight as verified in Stage",
 			stage: &kargoapi.Stage{
 				Spec: &kargoapi.StageSpec{
 					PromotionMechanisms: &kargoapi.PromotionMechanisms{},
@@ -305,7 +307,7 @@ func TestSyncNormalStage(t *testing.T) {
 				) *kargoapi.Health {
 					return nil
 				},
-				qualifyFreightFn: func(context.Context, string, string, string) error {
+				verifyFreightInStageFn: func(context.Context, string, string, string) error {
 					return errors.New("something went wrong")
 				},
 			},
@@ -316,7 +318,7 @@ func TestSyncNormalStage(t *testing.T) {
 			) {
 				require.Error(t, err)
 				require.Contains(t, err.Error(), "something went wrong")
-				require.Contains(t, err.Error(), "error qualifying Freight")
+				require.Contains(t, err.Error(), "error marking Freight")
 				// Status should be returned unchanged
 				require.Equal(t, initialStatus, newStatus)
 			},
@@ -347,7 +349,7 @@ func TestSyncNormalStage(t *testing.T) {
 				) *kargoapi.Health {
 					return nil
 				},
-				qualifyFreightFn: func(context.Context, string, string, string) error {
+				verifyFreightInStageFn: func(context.Context, string, string, string) error {
 					return nil
 				},
 			},
@@ -384,7 +386,7 @@ func TestSyncNormalStage(t *testing.T) {
 				) *kargoapi.Health {
 					return nil
 				},
-				qualifyFreightFn: func(context.Context, string, string, string) error {
+				verifyFreightInStageFn: func(context.Context, string, string, string) error {
 					return nil
 				},
 				isAutoPromotionPermittedFn: func(
@@ -434,7 +436,7 @@ func TestSyncNormalStage(t *testing.T) {
 				) *kargoapi.Health {
 					return nil
 				},
-				qualifyFreightFn: func(context.Context, string, string, string) error {
+				verifyFreightInStageFn: func(context.Context, string, string, string) error {
 					return nil
 				},
 				isAutoPromotionPermittedFn: func(
@@ -478,7 +480,7 @@ func TestSyncNormalStage(t *testing.T) {
 				) *kargoapi.Health {
 					return nil
 				},
-				qualifyFreightFn: func(context.Context, string, string, string) error {
+				verifyFreightInStageFn: func(context.Context, string, string, string) error {
 					return nil
 				},
 				isAutoPromotionPermittedFn: func(
@@ -491,7 +493,7 @@ func TestSyncNormalStage(t *testing.T) {
 				getLatestAvailableFreightFn: func(
 					context.Context,
 					string,
-					kargoapi.Subscriptions,
+					*kargoapi.Stage,
 				) (*kargoapi.Freight, error) {
 					return nil, errors.New("something went wrong")
 				},
@@ -535,7 +537,7 @@ func TestSyncNormalStage(t *testing.T) {
 				) *kargoapi.Health {
 					return nil
 				},
-				qualifyFreightFn: func(context.Context, string, string, string) error {
+				verifyFreightInStageFn: func(context.Context, string, string, string) error {
 					return nil
 				},
 				isAutoPromotionPermittedFn: func(
@@ -548,7 +550,7 @@ func TestSyncNormalStage(t *testing.T) {
 				getLatestAvailableFreightFn: func(
 					context.Context,
 					string,
-					kargoapi.Subscriptions,
+					*kargoapi.Stage,
 				) (*kargoapi.Freight, error) {
 					return nil, nil
 				},
@@ -588,7 +590,7 @@ func TestSyncNormalStage(t *testing.T) {
 				) *kargoapi.Health {
 					return nil
 				},
-				qualifyFreightFn: func(context.Context, string, string, string) error {
+				verifyFreightInStageFn: func(context.Context, string, string, string) error {
 					return nil
 				},
 				isAutoPromotionPermittedFn: func(
@@ -601,7 +603,7 @@ func TestSyncNormalStage(t *testing.T) {
 				getLatestAvailableFreightFn: func(
 					context.Context,
 					string,
-					kargoapi.Subscriptions,
+					*kargoapi.Stage,
 				) (*kargoapi.Freight, error) {
 					return &kargoapi.Freight{
 						ObjectMeta: metav1.ObjectMeta{
@@ -640,7 +642,7 @@ func TestSyncNormalStage(t *testing.T) {
 				) *kargoapi.Health {
 					return nil
 				},
-				qualifyFreightFn: func(context.Context, string, string, string) error {
+				verifyFreightInStageFn: func(context.Context, string, string, string) error {
 					return nil
 				},
 				isAutoPromotionPermittedFn: func(
@@ -653,7 +655,7 @@ func TestSyncNormalStage(t *testing.T) {
 				getLatestAvailableFreightFn: func(
 					context.Context,
 					string,
-					kargoapi.Subscriptions,
+					*kargoapi.Stage,
 				) (*kargoapi.Freight, error) {
 					return &kargoapi.Freight{
 						ObjectMeta: metav1.ObjectMeta{
@@ -702,7 +704,7 @@ func TestSyncNormalStage(t *testing.T) {
 				) *kargoapi.Health {
 					return nil
 				},
-				qualifyFreightFn: func(context.Context, string, string, string) error {
+				verifyFreightInStageFn: func(context.Context, string, string, string) error {
 					return nil
 				},
 				isAutoPromotionPermittedFn: func(
@@ -715,7 +717,7 @@ func TestSyncNormalStage(t *testing.T) {
 				getLatestAvailableFreightFn: func(
 					context.Context,
 					string,
-					kargoapi.Subscriptions,
+					*kargoapi.Stage,
 				) (*kargoapi.Freight, error) {
 					return &kargoapi.Freight{
 						ObjectMeta: metav1.ObjectMeta{
@@ -781,7 +783,7 @@ func TestSyncNormalStage(t *testing.T) {
 						Status: kargoapi.HealthStateHealthy,
 					}
 				},
-				qualifyFreightFn: func(context.Context, string, string, string) error {
+				verifyFreightInStageFn: func(context.Context, string, string, string) error {
 					return nil
 				},
 				isAutoPromotionPermittedFn: func(
@@ -794,7 +796,7 @@ func TestSyncNormalStage(t *testing.T) {
 				getLatestAvailableFreightFn: func(
 					context.Context,
 					string,
-					kargoapi.Subscriptions,
+					*kargoapi.Stage,
 				) (*kargoapi.Freight, error) {
 					return &kargoapi.Freight{
 						ObjectMeta: metav1.ObjectMeta{
@@ -911,7 +913,7 @@ func TestHasNonTerminalPromotions(t *testing.T) {
 	}
 }
 
-func TestQualifyFreight(t *testing.T) {
+func TestVerifyFreightInStage(t *testing.T) {
 	testCases := []struct {
 		name       string
 		reconciler *reconciler
@@ -951,7 +953,7 @@ func TestQualifyFreight(t *testing.T) {
 			},
 		},
 		{
-			name: "Freight already qualified for Stage",
+			name: "Freight already verified in Stage",
 			reconciler: &reconciler{
 				getFreightFn: func(
 					context.Context,
@@ -960,7 +962,7 @@ func TestQualifyFreight(t *testing.T) {
 				) (*kargoapi.Freight, error) {
 					return &kargoapi.Freight{
 						Status: kargoapi.FreightStatus{
-							Qualifications: map[string]kargoapi.Qualification{
+							VerifiedIn: map[string]kargoapi.VerifiedStage{
 								"fake-stage": {},
 							},
 						},
@@ -1020,7 +1022,7 @@ func TestQualifyFreight(t *testing.T) {
 	for _, testCase := range testCases {
 		t.Run(testCase.name, func(t *testing.T) {
 			testCase.assertions(
-				testCase.reconciler.qualifyFreight(
+				testCase.reconciler.verifyFreightInStage(
 					context.Background(),
 					"fake-namespace",
 					"fake-freight",
@@ -1153,15 +1155,16 @@ func TestIsAutoPromotionPermitted(t *testing.T) {
 }
 
 func TestGetLatestAvailableFreight(t *testing.T) {
+	now := time.Now().UTC()
 	testCases := []struct {
 		name       string
-		subs       kargoapi.Subscriptions
+		subs       *kargoapi.Subscriptions
 		reconciler *reconciler
 		assertions func(*kargoapi.Freight, error)
 	}{
 		{
 			name: "error getting latest Freight from Warehouse",
-			subs: kargoapi.Subscriptions{
+			subs: &kargoapi.Subscriptions{
 				Warehouse: "fake-warehouse",
 			},
 			reconciler: &reconciler{
@@ -1181,7 +1184,7 @@ func TestGetLatestAvailableFreight(t *testing.T) {
 		},
 		{
 			name: "found no Freight from Warehouse",
-			subs: kargoapi.Subscriptions{
+			subs: &kargoapi.Subscriptions{
 				Warehouse: "fake-warehouse",
 			},
 			reconciler: &reconciler{
@@ -1200,7 +1203,7 @@ func TestGetLatestAvailableFreight(t *testing.T) {
 		},
 		{
 			name: "success getting latest Freight from Warehouse",
-			subs: kargoapi.Subscriptions{
+			subs: &kargoapi.Subscriptions{
 				Warehouse: "fake-warehouse",
 			},
 			reconciler: &reconciler{
@@ -1218,12 +1221,12 @@ func TestGetLatestAvailableFreight(t *testing.T) {
 			},
 		},
 		{
-			name: "error getting latest qualified Freight for Stage",
-			subs: kargoapi.Subscriptions{
+			name: "error getting latest Freight verified in upstream Stages",
+			subs: &kargoapi.Subscriptions{
 				UpstreamStages: []kargoapi.StageSubscription{{}},
 			},
 			reconciler: &reconciler{
-				getLatestFreightQualifiedForUpstreamStagesFn: func(
+				getLatestVerifiedFreightFn: func(
 					context.Context,
 					string,
 					[]kargoapi.StageSubscription,
@@ -1237,23 +1240,61 @@ func TestGetLatestAvailableFreight(t *testing.T) {
 				require.Contains(
 					t,
 					err.Error(),
-					"error finding Freight qualified for Stage",
+					"error finding latest Freight verified in Stages upstream from Stage",
 				)
 			},
 		},
 		{
-			name: "found no qualified Freight for Stage",
-			subs: kargoapi.Subscriptions{
+			name: "error getting latest Freight approved for Stage",
+			subs: &kargoapi.Subscriptions{
 				UpstreamStages: []kargoapi.StageSubscription{{}},
 			},
 			reconciler: &reconciler{
-				getLatestFreightQualifiedForUpstreamStagesFn: func(
+				getLatestVerifiedFreightFn: func(
 					context.Context,
 					string,
 					[]kargoapi.StageSubscription,
 				) (*kargoapi.Freight, error) {
 					return nil, nil
 				},
+				getLatestApprovedFreightFn: func(
+					context.Context,
+					string,
+					string,
+				) (*kargoapi.Freight, error) {
+					return nil, errors.New("something went wrong")
+				},
+			},
+			assertions: func(_ *kargoapi.Freight, err error) {
+				require.Error(t, err)
+				require.Contains(t, err.Error(), "something went wrong")
+				require.Contains(
+					t,
+					err.Error(),
+					"error finding latest Freight approved for Stage",
+				)
+			},
+		},
+		{
+			name: "found no suitable Freight",
+			subs: &kargoapi.Subscriptions{
+				UpstreamStages: []kargoapi.StageSubscription{{}},
+			},
+			reconciler: &reconciler{
+				getLatestVerifiedFreightFn: func(
+					context.Context,
+					string,
+					[]kargoapi.StageSubscription,
+				) (*kargoapi.Freight, error) {
+					return nil, nil
+				},
+				getLatestApprovedFreightFn: func(
+					context.Context,
+					string,
+					string,
+				) (*kargoapi.Freight, error) {
+					return nil, nil
+				},
 			},
 			assertions: func(freight *kargoapi.Freight, err error) {
 				require.NoError(t, err)
@@ -1261,124 +1302,151 @@ func TestGetLatestAvailableFreight(t *testing.T) {
 			},
 		},
 		{
-			name: "success getting latest qualified Freight for Stage",
-			subs: kargoapi.Subscriptions{
+			name: "only found latest Freight verified in upstream Stages",
+			subs: &kargoapi.Subscriptions{
 				UpstreamStages: []kargoapi.StageSubscription{{}},
 			},
 			reconciler: &reconciler{
-				getLatestFreightQualifiedForUpstreamStagesFn: func(
+				getLatestVerifiedFreightFn: func(
 					context.Context,
 					string,
 					[]kargoapi.StageSubscription,
 				) (*kargoapi.Freight, error) {
 					return &kargoapi.Freight{}, nil
 				},
+				getLatestApprovedFreightFn: func(
+					context.Context,
+					string,
+					string,
+				) (*kargoapi.Freight, error) {
+					return nil, nil
+				},
 			},
 			assertions: func(freight *kargoapi.Freight, err error) {
 				require.NoError(t, err)
 				require.NotNil(t, freight)
 			},
 		},
-	}
-	for _, testCase := range testCases {
-		t.Run(testCase.name, func(t *testing.T) {
-			testCase.assertions(
-				testCase.reconciler.getLatestAvailableFreight(
-					context.Background(),
-					"fake-namespace",
-					testCase.subs,
-				),
-			)
-		})
-	}
-}
-
-func TestGetAllFreightFromWarehouse(t *testing.T) {
-	testCases := []struct {
-		name       string
-		reconciler *reconciler
-		assertions func([]kargoapi.Freight, error)
-	}{
 		{
-			name: "error listing Freight",
+			name: "only found latest Freight approved for Stage",
+			subs: &kargoapi.Subscriptions{
+				UpstreamStages: []kargoapi.StageSubscription{{}},
+			},
 			reconciler: &reconciler{
-				listFreightFn: func(
+				getLatestVerifiedFreightFn: func(
 					context.Context,
-					client.ObjectList,
-					...client.ListOption,
-				) error {
-					return errors.New("something went wrong")
+					string,
+					[]kargoapi.StageSubscription,
+				) (*kargoapi.Freight, error) {
+					return nil, nil
+				},
+				getLatestApprovedFreightFn: func(
+					context.Context,
+					string,
+					string,
+				) (*kargoapi.Freight, error) {
+					return &kargoapi.Freight{}, nil
 				},
 			},
-			assertions: func(_ []kargoapi.Freight, err error) {
-				require.Error(t, err)
-				require.Contains(t, err.Error(), "something went wrong")
-				require.Contains(t, err.Error(), "error listing Freight for Warehouse")
+			assertions: func(freight *kargoapi.Freight, err error) {
+				require.NoError(t, err)
+				require.NotNil(t, freight)
 			},
 		},
 		{
-			name: "no Freight found",
+			name: "latest verified Freight is newer than latest approved Freight",
+			subs: &kargoapi.Subscriptions{
+				UpstreamStages: []kargoapi.StageSubscription{{}},
+			},
 			reconciler: &reconciler{
-				listFreightFn: func(
+				getLatestVerifiedFreightFn: func(
 					context.Context,
-					client.ObjectList,
-					...client.ListOption,
-				) error {
-					return nil
+					string,
+					[]kargoapi.StageSubscription,
+				) (*kargoapi.Freight, error) {
+					return &kargoapi.Freight{
+						ObjectMeta: metav1.ObjectMeta{
+							Name: "newer-freight",
+							CreationTimestamp: metav1.Time{
+								Time: now,
+							},
+						},
+					}, nil
+				},
+				getLatestApprovedFreightFn: func(
+					context.Context,
+					string,
+					string,
+				) (*kargoapi.Freight, error) {
+					return &kargoapi.Freight{
+						ObjectMeta: metav1.ObjectMeta{
+							Name: "older-freight",
+							CreationTimestamp: metav1.Time{
+								Time: now.Add(-time.Hour),
+							},
+						},
+					}, nil
 				},
 			},
-			assertions: func(freight []kargoapi.Freight, err error) {
+			assertions: func(freight *kargoapi.Freight, err error) {
 				require.NoError(t, err)
-				require.Nil(t, freight)
+				require.NotNil(t, freight)
+				require.Equal(t, "newer-freight", freight.Name)
 			},
 		},
 		{
-			name: "success",
+			name: "latest approved Freight is newer than latest verified Freight",
+			subs: &kargoapi.Subscriptions{
+				UpstreamStages: []kargoapi.StageSubscription{{}},
+			},
 			reconciler: &reconciler{
-				listFreightFn: func(
-					_ context.Context,
-					objList client.ObjectList,
-					_ ...client.ListOption,
-				) error {
-					freight, ok := objList.(*kargoapi.FreightList)
-					require.True(t, ok)
-					freight.Items = []kargoapi.Freight{
-						{
-							ObjectMeta: metav1.ObjectMeta{
-								Name: "older-freight",
-								CreationTimestamp: metav1.Time{
-									Time: time.Now().Add(-time.Hour),
-								},
+				getLatestVerifiedFreightFn: func(
+					context.Context,
+					string,
+					[]kargoapi.StageSubscription,
+				) (*kargoapi.Freight, error) {
+					return &kargoapi.Freight{
+						ObjectMeta: metav1.ObjectMeta{
+							Name: "older-freight",
+							CreationTimestamp: metav1.Time{
+								Time: now.Add(-time.Hour),
 							},
 						},
-						{
-							ObjectMeta: metav1.ObjectMeta{
-								Name: "newer-freight",
-								CreationTimestamp: metav1.Time{
-									Time: time.Now(),
-								},
+					}, nil
+				},
+				getLatestApprovedFreightFn: func(
+					context.Context,
+					string,
+					string,
+				) (*kargoapi.Freight, error) {
+					return &kargoapi.Freight{
+						ObjectMeta: metav1.ObjectMeta{
+							Name: "newer-freight",
+							CreationTimestamp: metav1.Time{
+								Time: now,
 							},
 						},
-					}
-					return nil
+					}, nil
 				},
 			},
-			assertions: func(freight []kargoapi.Freight, err error) {
+			assertions: func(freight *kargoapi.Freight, err error) {
 				require.NoError(t, err)
-				require.Len(t, freight, 2)
-				// Be sure they've been sorted
-				require.Equal(t, "newer-freight", freight[0].Name)
-				require.Equal(t, "older-freight", freight[1].Name)
+				require.NotNil(t, freight)
+				require.Equal(t, "newer-freight", freight.Name)
 			},
 		},
 	}
 	for _, testCase := range testCases {
 		t.Run(testCase.name, func(t *testing.T) {
 			testCase.assertions(
-				testCase.reconciler.getAllFreightFromWarehouse(
+				testCase.reconciler.getLatestAvailableFreight(
 					context.Background(),
 					"fake-namespace",
-					"fake-warehouse",
+					&kargoapi.Stage{
+						Spec: &kargoapi.StageSpec{
+							Subscriptions: testCase.subs,
+						},
+					},
 				),
 			)
 		})
@@ -1392,14 +1460,14 @@ func TestGetLatestFreightFromWarehouse(t *testing.T) {
 		assertions func(*kargoapi.Freight, error)
 	}{
 		{
-			name: "error getting all Freight from Warehouse",
+			name: "error listing Freight from Warehouse",
 			reconciler: &reconciler{
-				getAllFreightFromWarehouseFn: func(
+				listFreightFn: func(
 					context.Context,
-					string,
-					string,
-				) ([]kargoapi.Freight, error) {
-					return nil, errors.New("something went wrong")
+					client.ObjectList,
+					...client.ListOption,
+				) error {
+					return errors.New("something went wrong")
 				},
 			},
 			assertions: func(_ *kargoapi.Freight, err error) {
@@ -1410,12 +1478,12 @@ func TestGetLatestFreightFromWarehouse(t *testing.T) {
 		{
 			name: "no Freight found from Warehouse",
 			reconciler: &reconciler{
-				getAllFreightFromWarehouseFn: func(
+				listFreightFn: func(
 					context.Context,
-					string,
-					string,
-				) ([]kargoapi.Freight, error) {
-					return nil, nil
+					client.ObjectList,
+					...client.ListOption,
+				) error {
+					return nil
 				},
 			},
 			assertions: func(freight *kargoapi.Freight, err error) {
@@ -1426,12 +1494,14 @@ func TestGetLatestFreightFromWarehouse(t *testing.T) {
 		{
 			name: "success",
 			reconciler: &reconciler{
-				getAllFreightFromWarehouseFn: func(
-					context.Context,
-					string,
-					string,
-				) ([]kargoapi.Freight, error) {
-					return []kargoapi.Freight{
+				listFreightFn: func(
+					_ context.Context,
+					objList client.ObjectList,
+					_ ...client.ListOption,
+				) error {
+					freight, ok := objList.(*kargoapi.FreightList)
+					require.True(t, ok)
+					freight.Items = []kargoapi.Freight{
 						{
 							ObjectMeta: metav1.ObjectMeta{
 								Name: "newer-freight",
@@ -1448,7 +1518,8 @@ func TestGetLatestFreightFromWarehouse(t *testing.T) {
 								},
 							},
 						},
-					}, nil
+					}
+					return nil
 				},
 			},
 			assertions: func(freight *kargoapi.Freight, err error) {
@@ -1472,7 +1543,7 @@ func TestGetLatestFreightFromWarehouse(t *testing.T) {
 	}
 }
 
-func TestGetAllFreightQualifiedForUpstreamStages(t *testing.T) {
+func TestGetAllVerifiedFreight(t *testing.T) {
 	testCases := []struct {
 		name       string
 		reconciler *reconciler
@@ -1495,7 +1566,7 @@ func TestGetAllFreightQualifiedForUpstreamStages(t *testing.T) {
 				require.Contains(
 					t,
 					err.Error(),
-					"error listing Freight qualified for Stage",
+					"error listing Freight verified in Stage",
 				)
 			},
 		},
@@ -1528,15 +1599,12 @@ func TestGetAllFreightQualifiedForUpstreamStages(t *testing.T) {
 					freight.Items = []kargoapi.Freight{
 						{
 							ObjectMeta: metav1.ObjectMeta{
-								Name: "older-freight",
-								CreationTimestamp: metav1.Time{
-									Time: time.Now().Add(-time.Hour),
-								},
+								Name: "fake-freight",
 							},
 						},
 						{
 							ObjectMeta: metav1.ObjectMeta{
-								Name: "newer-freight",
+								Name: "another-fake-freight",
 								CreationTimestamp: metav1.Time{
 									Time: time.Now(),
 								},
@@ -1549,16 +1617,14 @@ func TestGetAllFreightQualifiedForUpstreamStages(t *testing.T) {
 			assertions: func(freight []kargoapi.Freight, err error) {
 				require.NoError(t, err)
 				require.NotNil(t, freight)
-				// Be sure they've been sorted// Be sure we got the newest one
-				require.Equal(t, "newer-freight", freight[0].Name)
-				require.Equal(t, "older-freight", freight[1].Name)
+				require.Len(t, freight, 2)
 			},
 		},
 	}
 	for _, testCase := range testCases {
 		t.Run(testCase.name, func(t *testing.T) {
 			testCase.assertions(
-				testCase.reconciler.getAllFreightQualifiedForUpstreamStages(
+				testCase.reconciler.getAllVerifiedFreight(
 					context.Background(),
 					"fake-namespace",
 					[]kargoapi.StageSubscription{
@@ -1572,16 +1638,16 @@ func TestGetAllFreightQualifiedForUpstreamStages(t *testing.T) {
 	}
 }
 
-func TestGetLatestFreightQualifiedForUpstreamStages(t *testing.T) {
+func TestGetLatestVerifiedFreight(t *testing.T) {
 	testCases := []struct {
 		name       string
 		reconciler *reconciler
 		assertions func(*kargoapi.Freight, error)
 	}{
 		{
-			name: "error getting all Freight qualified for upstream Stages",
+			name: "error getting all Freight verified in upstream Stages",
 			reconciler: &reconciler{
-				getAllFreightQualifiedForUpstreamStagesFn: func(
+				getAllVerifiedFreightFn: func(
 					context.Context,
 					string,
 					[]kargoapi.StageSubscription,
@@ -1595,9 +1661,9 @@ func TestGetLatestFreightQualifiedForUpstreamStages(t *testing.T) {
 			},
 		},
 		{
-			name: "no Freight qualified for upstream Stages",
+			name: "no Freight verified in upstream Stages",
 			reconciler: &reconciler{
-				getAllFreightQualifiedForUpstreamStagesFn: func(
+				getAllVerifiedFreightFn: func(
 					context.Context,
 					string,
 					[]kargoapi.StageSubscription,
@@ -1613,7 +1679,7 @@ func TestGetLatestFreightQualifiedForUpstreamStages(t *testing.T) {
 		{
 			name: "success",
 			reconciler: &reconciler{
-				getAllFreightQualifiedForUpstreamStagesFn: func(
+				getAllVerifiedFreightFn: func(
 					context.Context,
 					string,
 					[]kargoapi.StageSubscription,
@@ -1649,7 +1715,7 @@ func TestGetLatestFreightQualifiedForUpstreamStages(t *testing.T) {
 	for _, testCase := range testCases {
 		t.Run(testCase.name, func(t *testing.T) {
 			testCase.assertions(
-				testCase.reconciler.getLatestFreightQualifiedForUpstreamStages(
+				testCase.reconciler.getLatestVerifiedFreight(
 					context.Background(),
 					"fake-namespace",
 					[]kargoapi.StageSubscription{},
diff --git a/internal/controller/warehouses/warehouses_test.go b/internal/controller/warehouses/warehouses_test.go
index 15166774..ec51f0bb 100644
--- a/internal/controller/warehouses/warehouses_test.go
+++ b/internal/controller/warehouses/warehouses_test.go
@@ -35,10 +35,12 @@ func TestNewReconciler(t *testing.T) {
 	require.NotNil(t, e.getLatestChartsFn)
 	require.NotNil(t, e.getLatestChartVersionFn)
 	require.NotNil(t, e.getLatestCommitMetaFn)
+	require.NotNil(t, e.getAvailableFreightAliasFn)
 	require.NotNil(t, e.createFreightFn)
 }
 
 func TestSyncWarehouse(t *testing.T) {
+	const fakeAlias = "fake-alias"
 	scheme := k8sruntime.NewScheme()
 	require.NoError(t, kargoapi.SchemeBuilder.AddToScheme(scheme))
 	testWarehouse := &kargoapi.Warehouse{
@@ -85,6 +87,30 @@ func TestSyncWarehouse(t *testing.T) {
 			},
 		},
 
+		{
+			name: "error getting alias for Freight",
+			reconciler: &reconciler{
+				getLatestFreightFromReposFn: func(
+					context.Context,
+					*kargoapi.Warehouse,
+				) (*kargoapi.Freight, error) {
+					return &kargoapi.Freight{}, nil
+				},
+				getAvailableFreightAliasFn: func(context.Context) (string, error) {
+					return "", errors.New("something went wrong")
+				},
+			},
+			assertions: func(err error) {
+				require.Error(t, err)
+				require.Contains(t, err.Error(), "something went wrong")
+				require.Contains(
+					t,
+					err.Error(),
+					"error getting available Freight alias",
+				)
+			},
+		},
+
 		{
 			name: "latest Freight from repos isn't new",
 			reconciler: &reconciler{
@@ -94,6 +120,9 @@ func TestSyncWarehouse(t *testing.T) {
 				) (*kargoapi.Freight, error) {
 					return &kargoapi.Freight{}, nil
 				},
+				getAvailableFreightAliasFn: func(context.Context) (string, error) {
+					return fakeAlias, nil
+				},
 				createFreightFn: func(
 					context.Context,
 					client.Object,
@@ -122,6 +151,9 @@ func TestSyncWarehouse(t *testing.T) {
 				) (*kargoapi.Freight, error) {
 					return &kargoapi.Freight{}, nil
 				},
+				getAvailableFreightAliasFn: func(context.Context) (string, error) {
+					return fakeAlias, nil
+				},
 				createFreightFn: func(
 					context.Context,
 					client.Object,
@@ -151,6 +183,9 @@ func TestSyncWarehouse(t *testing.T) {
 						},
 					}, nil
 				},
+				getAvailableFreightAliasFn: func(context.Context) (string, error) {
+					return fakeAlias, nil
+				},
 				createFreightFn: func(
 					context.Context,
 					client.Object,
diff --git a/internal/kubeclient/indexer_test.go b/internal/kubeclient/indexer_test.go
index 0cda4a51..8acd1677 100644
--- a/internal/kubeclient/indexer_test.go
+++ b/internal/kubeclient/indexer_test.go
@@ -258,22 +258,22 @@ func TestIndexFreightByWarehouse(t *testing.T) {
 	}
 }
 
-func TestIndexFreightByQualifiedStages(t *testing.T) {
+func TestIndexFreightByVerifiedStages(t *testing.T) {
 	testCases := []struct {
 		name     string
 		freight  *kargoapi.Freight
 		expected []string
 	}{
 		{
-			name:     "Freight has no qualified stages",
+			name:     "Freight is not verified in any Stages",
 			freight:  &kargoapi.Freight{},
 			expected: []string{},
 		},
 		{
-			name: "Freight has qualified stages",
+			name: "Freight is verified in a Stage",
 			freight: &kargoapi.Freight{
 				Status: kargoapi.FreightStatus{
-					Qualifications: map[string]kargoapi.Qualification{
+					VerifiedIn: map[string]kargoapi.VerifiedStage{
 						"fake-stage": {},
 					},
 				},
@@ -287,7 +287,43 @@ func TestIndexFreightByQualifiedStages(t *testing.T) {
 				require.Equal(
 					t,
 					testCase.expected,
-					indexFreightByQualifiedStages(testCase.freight),
+					indexFreightByVerifiedStages(testCase.freight),
+				)
+			})
+		})
+	}
+}
+
+func TestIndexFreightByApprovedStages(t *testing.T) {
+	testCases := []struct {
+		name     string
+		freight  *kargoapi.Freight
+		expected []string
+	}{
+		{
+			name:     "Freight is not approved for any Stages",
+			freight:  &kargoapi.Freight{},
+			expected: []string{},
+		},
+		{
+			name: "Freight is approved for a Stage",
+			freight: &kargoapi.Freight{
+				Status: kargoapi.FreightStatus{
+					ApprovedFor: map[string]kargoapi.ApprovedStage{
+						"fake-stage": {},
+					},
+				},
+			},
+			expected: []string{"fake-stage"},
+		},
+	}
+	for _, testCase := range testCases {
+		t.Run(testCase.name, func(t *testing.T) {
+			t.Run(testCase.name, func(t *testing.T) {
+				require.Equal(
+					t,
+					testCase.expected,
+					indexFreightByApprovedStages(testCase.freight),
 				)
 			})
 		})