Repository: akuity/kargo
Pull request #1865 (2024-04-18 17:14:06)

refactor: use normalized URLs in constructing freight id
Follow-up to #1854.

I've realized that we should use normalized URLs when constructing Freight IDs.

This way, if someone updates a subscription in a Warehouse to use a new URL that is semantically equivalent to the old URL, we won't end up with a new piece of Freight just because of that.


Patch:
diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml
index 6c6666a3..7935831d 100644
--- a/.github/workflows/release.yaml
+++ b/.github/workflows/release.yaml
@@ -181,6 +181,26 @@ jobs:
         key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
         restore-keys: |
           ${{ runner.os }}-go-
+    - name: Install pnpm
+      uses: pnpm/action-setup@v3
+      with:
+        version: 8
+        run_install: false
+    - name: Install nodejs
+      uses: actions/setup-node@v4
+      with:
+        node-version: "20.12.2"
+        cache: "pnpm"
+        cache-dependency-path: "**/pnpm-lock.yaml"
+    - name: Build UI
+      env:
+        VERSION: ${{ github.ref_name }}
+      working-directory: ./ui
+      run: |
+        pnpm install
+        NODE_ENV=production pnpm run build
+        rm -rf ../internal/api/ui
+        mv build ../internal/api/ui
     - name: Build CLI
       env:
         GOFLAGS: -buildvcs=false
@@ -210,10 +230,11 @@ jobs:
     runs-on: ubuntu-latest
     container:
       image: golang:1.22.2-bookworm
+    strategy:
+      matrix:
+        os: [linux, darwin, windows]
+        arch: [amd64, arm64]
     steps:
-    - name: Install awscli
-      run: |
-        apt update && apt install awscli -y
     - name: Checkout code
       uses: actions/checkout@v4
     - uses: actions/cache@v4
@@ -222,13 +243,38 @@ jobs:
         key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
         restore-keys: |
           ${{ runner.os }}-go-
+    - name: Install pnpm
+      uses: pnpm/action-setup@v3
+      with:
+        version: 8
+        run_install: false
+    - name: Install nodejs
+      uses: actions/setup-node@v4
+      with:
+        node-version: "20.12.2"
+        cache: "pnpm"
+        cache-dependency-path: "**/pnpm-lock.yaml"
+    - name: Build UI
+      env:
+        VERSION: ${{ github.ref_name }}
+      working-directory: ./ui
+      run: |
+        pnpm install
+        NODE_ENV=production pnpm run build
+        rm -rf ../internal/api/ui
+        mv build ../internal/api/ui
     - name: Build CLI
       env:
         GOFLAGS: -buildvcs=false
+        GOOS: ${{ matrix.os }}
+        GOARCH: ${{ matrix.arch }}
         VERSION: ${{ needs.publish-image.outputs.unstable-version }}
         GIT_COMMIT: ${{ github.sha }}
         GIT_TREE_STATE: clean
-      run: make nightly-cli
+      run: make build-nightly-cli
+    - name: Install awscli
+      run: |
+        apt update && apt install awscli -y
     - name: Configure AWS credentials
       uses: aws-actions/configure-aws-credentials@v4
       with:
@@ -239,7 +285,7 @@ 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}"
+        aws s3 sync "./bin/kargo-cli/${VERSION}/${{ matrix.os }}/${{ matrix.arch }}" "s3://kargo-release/kargo-cli/${VERSION}/${{ matrix.os }}/${{ matrix.arch }}"
         printf "${VERSION}" > ./bin/kargo-cli/unstable.txt
         aws s3 cp ./bin/kargo-cli/unstable.txt s3://kargo-release/kargo-cli/unstable.txt
         aws cloudfront create-invalidation \
diff --git a/Dockerfile b/Dockerfile
index 3e4d5fce..e595ddce 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,3 +1,19 @@
+####################################################################################################
+# ui-builder
+####################################################################################################
+FROM --platform=$BUILDPLATFORM docker.io/library/node:20.12.2 AS ui-builder
+
+RUN npm install --global pnpm
+WORKDIR /ui
+COPY ["ui/package.json", "ui/pnpm-lock.yaml", "./"]
+ARG VERSION
+
+RUN pnpm install
+
+COPY ["ui/", "."]
+
+RUN NODE_ENV='production' VERSION=${VERSION} pnpm run build
+
 ####################################################################################################
 # back-end-builder
 ####################################################################################################
@@ -17,6 +33,7 @@ COPY api/ api/
 COPY cmd/ cmd/
 COPY internal/ internal/
 COPY pkg/ pkg/
+COPY --from=ui-builder /ui/build internal/api/ui/
 
 ARG VERSION
 ARG GIT_COMMIT
@@ -30,22 +47,6 @@ RUN GOOS=${TARGETOS} GOARCH=${TARGETARCH} go build \
 
 WORKDIR /kargo/bin
 
-####################################################################################################
-# ui-builder
-####################################################################################################
-FROM --platform=$BUILDPLATFORM docker.io/library/node:20.12.1 AS ui-builder
-
-RUN npm install --global pnpm
-WORKDIR /ui
-COPY ["ui/package.json", "ui/pnpm-lock.yaml", "./"]
-ARG VERSION
-
-RUN pnpm install
-
-COPY ["ui/", "."]
-
-RUN NODE_ENV='production' VERSION=${VERSION} pnpm run build
-
 ####################################################################################################
 # tools
 ####################################################################################################
@@ -87,7 +88,7 @@ CMD ["/usr/local/bin/kargo"]
 # - supports development
 # - not used for official image builds
 ####################################################################################################
-FROM --platform=$BUILDPLATFORM docker.io/library/node:20.12.1 AS ui-dev
+FROM --platform=$BUILDPLATFORM docker.io/library/node:20.12.2 AS ui-dev
 
 RUN npm install --global pnpm
 WORKDIR /ui
@@ -110,7 +111,6 @@ USER root
 
 COPY --from=back-end-builder /kargo/bin/ /usr/local/bin/
 COPY --from=tools /tools/ /usr/local/bin/
-COPY --from=ui-builder /ui/build /ui/build
 
 USER 1000:0
 
diff --git a/Makefile b/Makefile
index bf0681ee..77a73feb 100644
--- a/Makefile
+++ b/Makefile
@@ -115,20 +115,11 @@ build-cli:
 # Used for Nighty/Unstable builds                                              #
 ################################################################################
 
-.PHONY: kargo-all
-kargo-all:
+.PHONY: build-nightly-cli
+build-nightly-cli:
 	CGO_ENABLED=0 go build \
 		-ldflags "-w -X $(VERSION_PACKAGE).version=$(VERSION) -X $(VERSION_PACKAGE).buildDate=$$(date -u +'%Y-%m-%dT%H:%M:%SZ') -X $(VERSION_PACKAGE).gitCommit=$(GIT_COMMIT) -X $(VERSION_PACKAGE).gitTreeState=$(GIT_TREE_STATE)" \
-		-o bin/kargo-cli/${VERSION}/${GOOS}/${GOARCH}/${BIN_NAME} ./cmd/cli
-
-.PHONY: nightly-cli
-nightly-cli:
-	make BIN_NAME=kargo GOOS=darwin GOARCH=amd64 kargo-all
-	make BIN_NAME=kargo GOOS=darwin GOARCH=arm64 kargo-all
-	make BIN_NAME=kargo GOOS=linux GOARCH=amd64 kargo-all
-	make BIN_NAME=kargo GOOS=linux GOARCH=arm64 kargo-all
-	make BIN_NAME=kargo.exe GOOS=windows GOARCH=amd64 kargo-all
-	make BIN_NAME=kargo.exe GOOS=windows GOARCH=arm64 kargo-all
+		-o bin/kargo-cli/${VERSION}/${GOOS}/${GOARCH}/kargo$(shell [ ${GOOS} = windows ] && echo .exe) ./cmd/cli
 
 ################################################################################
 # Code generation: To be run after modifications to API types                  #
diff --git a/api/v1alpha1/annotations.go b/api/v1alpha1/annotations.go
index a6e8c274..45c17be0 100644
--- a/api/v1alpha1/annotations.go
+++ b/api/v1alpha1/annotations.go
@@ -1,13 +1,27 @@
 package v1alpha1
 
-const (
-	AnnotationKeyDescription = "kargo.akuity.io/description"
+import "encoding/json"
 
+const (
+	// AnnotationKeyRefresh is an annotation key that can be set on a resource
+	// to trigger a refresh of the resource by the controller. The value of the
+	// annotation is interpreted as a token, and any change to the value of the
+	// annotation should trigger a reconciliation of the resource.
 	AnnotationKeyRefresh = "kargo.akuity.io/refresh"
 
-	AnnotationKeyReverify      = "kargo.akuity.io/reverify"
-	AnnotationKeyReverifyActor = "kargo.akuity.io/reverify-actor"
-	AnnotationKeyAbort         = "kargo.akuity.io/abort"
+	// AnnotationKeyReverify is an annotation key that can be set on a Stage
+	// resource to trigger the re-verification of its Freight. The value of the
+	// annotation should either be the ID of the verification to be reverified,
+	// or a JSON object with the structure of the VerificationRequest.
+	AnnotationKeyReverify = "kargo.akuity.io/reverify"
+
+	// AnnotationKeyAbort is an annotation key that can be set on a Stage
+	// resource to abort the verification of its Freight. The value of the
+	// annotation must be set to the identifier of the verification to be
+	// aborted.
+	AnnotationKeyAbort = "kargo.akuity.io/abort"
+
+	AnnotationKeyDescription = "kargo.akuity.io/description"
 
 	AnnotationKeyOIDCEmails   = "rbac.kargo.akuity.io/email"
 	AnnotationKeyOIDCGroups   = "rbac.kargo.akuity.io/groups"
@@ -29,3 +43,66 @@ const (
 	AnnotationKeyEventVerificationStartTime  = "event.kargo.akuity.io/verification-start-time"
 	AnnotationKeyEventVerificationFinishTime = "event.kargo.akuity.io/verification-finish-time"
 )
+
+// RefreshAnnotationValue returns the value of the AnnotationKeyRefresh
+// annotation which can be used to detect changes, and a boolean indicating
+// whether the annotation was present.
+func RefreshAnnotationValue(annotations map[string]string) (string, bool) {
+	requested, ok := annotations[AnnotationKeyRefresh]
+	return requested, ok
+}
+
+// ReverifyAnnotationValue returns the value of the AnnotationKeyReverify
+// annotation, which can be used to determine whether the verification of a
+// Freight should be rerun, and a boolean indicating whether the annotation was
+// present.
+//
+// If the value of the annotation is a valid JSON object, it is unmarshalled
+// into a VerificationRequest struct. Otherwise, the value is treated as the ID
+// of the verification to be reverified and set as the ID field of the returned
+// VerificationRequest.
+func ReverifyAnnotationValue(annotations map[string]string) (*VerificationRequest, bool) {
+	requested, ok := annotations[AnnotationKeyReverify]
+	if !ok {
+		return nil, ok
+	}
+	var vr VerificationRequest
+	if b := []byte(requested); json.Valid(b) {
+		if err := json.Unmarshal(b, &vr); err != nil {
+			return nil, false
+		}
+	} else {
+		vr.ID = requested
+	}
+	if !vr.HasID() {
+		return nil, false
+	}
+	return &vr, ok
+}
+
+// AbortAnnotationValue returns the value of the AnnotationKeyAbort annotation
+// which can be used to abort the verification of a Freight, and a boolean
+// indicating whether the annotation was present.
+//
+// If the value of the annotation is a valid JSON object, it is unmarshalled
+// into a VerificationRequest struct. Otherwise, the value is treated as the ID
+// of the verification to be aborted and set as the ID field of the returned
+// VerificationRequest.
+func AbortAnnotationValue(annotations map[string]string) (*VerificationRequest, bool) {
+	requested, ok := annotations[AnnotationKeyAbort]
+	if !ok {
+		return nil, ok
+	}
+	var vr VerificationRequest
+	if b := []byte(requested); json.Valid(b) {
+		if err := json.Unmarshal(b, &vr); err != nil {
+			return nil, false
+		}
+	} else {
+		vr.ID = requested
+	}
+	if !vr.HasID() {
+		return nil, false
+	}
+	return &vr, ok
+}
diff --git a/api/v1alpha1/freight_types.go b/api/v1alpha1/freight_types.go
index ace038d1..3f3e004c 100644
--- a/api/v1alpha1/freight_types.go
+++ b/api/v1alpha1/freight_types.go
@@ -7,6 +7,8 @@ import (
 	"sort"
 	"strings"
 
+	"github.com/akuity/kargo/internal/git"
+	"github.com/akuity/kargo/internal/helm"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 )
 
@@ -65,12 +67,12 @@ func (f *Freight) GenerateID() string {
 			// Freight for the new tag.
 			artifacts = append(
 				artifacts,
-				fmt.Sprintf("%s:%s:%s", commit.RepoURL, commit.Tag, commit.ID),
+				fmt.Sprintf("%s:%s:%s", git.NormalizeURL(commit.RepoURL), commit.Tag, commit.ID),
 			)
 		} else {
 			artifacts = append(
 				artifacts,
-				fmt.Sprintf("%s:%s", commit.RepoURL, commit.ID),
+				fmt.Sprintf("%s:%s", git.NormalizeURL(commit.RepoURL), commit.ID),
 			)
 		}
 	}
@@ -92,7 +94,7 @@ func (f *Freight) GenerateID() string {
 			fmt.Sprintf(
 				"%s:%s",
 				// path.Join accounts for the possibility that chart.Name is empty
-				path.Join(chart.RepoURL, chart.Name),
+				path.Join(helm.NormalizeChartRepositoryURL(chart.RepoURL), chart.Name),
 				chart.Version,
 			),
 		)
diff --git a/api/v1alpha1/generated.pb.go b/api/v1alpha1/generated.pb.go
index 2af09164..914926d1 100644
--- a/api/v1alpha1/generated.pb.go
+++ b/api/v1alpha1/generated.pb.go
@@ -1811,219 +1811,222 @@ func init() {
 }
 
 var fileDescriptor_e26b7f7bbc391025 = []byte{
-	// 3380 bytes of a gzipped FileDescriptorProto
-	0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xd4, 0x1b, 0x4d, 0x6c, 0x1c, 0x57,
-	0x39, 0xb3, 0xbb, 0x5e, 0x7b, 0xbf, 0xf5, 0xef, 0xb3, 0x93, 0xba, 0x2e, 0x71, 0xa2, 0xa1, 0x54,
-	0x2d, 0x6d, 0x77, 0x49, 0x52, 0xb7, 0x69, 0x5a, 0x5a, 0x76, 0x9d, 0x3f, 0xb7, 0x4e, 0x6a, 0x9e,
-	0x9d, 0xb4, 0xa4, 0xad, 0xe0, 0x79, 0xf7, 0x79, 0x77, 0xea, 0xdd, 0x99, 0xe9, 0xcc, 0xac, 0x53,
-	0x53, 0x09, 0x28, 0x50, 0xd1, 0x0b, 0x05, 0x89, 0x4b, 0x39, 0x21, 0x01, 0x02, 0x71, 0x80, 0x1b,
-	0x42, 0x88, 0x03, 0x12, 0x15, 0x52, 0xc5, 0x01, 0x55, 0x9c, 0xca, 0x25, 0x6a, 0xcd, 0x0d, 0xa9,
-	0x70, 0xe0, 0x16, 0x09, 0x09, 0xbd, 0x9f, 0x99, 0x79, 0x33, 0x3b, 0x6b, 0xcf, 0x6c, 0x93, 0xa8,
-	0xdc, 0x76, 0xdf, 0xf7, 0xf7, 0xde, 0xf7, 0xbe, 0xff, 0x99, 0x81, 0x47, 0x5a, 0x86, 0xd7, 0xee,
-	0x6d, 0x56, 0x1a, 0x56, 0xb7, 0x4a, 0xb6, 0x7b, 0x86, 0xb7, 0x5b, 0xdd, 0x26, 0x4e, 0xcb, 0xaa,
-	0x12, 0xdb, 0xa8, 0xee, 0x9c, 0x20, 0x1d, 0xbb, 0x4d, 0x4e, 0x54, 0x5b, 0xd4, 0xa4, 0x0e, 0xf1,
-	0x68, 0xb3, 0x62, 0x3b, 0x96, 0x67, 0xa1, 0x7b, 0x43, 0xaa, 0x8a, 0xa0, 0xaa, 0x70, 0xaa, 0x0a,
-	0xb1, 0x8d, 0x8a, 0x4f, 0xb5, 0xf0, 0xb0, 0xc2, 0xbb, 0x65, 0xb5, 0xac, 0x2a, 0x27, 0xde, 0xec,
-	0x6d, 0xf1, 0x7f, 0xfc, 0x0f, 0xff, 0x25, 0x98, 0x2e, 0x3c, 0xb2, 0x7d, 0xda, 0xad, 0x18, 0x5c,
-	0x72, 0x97, 0x34, 0xda, 0x86, 0x49, 0x9d, 0xdd, 0xaa, 0xbd, 0xdd, 0x62, 0x0b, 0x6e, 0xb5, 0x4b,
-	0x3d, 0x52, 0xdd, 0xe9, 0xdb, 0xca, 0x42, 0x75, 0x10, 0x95, 0xd3, 0x33, 0x3d, 0xa3, 0x4b, 0xfb,
-	0x08, 0x1e, 0x3d, 0x88, 0xc0, 0x6d, 0xb4, 0x69, 0x97, 0xc4, 0xe9, 0xf4, 0x97, 0x60, 0xb6, 0x66,
-	0x92, 0xce, 0xae, 0x6b, 0xb8, 0xb8, 0x67, 0xd6, 0x9c, 0x56, 0xaf, 0x4b, 0x4d, 0x0f, 0x1d, 0x87,
-	0x82, 0x49, 0xba, 0x74, 0x5e, 0x3b, 0xae, 0xdd, 0x5f, 0xaa, 0x8f, 0xbf, 0x77, 0xe3, 0xd8, 0xa1,
-	0xbd, 0x1b, 0xc7, 0x0a, 0x97, 0x49, 0x97, 0x62, 0x0e, 0x41, 0x9f, 0x85, 0x91, 0x1d, 0xd2, 0xe9,
-	0xd1, 0xf9, 0x1c, 0x47, 0x99, 0x90, 0x28, 0x23, 0x57, 0xd9, 0x22, 0x16, 0x30, 0xfd, 0x3b, 0xf9,
-	0x08, 0xfb, 0x4b, 0xd4, 0x23, 0x4d, 0xe2, 0x11, 0xd4, 0x85, 0x62, 0x87, 0x6c, 0xd2, 0x8e, 0x3b,
-	0xaf, 0x1d, 0xcf, 0xdf, 0x5f, 0x3e, 0x79, 0xae, 0x92, 0x46, 0xf5, 0x95, 0x04, 0x56, 0x95, 0x55,
-	0xce, 0xe7, 0x9c, 0xe9, 0x39, 0xbb, 0xf5, 0x49, 0xb9, 0x89, 0xa2, 0x58, 0xc4, 0x52, 0x08, 0x7a,
-	0x43, 0x83, 0x32, 0x31, 0x4d, 0xcb, 0x23, 0x9e, 0x61, 0x99, 0xee, 0x7c, 0x8e, 0x0b, 0x7d, 0x66,
-	0x78, 0xa1, 0xb5, 0x90, 0x99, 0x90, 0x3c, 0x2b, 0x25, 0x97, 0x15, 0x08, 0x56, 0x65, 0x2e, 0x3c,
-	0x0e, 0x65, 0x65, 0xab, 0x68, 0x1a, 0xf2, 0xdb, 0x74, 0x57, 0xe8, 0x17, 0xb3, 0x9f, 0x68, 0x2e,
-	0xa2, 0x50, 0xa9, 0xc1, 0x33, 0xb9, 0xd3, 0xda, 0xc2, 0x53, 0x30, 0x1d, 0x17, 0x98, 0x85, 0x5e,
-	0x7f, 0x5b, 0x83, 0x39, 0xe5, 0x14, 0x98, 0x6e, 0x51, 0x87, 0x9a, 0x0d, 0x8a, 0xaa, 0x50, 0x62,
-	0x77, 0xe9, 0xda, 0xa4, 0xe1, 0x5f, 0xf5, 0x8c, 0x3c, 0x48, 0xe9, 0xb2, 0x0f, 0xc0, 0x21, 0x4e,
-	0x60, 0x16, 0xb9, 0xfd, 0xcc, 0xc2, 0x6e, 0x13, 0x97, 0xce, 0xe7, 0xa3, 0x66, 0xb1, 0xc6, 0x16,
-	0xb1, 0x80, 0xe9, 0x5f, 0x84, 0xbb, 0xfd, 0xfd, 0x6c, 0xd0, 0xae, 0xdd, 0x21, 0x1e, 0x0d, 0x37,
-	0x75, 0xa0, 0xe9, 0xe9, 0x53, 0x30, 0x51, 0xb3, 0x6d, 0xc7, 0xda, 0xa1, 0xcd, 0x75, 0x8f, 0xb4,
-	0xa8, 0xfe, 0x6d, 0x0d, 0x0e, 0xd7, 0x9c, 0x96, 0xb5, 0x7c, 0xb6, 0x66, 0xdb, 0x17, 0x29, 0xe9,
-	0x78, 0xed, 0x75, 0x8f, 0x78, 0x3d, 0x17, 0x3d, 0x05, 0x45, 0x97, 0xff, 0x92, 0xec, 0xee, 0xf3,
-	0x2d, 0x44, 0xc0, 0x6f, 0xde, 0x38, 0x36, 0x97, 0x40, 0x48, 0xb1, 0xa4, 0x42, 0x0f, 0xc0, 0x68,
-	0x97, 0xba, 0x2e, 0x69, 0xf9, 0x67, 0x9e, 0x92, 0x0c, 0x46, 0x2f, 0x89, 0x65, 0xec, 0xc3, 0xf5,
-	0xbf, 0xe4, 0x60, 0x2a, 0xe0, 0x25, 0xc5, 0xdf, 0x06, 0x05, 0xf7, 0x60, 0xbc, 0xad, 0x9c, 0x90,
-	0xeb, 0xb9, 0x7c, 0xf2, 0x89, 0x94, 0xb6, 0x9c, 0xa4, 0xa4, 0xfa, 0x9c, 0x14, 0x33, 0xae, 0xae,
-	0xe2, 0x88, 0x18, 0xd4, 0x05, 0x70, 0x77, 0xcd, 0x86, 0x14, 0x5a, 0xe0, 0x42, 0x1f, 0xcf, 0x28,
-	0x74, 0x3d, 0x60, 0x50, 0x47, 0x52, 0x24, 0x84, 0x6b, 0x58, 0x11, 0xa0, 0xff, 0x46, 0x83, 0xd9,
-	0x04, 0x3a, 0xf4, 0x64, 0xec, 0x3e, 0xef, 0xed, 0xbb, 0x4f, 0xd4, 0x47, 0x16, 0xde, 0xe6, 0x43,
-	0x30, 0xe6, 0xd0, 0x1d, 0xc3, 0x35, 0x2c, 0x53, 0x6a, 0x78, 0x5a, 0xd2, 0x8f, 0x61, 0xb9, 0x8e,
-	0x03, 0x0c, 0xf4, 0x20, 0x94, 0xfc, 0xdf, 0x4c, 0xcd, 0x79, 0x66, 0xce, 0xec, 0xe2, 0x7c, 0x54,
-	0x17, 0x87, 0x70, 0xfd, 0x63, 0x4d, 0xb9, 0xfd, 0x2b, 0x76, 0x93, 0x78, 0x94, 0x19, 0x0f, 0xb1,
-	0xed, 0xcb, 0xa1, 0x31, 0x07, 0xc6, 0x53, 0x13, 0xcb, 0xd8, 0x87, 0xa3, 0xd3, 0x30, 0x2e, 0x7f,
-	0x0a, 0x5b, 0x11, 0xbb, 0x0b, 0x2e, 0xa6, 0xa6, 0xc0, 0x70, 0x04, 0x13, 0xf5, 0x60, 0xc2, 0xb5,
-	0x7a, 0x4e, 0x83, 0x0a, 0xa1, 0x62, 0xa7, 0xe5, 0x93, 0xa7, 0xb3, 0xdc, 0xcd, 0xba, 0xc2, 0xa0,
-	0x7e, 0x58, 0x0a, 0x9d, 0x50, 0x57, 0x5d, 0x1c, 0x95, 0xa2, 0xbf, 0x0a, 0x20, 0x68, 0x2f, 0xd2,
-	0x4e, 0x17, 0x35, 0xa0, 0x68, 0x74, 0x49, 0x8b, 0xfa, 0xf1, 0x3c, 0x93, 0x39, 0x32, 0x0e, 0x2b,
-	0x8c, 0x5a, 0x6e, 0x20, 0x88, 0xe2, 0x7c, 0xd1, 0xc5, 0x92, 0xb5, 0xfe, 0x4e, 0xe0, 0xe5, 0x31,
-	0x0a, 0x16, 0x74, 0x38, 0x8e, 0x54, 0x73, 0x10, 0x74, 0x38, 0x0e, 0x16, 0x30, 0x74, 0x54, 0x44,
-	0x4c, 0xa1, 0xd9, 0xb2, 0x44, 0xc9, 0x3f, 0x4b, 0x77, 0x45, 0xf8, 0x7c, 0xc2, 0x0f, 0x9f, 0x22,
-	0x70, 0x7d, 0x2e, 0x92, 0xcf, 0x58, 0x9c, 0x50, 0x04, 0xf2, 0xb5, 0x8d, 0x5d, 0x3b, 0xc8, 0x73,
-	0xaf, 0xfb, 0x97, 0xff, 0x6c, 0xcf, 0xf5, 0xac, 0xae, 0xf1, 0x75, 0x8a, 0xda, 0x31, 0x95, 0x7c,
-	0x29, 0x8b, 0x4a, 0x02, 0x36, 0x69, 0xf4, 0xe2, 0xc0, 0xc2, 0x60, 0xaa, 0x74, 0xba, 0xa9, 0x42,
-	0xa9, 0xe7, 0xd2, 0xb3, 0x46, 0x8b, 0xba, 0x1e, 0xd7, 0xd0, 0x58, 0x18, 0xa7, 0xae, 0xf8, 0x00,
-	0x1c, 0xe2, 0xe8, 0xff, 0xcc, 0x01, 0xea, 0xb7, 0x1d, 0x66, 0xf1, 0x0e, 0xb5, 0xad, 0x2b, 0x78,
-	0x35, 0x6e, 0xf1, 0x58, 0x2c, 0x63, 0x1f, 0xce, 0xf6, 0xd5, 0x68, 0x13, 0xc7, 0x8b, 0xd7, 0x0f,
-	0xcb, 0x6c, 0x11, 0x0b, 0x18, 0x5a, 0x83, 0xb9, 0x1e, 0xe7, 0xbc, 0x41, 0x9c, 0x16, 0xf5, 0x7c,
-	0xcf, 0xe3, 0x77, 0x34, 0x56, 0xff, 0x8c, 0xa4, 0x99, 0xbb, 0x92, 0x80, 0x83, 0x13, 0x29, 0xd1,
-	0x26, 0x94, 0xb6, 0x7d, 0x35, 0xc9, 0x30, 0xb6, 0x34, 0xd4, 0xcd, 0x88, 0x58, 0x10, 0xfc, 0xc5,
-	0x21, 0x5b, 0x74, 0x19, 0x0a, 0x6d, 0xda, 0xe9, 0xce, 0x8f, 0x70, 0xf6, 0x5f, 0xc8, 0xea, 0x0b,
-	0xf5, 0x31, 0x16, 0xf2, 0xd9, 0x2f, 0xcc, 0xf9, 0xe8, 0xdf, 0x04, 0xa1, 0x95, 0x2c, 0xea, 0x3d,
-	0x38, 0x91, 0x3c, 0x00, 0xa3, 0x3b, 0xd4, 0x09, 0xd4, 0xa9, 0x30, 0xbb, 0x2a, 0x96, 0xb1, 0x0f,
-	0xd7, 0x7f, 0xa1, 0xc1, 0x0c, 0xdf, 0xc1, 0x7a, 0x6f, 0xd3, 0x6d, 0x38, 0x86, 0xcd, 0x0a, 0x91,
-	0x5b, 0xbb, 0x9b, 0xb3, 0x30, 0xed, 0xd2, 0xee, 0x0e, 0x75, 0x96, 0x2d, 0xd3, 0xf5, 0x1c, 0x62,
-	0x98, 0x9e, 0xdc, 0xd6, 0xbc, 0xc4, 0x9e, 0x5e, 0x8f, 0xc1, 0x71, 0x1f, 0x85, 0xfe, 0xf3, 0x02,
-	0x8c, 0x9e, 0x77, 0xa8, 0xd1, 0x6a, 0x7b, 0xe8, 0x6b, 0x30, 0xd6, 0x95, 0xf5, 0x1a, 0xdf, 0x1f,
-	0xbb, 0x09, 0x51, 0x24, 0x57, 0xd4, 0x22, 0xb9, 0x62, 0x6f, 0xb7, 0xd8, 0x82, 0x5b, 0x61, 0xd8,
-	0x95, 0x9d, 0x13, 0x95, 0xe7, 0x36, 0x5f, 0xa1, 0x0d, 0x8f, 0xd5, 0x7a, 0x61, 0x9a, 0x0a, 0xd7,
-	0x70, 0xc0, 0x95, 0x99, 0x30, 0xe9, 0x18, 0xc4, 0x9d, 0x1f, 0x8d, 0x9a, 0x70, 0x8d, 0x2d, 0x62,
-	0x01, 0x63, 0xae, 0x75, 0x9d, 0x38, 0xb4, 0x6d, 0xf5, 0x5c, 0x3a, 0x3f, 0x16, 0x2d, 0x01, 0x9e,
-	0xf7, 0x01, 0x38, 0xc4, 0x41, 0xd7, 0x60, 0xb4, 0x61, 0x75, 0xbb, 0x86, 0xe7, 0x87, 0xf2, 0x6a,
-	0x3a, 0x03, 0xba, 0x60, 0x78, 0xcb, 0x9c, 0x2e, 0xbc, 0x07, 0xf1, 0xdf, 0xc5, 0x3e, 0x43, 0xb4,
-	0x1e, 0x04, 0xa5, 0x02, 0x67, 0xfd, 0x60, 0x3a, 0xd6, 0x3c, 0x56, 0x0c, 0x8a, 0x3f, 0x8c, 0x29,
-	0xf7, 0x56, 0x77, 0x7e, 0x24, 0x0b, 0x53, 0x6e, 0x50, 0x21, 0x53, 0xfe, 0xd7, 0xc5, 0x92, 0x15,
-	0x7a, 0x31, 0x48, 0xf4, 0x45, 0x7e, 0x77, 0xa7, 0xd2, 0x31, 0x95, 0x97, 0x2f, 0xab, 0x8c, 0xc9,
-	0x68, 0x75, 0xe0, 0xd7, 0x01, 0xfa, 0x1f, 0x35, 0x28, 0x4b, 0xcc, 0x55, 0xc3, 0xf5, 0xd0, 0x4b,
-	0x7d, 0xa6, 0x52, 0x49, 0x67, 0x2a, 0x8c, 0x9a, 0x1b, 0x4a, 0x50, 0x47, 0xf8, 0x2b, 0x8a, 0x99,
-	0x60, 0x18, 0x31, 0x3c, 0xda, 0xf5, 0xdb, 0x8e, 0x87, 0x33, 0x9d, 0x44, 0x09, 0xd8, 0x8c, 0x07,
-	0x16, 0xac, 0xf4, 0x8f, 0x0b, 0x30, 0x2d, 0x31, 0x32, 0x54, 0xce, 0x51, 0x63, 0x2c, 0x66, 0x33,
-	0xc6, 0xdc, 0xed, 0x33, 0xc6, 0xfc, 0xed, 0x30, 0xc6, 0xc2, 0xad, 0x33, 0xc6, 0xd7, 0x60, 0x7a,
-	0x87, 0x3a, 0xc6, 0x96, 0xd1, 0xe0, 0x2d, 0xd8, 0x8a, 0xb9, 0x65, 0xc9, 0xe0, 0xfe, 0x68, 0x3a,
-	0xf6, 0x57, 0x63, 0xd4, 0xf5, 0x39, 0x16, 0xd0, 0xe2, 0xab, 0xb8, 0x4f, 0x0a, 0x7a, 0x53, 0x83,
-	0x59, 0x75, 0xf1, 0xa2, 0xe1, 0x7a, 0x96, 0xb3, 0x3b, 0x3f, 0xca, 0x0f, 0x37, 0xac, 0xf4, 0x7b,
-	0xe4, 0x39, 0x67, 0xaf, 0xf6, 0xb3, 0xc6, 0x49, 0xf2, 0xf4, 0x7f, 0xe5, 0x61, 0x22, 0xe2, 0x5b,
-	0xe8, 0x3a, 0x80, 0x40, 0xa4, 0xcd, 0x15, 0x53, 0xd6, 0x38, 0xcb, 0x43, 0x38, 0xa9, 0xdc, 0x1d,
-	0xe3, 0x22, 0x5a, 0xe9, 0x20, 0xe6, 0x86, 0x00, 0xac, 0x88, 0x42, 0xaf, 0x43, 0x99, 0xc8, 0xee,
-	0xef, 0xbc, 0xe5, 0x48, 0xb3, 0x3c, 0x3b, 0x8c, 0xe4, 0x5a, 0xc8, 0x26, 0xde, 0xc5, 0x87, 0x10,
-	0xac, 0x4a, 0x5b, 0x70, 0x60, 0x2a, 0xb6, 0xdf, 0x84, 0x4e, 0x7c, 0x45, 0xed, 0xc4, 0x53, 0x87,
-	0x2e, 0x9f, 0x2f, 0x6f, 0x69, 0xd5, 0xf6, 0xdf, 0x85, 0xe9, 0xf8, 0x4e, 0x6f, 0x99, 0xd0, 0x48,
-	0x1f, 0xad, 0xce, 0x0c, 0x7e, 0x9b, 0x83, 0x52, 0xe0, 0xc4, 0x59, 0x52, 0xfd, 0x02, 0xe4, 0x8c,
-	0xa6, 0x4c, 0xf4, 0x20, 0xb1, 0x72, 0x2b, 0x67, 0x71, 0xce, 0x68, 0xa2, 0xfb, 0xa0, 0xb8, 0xe9,
-	0x10, 0xb3, 0xd1, 0x96, 0xa9, 0x3d, 0xf0, 0xb7, 0x3a, 0x5f, 0xc5, 0x12, 0xca, 0x4a, 0x75, 0x8f,
-	0xb4, 0x78, 0x79, 0xa6, 0x94, 0xea, 0x1b, 0xa4, 0x85, 0xd9, 0x3a, 0xba, 0x00, 0x33, 0xa2, 0x37,
-	0x5d, 0x6e, 0xd3, 0xc6, 0xb6, 0xd8, 0x22, 0xf7, 0xc7, 0x52, 0xfd, 0x6e, 0x89, 0x3c, 0x73, 0x31,
-	0x8e, 0x80, 0xfb, 0x69, 0xd4, 0xee, 0xbe, 0xb8, 0x7f, 0x77, 0xcf, 0xb6, 0x4e, 0x7a, 0x5e, 0xdb,
-	0x72, 0x64, 0xb2, 0x0f, 0xb6, 0x5e, 0xe3, 0xab, 0x58, 0x42, 0xf5, 0x59, 0x98, 0xb9, 0x60, 0x78,
-	0x17, 0x7b, 0x9b, 0x6b, 0xbd, 0x4e, 0x07, 0xd3, 0x57, 0x7b, 0xac, 0x5a, 0x16, 0x8b, 0xab, 0x24,
-	0xb2, 0xf8, 0xcb, 0x11, 0x98, 0xb8, 0x60, 0x78, 0x5c, 0x81, 0x99, 0xab, 0xe7, 0x75, 0x38, 0x6c,
-	0x98, 0x2e, 0x6d, 0xf4, 0x1c, 0xba, 0xbe, 0x6d, 0xd8, 0x1b, 0xab, 0xeb, 0xdc, 0x7c, 0x76, 0x65,
-	0xf1, 0x7e, 0x54, 0x12, 0x1e, 0x5e, 0x49, 0x42, 0xc2, 0xc9, 0xb4, 0xe8, 0x24, 0x80, 0x43, 0x49,
-	0xb3, 0xae, 0x5e, 0x51, 0xe0, 0x8d, 0x38, 0x80, 0x60, 0x05, 0x0b, 0x2d, 0x41, 0xf9, 0xba, 0x63,
-	0x78, 0x54, 0x12, 0x89, 0x2b, 0x0b, 0xfc, 0xe8, 0xf9, 0x10, 0x84, 0x55, 0x3c, 0xb4, 0x03, 0x65,
-	0x3b, 0xd4, 0x85, 0x0c, 0xa6, 0x29, 0xc3, 0x87, 0xa2, 0xc4, 0x35, 0xc7, 0xea, 0x5a, 0x2c, 0x4e,
-	0x5d, 0xa2, 0x8d, 0x36, 0x31, 0x0d, 0xb7, 0x5b, 0x9f, 0x62, 0x72, 0x15, 0x14, 0xac, 0x0a, 0x42,
-	0x2d, 0x28, 0x3a, 0xd4, 0x6c, 0x52, 0x47, 0x96, 0x15, 0x29, 0x45, 0x3e, 0xcb, 0x96, 0x30, 0x27,
-	0x4c, 0x10, 0x09, 0xcc, 0x0e, 0x04, 0x14, 0x4b, 0xf6, 0xc8, 0x54, 0xfb, 0x8c, 0x51, 0x2e, 0xab,
-	0x96, 0x52, 0x96, 0x4f, 0x96, 0x20, 0x69, 0x70, 0xcf, 0x71, 0x4d, 0xf6, 0x1c, 0x63, 0x5c, 0xd4,
-	0x93, 0xe9, 0x44, 0xb1, 0x1e, 0x23, 0x41, 0x4a, 0xbc, 0xff, 0xf8, 0x53, 0x01, 0xa6, 0x2e, 0x18,
-	0x43, 0x17, 0xff, 0x1e, 0xdc, 0x25, 0x52, 0xfe, 0x3a, 0xed, 0xd0, 0x06, 0xa3, 0x5e, 0xf7, 0x1c,
-	0xe2, 0xd1, 0x96, 0xdf, 0x8c, 0x9f, 0x91, 0xa4, 0x77, 0x2d, 0x27, 0xa3, 0xdd, 0x1c, 0x0c, 0xc2,
-	0x83, 0x58, 0xa7, 0x8e, 0x35, 0x49, 0x8d, 0x47, 0x21, 0x6b, 0xe3, 0xc1, 0x0a, 0x2b, 0xd2, 0xe9,
-	0x58, 0xd7, 0x37, 0x48, 0xcb, 0x95, 0xa1, 0x28, 0x28, 0xac, 0x6a, 0x3e, 0x00, 0x87, 0x38, 0xa8,
-	0x02, 0x60, 0xb4, 0x4c, 0xcb, 0xa1, 0x9c, 0xa2, 0xc8, 0xa7, 0x4b, 0x93, 0xcc, 0xcf, 0x56, 0x82,
-	0x55, 0xac, 0x60, 0x0c, 0x76, 0xf8, 0xd1, 0x4f, 0xe0, 0xf0, 0x8f, 0xc0, 0xb8, 0x61, 0x36, 0x3a,
-	0xbd, 0x26, 0x5d, 0x23, 0x5e, 0xdb, 0x9d, 0x1f, 0xe3, 0xdb, 0x98, 0xde, 0xbb, 0x71, 0x6c, 0x7c,
-	0x45, 0x59, 0xc7, 0x11, 0x2c, 0x46, 0x45, 0x5f, 0x53, 0xa8, 0x4a, 0x21, 0xd5, 0xb9, 0xd7, 0x54,
-	0x2a, 0x15, 0x4b, 0xff, 0xab, 0x06, 0x45, 0x11, 0x94, 0xd1, 0x52, 0x6c, 0x88, 0x77, 0xb4, 0x6f,
-	0x88, 0x57, 0x4e, 0x9a, 0xc5, 0xea, 0x50, 0x34, 0x5c, 0xb7, 0x47, 0x45, 0x29, 0x5a, 0x12, 0x6e,
-	0xb7, 0xc2, 0x57, 0xb0, 0x84, 0x20, 0x03, 0x80, 0xf8, 0x53, 0x38, 0xbf, 0xae, 0x5c, 0xca, 0x3a,
-	0xa6, 0x8c, 0x8d, 0x28, 0x03, 0x80, 0x8b, 0x15, 0xe6, 0xfa, 0x4f, 0x35, 0xb8, 0x9b, 0x39, 0x09,
-	0xaf, 0x15, 0xcf, 0x52, 0x9b, 0xf9, 0xbd, 0xd9, 0xd8, 0x95, 0xb1, 0x9c, 0xc7, 0x52, 0xdb, 0x72,
-	0x0d, 0x5e, 0xae, 0x69, 0xf1, 0x58, 0xea, 0x43, 0xb0, 0x82, 0x95, 0xa2, 0x4b, 0xae, 0x42, 0x89,
-	0x97, 0xa4, 0x4c, 0xa5, 0xd2, 0xae, 0x03, 0x33, 0x5b, 0xf6, 0x01, 0x38, 0xc4, 0xd1, 0xff, 0xa6,
-	0xc1, 0xd4, 0x50, 0xd3, 0xb2, 0xa7, 0x60, 0x92, 0x17, 0x03, 0xee, 0x79, 0xa3, 0xc3, 0x6f, 0x50,
-	0xee, 0xea, 0x88, 0xc4, 0x9e, 0xbc, 0x1a, 0x81, 0xe2, 0x18, 0xb6, 0x3f, 0x6d, 0xcb, 0x1f, 0x34,
-	0x6d, 0x2b, 0x0c, 0x31, 0x6d, 0xfb, 0x50, 0x83, 0x23, 0xc9, 0xa1, 0x0b, 0xbd, 0x1c, 0x9b, 0xba,
-	0x2d, 0xa5, 0x0f, 0x84, 0x29, 0x46, 0x6d, 0x2c, 0x7d, 0xc8, 0xee, 0x42, 0x94, 0x9d, 0x4f, 0xa7,
-	0x67, 0x9f, 0x68, 0x26, 0x83, 0x3a, 0x0e, 0xfd, 0xd7, 0x1a, 0x88, 0xfb, 0xc8, 0x12, 0x68, 0x4f,
-	0x02, 0xb4, 0x64, 0x41, 0x81, 0x57, 0xe5, 0x7d, 0x05, 0x36, 0x77, 0x21, 0x80, 0x60, 0x05, 0xcb,
-	0x2f, 0xb5, 0xf2, 0x03, 0x4a, 0xad, 0xfb, 0xa0, 0xd8, 0x14, 0x53, 0xc1, 0x42, 0x34, 0x8a, 0xca,
-	0x91, 0xa0, 0x84, 0xea, 0x6f, 0x17, 0x60, 0x86, 0xef, 0x77, 0xd8, 0x24, 0x31, 0xcc, 0xde, 0x6d,
-	0x38, 0xc2, 0xef, 0xa5, 0x3f, 0xaf, 0x88, 0xe3, 0x9c, 0x96, 0xf4, 0x47, 0x56, 0x12, 0xb1, 0x6e,
-	0x0e, 0x84, 0xe0, 0x01, 0x7c, 0xff, 0x5f, 0x92, 0xc5, 0x43, 0x30, 0x66, 0x77, 0x88, 0xb7, 0x65,
-	0x39, 0x5d, 0x59, 0xae, 0x06, 0xf3, 0x89, 0x35, 0xb9, 0x8e, 0x03, 0x8c, 0xc1, 0xa9, 0x65, 0x6c,
-	0xf8, 0xd4, 0xa2, 0x9b, 0x70, 0x44, 0x29, 0x9a, 0x6e, 0xff, 0x40, 0xfa, 0x4d, 0x0d, 0x8e, 0xee,
-	0x5b, 0xa5, 0xa1, 0x66, 0x2c, 0x34, 0x3c, 0x99, 0xb9, 0xf4, 0x4b, 0x33, 0x8c, 0x7f, 0x5b, 0x83,
-	0xb9, 0xe1, 0xe7, 0xf0, 0xc7, 0xa1, 0x60, 0x87, 0xb1, 0x36, 0xc8, 0x00, 0x3c, 0xc2, 0x72, 0x48,
-	0x54, 0x31, 0xf9, 0x14, 0x8a, 0x79, 0x43, 0x83, 0x7b, 0xf6, 0x29, 0x29, 0xd1, 0x66, 0x4c, 0x2d,
-	0x67, 0x32, 0x56, 0xa9, 0x69, 0x94, 0xf2, 0xe3, 0x1c, 0x8c, 0xae, 0x39, 0xd6, 0x2b, 0xb4, 0x71,
-	0x27, 0xc6, 0xb2, 0xcf, 0x41, 0xc1, 0xb5, 0x69, 0x43, 0x36, 0xc2, 0x27, 0x52, 0x36, 0x15, 0x62,
-	0x7b, 0xeb, 0x36, 0x6d, 0x88, 0xfa, 0x97, 0xfd, 0xc2, 0x9c, 0x91, 0x32, 0x8b, 0xcc, 0x67, 0xe9,
-	0xad, 0x7d, 0x96, 0x07, 0xcf, 0x22, 0x25, 0xe6, 0xa7, 0x76, 0x16, 0x29, 0xf7, 0x37, 0x60, 0x16,
-	0xf9, 0xfd, 0xf0, 0x04, 0x4c, 0x69, 0xe8, 0x1b, 0x30, 0x63, 0xfb, 0x76, 0xb6, 0x66, 0x75, 0x8c,
-	0x86, 0x91, 0x35, 0x1d, 0xaf, 0x45, 0xc8, 0x77, 0xc3, 0xae, 0x7e, 0x2d, 0xce, 0x17, 0xf7, 0x8b,
-	0xd2, 0x2d, 0x98, 0x88, 0xa8, 0x1e, 0x9d, 0xf2, 0xdf, 0x49, 0x88, 0x96, 0x9b, 0xe2, 0x9d, 0x84,
-	0x9b, 0x37, 0x8e, 0x8d, 0x4b, 0x74, 0xf5, 0x1d, 0x85, 0x2c, 0x4f, 0xfe, 0x7f, 0x92, 0x83, 0x52,
-	0xb0, 0xb3, 0x3b, 0x60, 0xe0, 0x5f, 0x8e, 0x18, 0xf8, 0xa9, 0x8c, 0x3a, 0x4d, 0x34, 0xf1, 0x97,
-	0x63, 0x26, 0x9e, 0xf5, 0xa2, 0x0e, 0x30, 0xf2, 0x7f, 0x6b, 0xfc, 0x4e, 0x04, 0x2e, 0x1f, 0x6c,
-	0x1e, 0x3c, 0xab, 0x26, 0x30, 0xba, 0x25, 0xc6, 0x75, 0xf2, 0xa0, 0x8f, 0x66, 0x9a, 0xf1, 0x05,
-	0x63, 0xf1, 0xf0, 0xe2, 0x7c, 0x88, 0xcf, 0x17, 0x7d, 0xe5, 0xd6, 0x9c, 0x1a, 0x12, 0x4e, 0xfc,
-	0xae, 0x7a, 0xe2, 0x3b, 0xe0, 0xd8, 0x1b, 0x51, 0xc7, 0xae, 0x66, 0x3c, 0xc9, 0x00, 0xd7, 0xfe,
-	0x5e, 0x0e, 0x66, 0xfb, 0x73, 0x86, 0x8b, 0x5c, 0x98, 0x6c, 0xa9, 0xa3, 0x2b, 0xdf, 0xbf, 0x4f,
-	0xa5, 0x7e, 0x3a, 0x10, 0xd2, 0x86, 0x2d, 0x45, 0x64, 0xd9, 0xc5, 0x31, 0x11, 0xe8, 0x75, 0x98,
-	0x26, 0xd1, 0x37, 0x2c, 0xfc, 0xd3, 0x66, 0xed, 0xf0, 0xa4, 0xe0, 0xa0, 0x66, 0x8b, 0x01, 0x5c,
-	0xdc, 0x27, 0x48, 0x7f, 0x4b, 0x83, 0xa9, 0x58, 0x58, 0x62, 0x29, 0xdd, 0xf5, 0x12, 0x52, 0xba,
-	0x1c, 0xa6, 0x72, 0x18, 0x5a, 0x83, 0x39, 0xd2, 0xf3, 0xac, 0x80, 0xf6, 0x9c, 0x49, 0x36, 0x3b,
-	0xb4, 0x29, 0x8b, 0x9a, 0xe0, 0x11, 0x76, 0x2d, 0x01, 0x07, 0x27, 0x52, 0xea, 0x5f, 0x55, 0x2c,
-	0x8b, 0x07, 0xdc, 0x54, 0xfb, 0x78, 0x20, 0xea, 0x4e, 0xa5, 0xc1, 0x6e, 0xa1, 0xff, 0x2c, 0xa7,
-	0x9c, 0x55, 0xc6, 0xd0, 0xa5, 0x68, 0x0c, 0x3d, 0x16, 0x8f, 0xa1, 0x93, 0xa1, 0x72, 0x86, 0x8b,
-	0xa2, 0xe8, 0x55, 0xc5, 0x3f, 0xf2, 0x59, 0x1e, 0x27, 0xc4, 0xb6, 0x5a, 0xf1, 0xdf, 0xd2, 0x13,
-	0x33, 0xfd, 0xc0, 0x69, 0xfc, 0xe5, 0xd0, 0x69, 0x16, 0x9e, 0x80, 0x89, 0x08, 0x72, 0xa6, 0xb7,
-	0xea, 0xfe, 0xae, 0xc1, 0xd1, 0x7d, 0x47, 0x91, 0xac, 0x6e, 0x10, 0x07, 0x90, 0xfe, 0xfe, 0x58,
-	0x6a, 0xef, 0x88, 0xce, 0x8f, 0x45, 0x80, 0x11, 0xcb, 0x58, 0xb2, 0x94, 0xcc, 0x3b, 0x64, 0x53,
-	0x46, 0xc7, 0xf4, 0xcc, 0xa3, 0x73, 0xe8, 0x80, 0xf9, 0x2a, 0x11, 0xcc, 0x3b, 0x64, 0x53, 0x7f,
-	0x27, 0x07, 0xd3, 0xcc, 0xf5, 0x22, 0xdd, 0xdc, 0x1a, 0xe4, 0x5b, 0x86, 0x27, 0xcf, 0xb2, 0x94,
-	0x5a, 0x9c, 0xca, 0xa3, 0x3e, 0xca, 0xba, 0x4b, 0xe6, 0xe7, 0x8c, 0x15, 0x7a, 0xc1, 0xaf, 0x89,
-	0x33, 0x1d, 0xa1, 0xaf, 0xcf, 0xac, 0x97, 0xfa, 0x0a, 0xe9, 0x17, 0xfc, 0xb7, 0x4b, 0xf2, 0x59,
-	0x38, 0xf7, 0xbd, 0xe3, 0x20, 0x38, 0xab, 0xaf, 0xa4, 0xe8, 0x3f, 0xca, 0x81, 0x70, 0xac, 0x3b,
-	0x90, 0xe8, 0x2f, 0x45, 0x12, 0x7d, 0xca, 0x98, 0xce, 0x37, 0x97, 0x98, 0xe4, 0xe3, 0xe9, 0xee,
-	0x44, 0x16, 0x86, 0xfb, 0x27, 0xf8, 0x3f, 0x68, 0x50, 0xe2, 0x78, 0x77, 0x20, 0xd5, 0xad, 0x45,
-	0x53, 0xdd, 0x83, 0x19, 0x4e, 0x31, 0x20, 0xcd, 0xfd, 0x20, 0x2f, 0x77, 0x1f, 0x84, 0xd3, 0x36,
-	0x71, 0x9a, 0xb2, 0xb3, 0x0f, 0xc3, 0x29, 0x5b, 0xc4, 0x02, 0x86, 0x3a, 0x30, 0xe1, 0x2a, 0x86,
-	0xe2, 0xca, 0x73, 0xa6, 0x4c, 0x80, 0xaa, 0x8d, 0xb9, 0xf5, 0x19, 0xfe, 0xb6, 0x9d, 0xba, 0x84,
-	0xa3, 0xcc, 0xd1, 0x77, 0x35, 0x98, 0xb5, 0xfb, 0xf3, 0xb0, 0x34, 0x8c, 0xc7, 0x33, 0xc6, 0xc9,
-	0x90, 0x41, 0xfd, 0xae, 0xbd, 0x1b, 0xc7, 0x92, 0x32, 0x3c, 0x4e, 0x12, 0x87, 0xda, 0x30, 0xae,
-	0x3e, 0x1c, 0x96, 0x66, 0x74, 0x32, 0xfb, 0x53, 0x68, 0x31, 0x2d, 0x56, 0x57, 0x70, 0x84, 0xb3,
-	0xfe, 0xe7, 0x11, 0x28, 0x2b, 0x76, 0x87, 0x4e, 0x44, 0xd3, 0xcf, 0x3d, 0xf1, 0xf4, 0x03, 0x1c,
-	0x39, 0x92, 0x7a, 0x1c, 0x98, 0x6c, 0xf4, 0x1c, 0x87, 0x9a, 0xde, 0xf9, 0x5b, 0x52, 0x46, 0x22,
-	0x56, 0xa2, 0x2c, 0x47, 0x38, 0xe2, 0x98, 0x04, 0x56, 0xb3, 0xb6, 0xe5, 0x13, 0xfa, 0x7c, 0x96,
-	0x27, 0xf4, 0x83, 0x6b, 0x56, 0xff, 0xa9, 0xbc, 0xcf, 0x17, 0xad, 0x41, 0x51, 0x3c, 0xc8, 0x94,
-	0x8f, 0x7a, 0x1e, 0x4a, 0x3b, 0x82, 0x64, 0x34, 0x22, 0xd8, 0x8b, 0xdf, 0x58, 0xf2, 0x51, 0x73,
-	0x74, 0xe9, 0x80, 0x1c, 0xfd, 0x0c, 0x20, 0x6b, 0xd3, 0xa5, 0xce, 0x0e, 0x6d, 0x5e, 0x10, 0x1f,
-	0x12, 0x30, 0x33, 0x28, 0x1e, 0xd7, 0xee, 0xcf, 0xd7, 0x17, 0x24, 0x15, 0x7a, 0xae, 0x0f, 0x03,
-	0x27, 0x50, 0xa1, 0x1e, 0x4c, 0x4b, 0xed, 0x05, 0xf6, 0x27, 0x1f, 0x94, 0x65, 0xed, 0x68, 0xc2,
-	0x37, 0x2a, 0x96, 0x63, 0x0c, 0x71, 0x9f, 0x08, 0xe6, 0xb8, 0x1d, 0xa2, 0xa4, 0xeb, 0x79, 0x18,
-	0x5e, 0x26, 0x77, 0xdc, 0x55, 0x95, 0x1b, 0x8e, 0x32, 0xd7, 0x97, 0x60, 0x46, 0x98, 0xb1, 0x9a,
-	0x48, 0x0f, 0x7e, 0xc3, 0xfd, 0xf7, 0x1a, 0x44, 0x03, 0x42, 0xf4, 0xcd, 0x1d, 0x2d, 0xc5, 0x9b,
-	0x3b, 0xd7, 0x61, 0xb2, 0x67, 0xbb, 0x9e, 0x43, 0x49, 0x97, 0xef, 0xc0, 0x0f, 0x97, 0x8f, 0x65,
-	0x09, 0xfa, 0x6a, 0x2a, 0x0c, 0xca, 0xf4, 0x2b, 0x11, 0xb6, 0x38, 0x26, 0x46, 0xff, 0x6f, 0x0e,
-	0x22, 0x9e, 0x8d, 0xde, 0xd2, 0x60, 0x86, 0xc4, 0x5e, 0xf7, 0xf7, 0x1b, 0x86, 0xa7, 0xb3, 0x7d,
-	0x83, 0xd1, 0xf7, 0xb5, 0x40, 0x38, 0x1a, 0x88, 0xa3, 0xb8, 0xb8, 0x5f, 0x28, 0x8f, 0xa3, 0xa4,
-	0xff, 0x7b, 0x8e, 0x6c, 0x71, 0x34, 0xe1, 0x83, 0x10, 0x11, 0x47, 0x13, 0x00, 0x38, 0x49, 0x1c,
-	0x7a, 0x11, 0x0a, 0xc4, 0x69, 0xf9, 0xcf, 0xa7, 0xb2, 0x8b, 0xf5, 0x3f, 0xd3, 0x09, 0x6d, 0xa7,
-	0xe6, 0xb4, 0x5c, 0xcc, 0x99, 0xea, 0xbf, 0xcb, 0x43, 0xdf, 0x9b, 0x45, 0xf2, 0xad, 0x8c, 0x42,
-	0xe2, 0x5b, 0x19, 0xcf, 0x43, 0xc9, 0xf5, 0x88, 0xe3, 0x6d, 0x18, 0x5d, 0x2a, 0x9f, 0xc4, 0x7f,
-	0x3e, 0x5d, 0xba, 0x66, 0x14, 0xe2, 0x99, 0xf4, 0xba, 0xcf, 0x00, 0x87, 0xbc, 0xd0, 0xe9, 0x68,
-	0xd0, 0xd6, 0xe3, 0x41, 0x7b, 0x46, 0xdd, 0xe6, 0xb0, 0x6d, 0x43, 0x17, 0xca, 0x8a, 0x8a, 0x65,
-	0x4a, 0x3a, 0x93, 0x59, 0xa5, 0x4a, 0xe8, 0x15, 0x9f, 0xf1, 0x84, 0x10, 0x95, 0x3f, 0xba, 0x06,
-	0xb0, 0x65, 0x98, 0x86, 0xdb, 0xe6, 0xda, 0x2a, 0x66, 0xd6, 0x16, 0x1f, 0xdb, 0x9f, 0x0f, 0x38,
-	0x60, 0x85, 0x9b, 0x3e, 0x05, 0x13, 0x91, 0x97, 0x80, 0xf8, 0x60, 0x29, 0x70, 0xee, 0x4f, 0xeb,
-	0x60, 0x29, 0xd8, 0xe0, 0xad, 0x1c, 0x2c, 0x85, 0x4c, 0xf7, 0xaf, 0x3b, 0xdf, 0xd5, 0x60, 0x22,
-	0xc0, 0xfd, 0xd4, 0x8e, 0x59, 0x82, 0x1d, 0x0e, 0xa8, 0x3f, 0x7f, 0xa5, 0x9e, 0x22, 0x5a, 0x83,
-	0xe6, 0xf6, 0xa9, 0x41, 0xdd, 0xfe, 0x1a, 0x34, 0x43, 0xcd, 0x11, 0xef, 0xef, 0x94, 0x0f, 0x3f,
-	0xf6, 0x29, 0x45, 0xf5, 0xff, 0x68, 0x30, 0x15, 0xbb, 0x1d, 0xd5, 0x5d, 0xf3, 0x43, 0x55, 0x10,
-	0x85, 0xa1, 0x2a, 0x08, 0x03, 0xca, 0x2c, 0xdb, 0xfa, 0xe5, 0xdd, 0xc8, 0x27, 0x9b, 0x12, 0x32,
-	0xb7, 0x5f, 0x0d, 0xd9, 0x61, 0x95, 0x77, 0xfd, 0x99, 0xf7, 0x3e, 0x5a, 0x3c, 0xf4, 0xfe, 0x47,
-	0x8b, 0x87, 0x3e, 0xf8, 0x68, 0xf1, 0xd0, 0xb7, 0xf6, 0x16, 0xb5, 0xf7, 0xf6, 0x16, 0xb5, 0xf7,
-	0xf7, 0x16, 0xb5, 0x0f, 0xf6, 0x16, 0xb5, 0x0f, 0xf7, 0x16, 0xb5, 0x1f, 0xfe, 0x63, 0xf1, 0xd0,
-	0xb5, 0x7b, 0xd3, 0x7c, 0x76, 0xfa, 0xbf, 0x00, 0x00, 0x00, 0xff, 0xff, 0x33, 0x3b, 0x04, 0xe2,
-	0x9d, 0x3a, 0x00, 0x00,
+	// 3439 bytes of a gzipped FileDescriptorProto
+	0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xd4, 0x5b, 0x4d, 0x6c, 0x24, 0xd5,
+	0xb5, 0x9e, 0xea, 0x6e, 0xb7, 0xdd, 0xa7, 0xfd, 0x7b, 0xed, 0x19, 0x8c, 0x79, 0xe3, 0x19, 0x15,
+	0x3c, 0x04, 0x0f, 0x68, 0xbf, 0x99, 0xc1, 0x30, 0x0c, 0x3c, 0x78, 0x6e, 0xcf, 0x9f, 0x07, 0xcf,
+	0xe0, 0x5c, 0x7b, 0x06, 0x32, 0x80, 0x92, 0xeb, 0xee, 0xeb, 0xee, 0xc2, 0xdd, 0x5d, 0x45, 0xdd,
+	0x6a, 0x0f, 0x0e, 0x52, 0x12, 0x92, 0xa0, 0xb0, 0x09, 0x49, 0x94, 0x0d, 0x59, 0x45, 0x8a, 0xa2,
+	0x44, 0x59, 0x24, 0xbb, 0x2c, 0xa2, 0x2c, 0x90, 0xc2, 0x06, 0x65, 0x11, 0xa1, 0xac, 0x88, 0x14,
+	0x8d, 0xc0, 0xd9, 0x45, 0x22, 0xec, 0x47, 0x8a, 0x14, 0xdd, 0x9f, 0xaa, 0xba, 0x55, 0x5d, 0x6d,
+	0x57, 0x35, 0x33, 0x23, 0xb2, 0xb3, 0xef, 0x39, 0xe7, 0x3b, 0xf7, 0xe7, 0xdc, 0xf3, 0x57, 0xb7,
+	0xe1, 0xf1, 0x86, 0xe5, 0x35, 0xbb, 0x9b, 0x95, 0x9a, 0xdd, 0x5e, 0x20, 0xdb, 0x5d, 0xcb, 0xdb,
+	0x5d, 0xd8, 0x26, 0x6e, 0xc3, 0x5e, 0x20, 0x8e, 0xb5, 0xb0, 0x73, 0x82, 0xb4, 0x9c, 0x26, 0x39,
+	0xb1, 0xd0, 0xa0, 0x1d, 0xea, 0x12, 0x8f, 0xd6, 0x2b, 0x8e, 0x6b, 0x7b, 0x36, 0x7a, 0x20, 0x94,
+	0xaa, 0x48, 0xa9, 0x8a, 0x90, 0xaa, 0x10, 0xc7, 0xaa, 0xf8, 0x52, 0x73, 0x8f, 0x69, 0xd8, 0x0d,
+	0xbb, 0x61, 0x2f, 0x08, 0xe1, 0xcd, 0xee, 0x96, 0xf8, 0x4f, 0xfc, 0x23, 0xfe, 0x92, 0xa0, 0x73,
+	0x8f, 0x6f, 0x9f, 0x66, 0x15, 0x4b, 0x68, 0x6e, 0x93, 0x5a, 0xd3, 0xea, 0x50, 0x77, 0x77, 0xc1,
+	0xd9, 0x6e, 0xf0, 0x01, 0xb6, 0xd0, 0xa6, 0x1e, 0x59, 0xd8, 0xe9, 0x99, 0xca, 0xdc, 0x42, 0x3f,
+	0x29, 0xb7, 0xdb, 0xf1, 0xac, 0x36, 0xed, 0x11, 0x78, 0xe2, 0x20, 0x01, 0x56, 0x6b, 0xd2, 0x36,
+	0x89, 0xcb, 0x99, 0xaf, 0xc0, 0xf4, 0x52, 0x87, 0xb4, 0x76, 0x99, 0xc5, 0x70, 0xb7, 0xb3, 0xe4,
+	0x36, 0xba, 0x6d, 0xda, 0xf1, 0xd0, 0x71, 0x28, 0x74, 0x48, 0x9b, 0xce, 0x1a, 0xc7, 0x8d, 0x87,
+	0x4a, 0xd5, 0xd1, 0x0f, 0x6f, 0x1e, 0x3b, 0xb4, 0x77, 0xf3, 0x58, 0xe1, 0x0a, 0x69, 0x53, 0x2c,
+	0x28, 0xe8, 0x7e, 0x18, 0xda, 0x21, 0xad, 0x2e, 0x9d, 0xcd, 0x09, 0x96, 0x31, 0xc5, 0x32, 0x74,
+	0x8d, 0x0f, 0x62, 0x49, 0x33, 0xbf, 0x9b, 0x8f, 0xc0, 0x5f, 0xa6, 0x1e, 0xa9, 0x13, 0x8f, 0xa0,
+	0x36, 0x14, 0x5b, 0x64, 0x93, 0xb6, 0xd8, 0xac, 0x71, 0x3c, 0xff, 0x50, 0xf9, 0xe4, 0xb9, 0x4a,
+	0x9a, 0xad, 0xaf, 0x24, 0x40, 0x55, 0x56, 0x05, 0xce, 0xb9, 0x8e, 0xe7, 0xee, 0x56, 0xc7, 0xd5,
+	0x24, 0x8a, 0x72, 0x10, 0x2b, 0x25, 0xe8, 0x2d, 0x03, 0xca, 0xa4, 0xd3, 0xb1, 0x3d, 0xe2, 0x59,
+	0x76, 0x87, 0xcd, 0xe6, 0x84, 0xd2, 0x4b, 0x83, 0x2b, 0x5d, 0x0a, 0xc1, 0xa4, 0xe6, 0x69, 0xa5,
+	0xb9, 0xac, 0x51, 0xb0, 0xae, 0x73, 0xee, 0x29, 0x28, 0x6b, 0x53, 0x45, 0x93, 0x90, 0xdf, 0xa6,
+	0xbb, 0x72, 0x7f, 0x31, 0xff, 0x13, 0xcd, 0x44, 0x36, 0x54, 0xed, 0xe0, 0x99, 0xdc, 0x69, 0x63,
+	0xee, 0x59, 0x98, 0x8c, 0x2b, 0xcc, 0x22, 0x6f, 0xbe, 0x6b, 0xc0, 0x8c, 0xb6, 0x0a, 0x4c, 0xb7,
+	0xa8, 0x4b, 0x3b, 0x35, 0x8a, 0x16, 0xa0, 0xc4, 0xcf, 0x92, 0x39, 0xa4, 0xe6, 0x1f, 0xf5, 0x94,
+	0x5a, 0x48, 0xe9, 0x8a, 0x4f, 0xc0, 0x21, 0x4f, 0x60, 0x16, 0xb9, 0xfd, 0xcc, 0xc2, 0x69, 0x12,
+	0x46, 0x67, 0xf3, 0x51, 0xb3, 0x58, 0xe3, 0x83, 0x58, 0xd2, 0xcc, 0xff, 0x83, 0x7b, 0xfd, 0xf9,
+	0x6c, 0xd0, 0xb6, 0xd3, 0x22, 0x1e, 0x0d, 0x27, 0x75, 0xa0, 0xe9, 0x99, 0x13, 0x30, 0xb6, 0xe4,
+	0x38, 0xae, 0xbd, 0x43, 0xeb, 0xeb, 0x1e, 0x69, 0x50, 0xf3, 0x3b, 0x06, 0x1c, 0x5e, 0x72, 0x1b,
+	0xf6, 0xf2, 0xd9, 0x25, 0xc7, 0xb9, 0x48, 0x49, 0xcb, 0x6b, 0xae, 0x7b, 0xc4, 0xeb, 0x32, 0xf4,
+	0x2c, 0x14, 0x99, 0xf8, 0x4b, 0xc1, 0x3d, 0xe8, 0x5b, 0x88, 0xa4, 0xdf, 0xba, 0x79, 0x6c, 0x26,
+	0x41, 0x90, 0x62, 0x25, 0x85, 0x1e, 0x86, 0xe1, 0x36, 0x65, 0x8c, 0x34, 0xfc, 0x35, 0x4f, 0x28,
+	0x80, 0xe1, 0xcb, 0x72, 0x18, 0xfb, 0x74, 0xf3, 0x4f, 0x39, 0x98, 0x08, 0xb0, 0x94, 0xfa, 0x3b,
+	0xb0, 0xc1, 0x5d, 0x18, 0x6d, 0x6a, 0x2b, 0x14, 0xfb, 0x5c, 0x3e, 0xf9, 0x74, 0x4a, 0x5b, 0x4e,
+	0xda, 0xa4, 0xea, 0x8c, 0x52, 0x33, 0xaa, 0x8f, 0xe2, 0x88, 0x1a, 0xd4, 0x06, 0x60, 0xbb, 0x9d,
+	0x9a, 0x52, 0x5a, 0x10, 0x4a, 0x9f, 0xca, 0xa8, 0x74, 0x3d, 0x00, 0xa8, 0x22, 0xa5, 0x12, 0xc2,
+	0x31, 0xac, 0x29, 0x30, 0x7f, 0x6b, 0xc0, 0x74, 0x82, 0x1c, 0x7a, 0x26, 0x76, 0x9e, 0x0f, 0xf4,
+	0x9c, 0x27, 0xea, 0x11, 0x0b, 0x4f, 0xf3, 0x51, 0x18, 0x71, 0xe9, 0x8e, 0xc5, 0x2c, 0xbb, 0xa3,
+	0x76, 0x78, 0x52, 0xc9, 0x8f, 0x60, 0x35, 0x8e, 0x03, 0x0e, 0xf4, 0x08, 0x94, 0xfc, 0xbf, 0xf9,
+	0x36, 0xe7, 0xb9, 0x39, 0xf3, 0x83, 0xf3, 0x59, 0x19, 0x0e, 0xe9, 0xe6, 0x67, 0x86, 0x76, 0xfa,
+	0x57, 0x9d, 0x3a, 0xf1, 0x28, 0x37, 0x1e, 0xe2, 0x38, 0x57, 0x42, 0x63, 0x0e, 0x8c, 0x67, 0x49,
+	0x0e, 0x63, 0x9f, 0x8e, 0x4e, 0xc3, 0xa8, 0xfa, 0x53, 0xda, 0x8a, 0x9c, 0x5d, 0x70, 0x30, 0x4b,
+	0x1a, 0x0d, 0x47, 0x38, 0x51, 0x17, 0xc6, 0x98, 0xdd, 0x75, 0x6b, 0x54, 0x2a, 0x95, 0x33, 0x2d,
+	0x9f, 0x3c, 0x9d, 0xe5, 0x6c, 0xd6, 0x35, 0x80, 0xea, 0x61, 0xa5, 0x74, 0x4c, 0x1f, 0x65, 0x38,
+	0xaa, 0xc5, 0x7c, 0x1d, 0x40, 0xca, 0x5e, 0xa4, 0xad, 0x36, 0xaa, 0x41, 0xd1, 0x6a, 0x93, 0x06,
+	0xf5, 0xfd, 0x79, 0x26, 0x73, 0xe4, 0x08, 0x2b, 0x5c, 0x5a, 0x4d, 0x20, 0xf0, 0xe2, 0x62, 0x90,
+	0x61, 0x05, 0x6d, 0xbe, 0x17, 0xdc, 0xf2, 0x98, 0x04, 0x77, 0x3a, 0x82, 0x47, 0x6d, 0x73, 0xe0,
+	0x74, 0x04, 0x0f, 0x96, 0x34, 0x74, 0x54, 0x7a, 0x4c, 0xb9, 0xb3, 0x65, 0xc5, 0x92, 0x7f, 0x9e,
+	0xee, 0x4a, 0xf7, 0xf9, 0xb4, 0xef, 0x3e, 0xa5, 0xe3, 0xfa, 0xef, 0x48, 0x3c, 0xe3, 0x7e, 0x42,
+	0x53, 0x28, 0xc6, 0x36, 0x76, 0x9d, 0x20, 0xce, 0xbd, 0xe9, 0x1f, 0xfe, 0xf3, 0x5d, 0xe6, 0xd9,
+	0x6d, 0xeb, 0x1b, 0x14, 0x35, 0x63, 0x5b, 0xf2, 0xff, 0x59, 0xb6, 0x24, 0x80, 0x49, 0xb3, 0x2f,
+	0x2e, 0xcc, 0xf5, 0x97, 0x4a, 0xb7, 0x37, 0x0b, 0x50, 0xea, 0x32, 0x7a, 0xd6, 0x6a, 0x50, 0xe6,
+	0x89, 0x1d, 0x1a, 0x09, 0xfd, 0xd4, 0x55, 0x9f, 0x80, 0x43, 0x1e, 0xf3, 0x1f, 0x39, 0x40, 0xbd,
+	0xb6, 0xc3, 0x2d, 0xde, 0xa5, 0x8e, 0x7d, 0x15, 0xaf, 0xc6, 0x2d, 0x1e, 0xcb, 0x61, 0xec, 0xd3,
+	0xf9, 0xbc, 0x6a, 0x4d, 0xe2, 0x7a, 0xf1, 0xfc, 0x61, 0x99, 0x0f, 0x62, 0x49, 0x43, 0x6b, 0x30,
+	0xd3, 0x15, 0xc8, 0x1b, 0xc4, 0x6d, 0x50, 0xcf, 0xbf, 0x79, 0xe2, 0x8c, 0x46, 0xaa, 0xff, 0xa5,
+	0x64, 0x66, 0xae, 0x26, 0xf0, 0xe0, 0x44, 0x49, 0xb4, 0x09, 0xa5, 0x6d, 0x7f, 0x9b, 0x94, 0x1b,
+	0x5b, 0x1c, 0xe8, 0x64, 0xa4, 0x2f, 0x08, 0xfe, 0xc5, 0x21, 0x2c, 0xba, 0x02, 0x85, 0x26, 0x6d,
+	0xb5, 0x67, 0x87, 0x04, 0xfc, 0xff, 0x66, 0xbd, 0x0b, 0xd5, 0x11, 0xee, 0xf2, 0xf9, 0x5f, 0x58,
+	0xe0, 0x98, 0xdf, 0x02, 0xb9, 0x2b, 0x59, 0xb6, 0xf7, 0xe0, 0x40, 0xf2, 0x30, 0x0c, 0xef, 0x50,
+	0x37, 0xd8, 0x4e, 0x0d, 0xec, 0x9a, 0x1c, 0xc6, 0x3e, 0xdd, 0xfc, 0xa5, 0x01, 0x53, 0x62, 0x06,
+	0xeb, 0xdd, 0x4d, 0x56, 0x73, 0x2d, 0x87, 0x27, 0x22, 0xb7, 0x77, 0x36, 0x67, 0x61, 0x92, 0xd1,
+	0xf6, 0x0e, 0x75, 0x97, 0xed, 0x0e, 0xf3, 0x5c, 0x62, 0x75, 0x3c, 0x35, 0xad, 0x59, 0xc5, 0x3d,
+	0xb9, 0x1e, 0xa3, 0xe3, 0x1e, 0x09, 0xf3, 0x17, 0x05, 0x18, 0x3e, 0xef, 0x52, 0xab, 0xd1, 0xf4,
+	0xd0, 0xd7, 0x61, 0xa4, 0xad, 0xf2, 0x35, 0x31, 0x3f, 0x7e, 0x12, 0x32, 0x49, 0xae, 0xe8, 0x49,
+	0x72, 0xc5, 0xd9, 0x6e, 0xf0, 0x01, 0x56, 0xe1, 0xdc, 0x95, 0x9d, 0x13, 0x95, 0x17, 0x36, 0x5f,
+	0xa3, 0x35, 0x8f, 0xe7, 0x7a, 0x61, 0x98, 0x0a, 0xc7, 0x70, 0x80, 0xca, 0x4d, 0x98, 0xb4, 0x2c,
+	0xc2, 0x66, 0x87, 0xa3, 0x26, 0xbc, 0xc4, 0x07, 0xb1, 0xa4, 0xf1, 0xab, 0x75, 0x83, 0xb8, 0xb4,
+	0x69, 0x77, 0x19, 0x9d, 0x1d, 0x89, 0xa6, 0x00, 0x2f, 0xfa, 0x04, 0x1c, 0xf2, 0xa0, 0xeb, 0x30,
+	0x5c, 0xb3, 0xdb, 0x6d, 0xcb, 0xf3, 0x5d, 0xf9, 0x42, 0x3a, 0x03, 0xba, 0x60, 0x79, 0xcb, 0x42,
+	0x2e, 0x3c, 0x07, 0xf9, 0x3f, 0xc3, 0x3e, 0x20, 0x5a, 0x0f, 0x9c, 0x52, 0x41, 0x40, 0x3f, 0x92,
+	0x0e, 0x5a, 0xf8, 0x8a, 0x7e, 0xfe, 0x87, 0x83, 0x8a, 0xdb, 0xca, 0x66, 0x87, 0xb2, 0x80, 0x0a,
+	0x83, 0x0a, 0x41, 0xc5, 0xbf, 0x0c, 0x2b, 0x28, 0xf4, 0x72, 0x10, 0xe8, 0x8b, 0xe2, 0xec, 0x4e,
+	0xa5, 0x03, 0x55, 0x87, 0xaf, 0xb2, 0x8c, 0xf1, 0x68, 0x76, 0xe0, 0xe7, 0x01, 0xe6, 0xfb, 0x06,
+	0x94, 0x15, 0xe7, 0xaa, 0xc5, 0x3c, 0xf4, 0x4a, 0x8f, 0xa9, 0x54, 0xd2, 0x99, 0x0a, 0x97, 0x16,
+	0x86, 0x12, 0xe4, 0x11, 0xfe, 0x88, 0x66, 0x26, 0x18, 0x86, 0x2c, 0x8f, 0xb6, 0xfd, 0xb2, 0xe3,
+	0xb1, 0x4c, 0x2b, 0xd1, 0x1c, 0x36, 0xc7, 0xc0, 0x12, 0xca, 0xfc, 0xac, 0x00, 0x93, 0x8a, 0x23,
+	0x43, 0xe6, 0x1c, 0x35, 0xc6, 0x62, 0x36, 0x63, 0xcc, 0xdd, 0x39, 0x63, 0xcc, 0xdf, 0x09, 0x63,
+	0x2c, 0xdc, 0x3e, 0x63, 0x7c, 0x03, 0x26, 0x77, 0xa8, 0x6b, 0x6d, 0x59, 0x35, 0x51, 0x82, 0xad,
+	0x74, 0xb6, 0x6c, 0xe5, 0xdc, 0x9f, 0x48, 0x07, 0x7f, 0x2d, 0x26, 0x5d, 0x9d, 0xe1, 0x0e, 0x2d,
+	0x3e, 0x8a, 0x7b, 0xb4, 0xa0, 0xb7, 0x0d, 0x98, 0xd6, 0x07, 0x2f, 0x5a, 0xcc, 0xb3, 0xdd, 0xdd,
+	0xd9, 0x61, 0xb1, 0xb8, 0x41, 0xb5, 0xdf, 0xa7, 0xd6, 0x39, 0x7d, 0xad, 0x17, 0x1a, 0x27, 0xe9,
+	0x33, 0xff, 0x99, 0x87, 0xb1, 0xc8, 0xdd, 0x42, 0x37, 0x00, 0x24, 0x23, 0xad, 0xaf, 0x74, 0x54,
+	0x8e, 0xb3, 0x3c, 0xc0, 0x25, 0x55, 0xb3, 0xe3, 0x28, 0xb2, 0x94, 0x0e, 0x7c, 0x6e, 0x48, 0xc0,
+	0x9a, 0x2a, 0xf4, 0x26, 0x94, 0x89, 0xaa, 0xfe, 0xce, 0xdb, 0xae, 0x32, 0xcb, 0xb3, 0x83, 0x68,
+	0x5e, 0x0a, 0x61, 0xe2, 0x55, 0x7c, 0x48, 0xc1, 0xba, 0xb6, 0x39, 0x17, 0x26, 0x62, 0xf3, 0x4d,
+	0xa8, 0xc4, 0x57, 0xf4, 0x4a, 0x3c, 0xb5, 0xeb, 0xf2, 0x71, 0x45, 0x49, 0xab, 0x97, 0xff, 0x0c,
+	0x26, 0xe3, 0x33, 0xbd, 0x6d, 0x4a, 0x23, 0x75, 0xb4, 0xde, 0x33, 0xf8, 0x5d, 0x0e, 0x4a, 0xc1,
+	0x25, 0xce, 0x12, 0xea, 0xe7, 0x20, 0x67, 0xd5, 0x55, 0xa0, 0x07, 0xc5, 0x95, 0x5b, 0x39, 0x8b,
+	0x73, 0x56, 0x1d, 0x3d, 0x08, 0xc5, 0x4d, 0x97, 0x74, 0x6a, 0x4d, 0x15, 0xda, 0x83, 0xfb, 0x56,
+	0x15, 0xa3, 0x58, 0x51, 0x79, 0xaa, 0xee, 0x91, 0x86, 0x48, 0xcf, 0xb4, 0x54, 0x7d, 0x83, 0x34,
+	0x30, 0x1f, 0x47, 0x17, 0x60, 0x4a, 0xd6, 0xa6, 0xcb, 0x4d, 0x5a, 0xdb, 0x96, 0x53, 0x14, 0xf7,
+	0xb1, 0x54, 0xbd, 0x57, 0x31, 0x4f, 0x5d, 0x8c, 0x33, 0xe0, 0x5e, 0x19, 0xbd, 0xba, 0x2f, 0xee,
+	0x5f, 0xdd, 0xf3, 0xa9, 0x93, 0xae, 0xd7, 0xb4, 0x5d, 0x15, 0xec, 0x83, 0xa9, 0x2f, 0x89, 0x51,
+	0xac, 0xa8, 0xe6, 0x34, 0x4c, 0x5d, 0xb0, 0xbc, 0x8b, 0xdd, 0xcd, 0xb5, 0x6e, 0xab, 0x85, 0xe9,
+	0xeb, 0x5d, 0x9e, 0x2d, 0xcb, 0xc1, 0x55, 0x12, 0x19, 0xfc, 0xd5, 0x10, 0x8c, 0x5d, 0xb0, 0x3c,
+	0xb1, 0x81, 0x99, 0xb3, 0xe7, 0x75, 0x38, 0x6c, 0x75, 0x18, 0xad, 0x75, 0x5d, 0xba, 0xbe, 0x6d,
+	0x39, 0x1b, 0xab, 0xeb, 0xc2, 0x7c, 0x76, 0x55, 0xf2, 0x7e, 0x54, 0x09, 0x1e, 0x5e, 0x49, 0x62,
+	0xc2, 0xc9, 0xb2, 0xe8, 0x24, 0x80, 0x4b, 0x49, 0xbd, 0xaa, 0x1f, 0x51, 0x70, 0x1b, 0x71, 0x40,
+	0xc1, 0x1a, 0x17, 0x5a, 0x84, 0xf2, 0x0d, 0xd7, 0xf2, 0xa8, 0x12, 0x92, 0x47, 0x16, 0xdc, 0xa3,
+	0x17, 0x43, 0x12, 0xd6, 0xf9, 0xd0, 0x0e, 0x94, 0x9d, 0x70, 0x2f, 0x94, 0x33, 0x4d, 0xe9, 0x3e,
+	0xb4, 0x4d, 0x5c, 0x73, 0xed, 0xb6, 0xcd, 0xfd, 0xd4, 0x65, 0x5a, 0x6b, 0x92, 0x8e, 0xc5, 0xda,
+	0xd5, 0x09, 0xae, 0x57, 0x63, 0xc1, 0xba, 0x22, 0xd4, 0x80, 0xa2, 0x4b, 0x3b, 0x75, 0xea, 0xaa,
+	0xb4, 0x22, 0xa5, 0xca, 0xe7, 0xf9, 0x10, 0x16, 0x82, 0x09, 0x2a, 0x81, 0xdb, 0x81, 0xa4, 0x62,
+	0x05, 0x8f, 0x3a, 0x7a, 0x9d, 0x31, 0x2c, 0x74, 0x2d, 0xa5, 0xd4, 0xe5, 0x8b, 0x25, 0x68, 0xea,
+	0x5f, 0x73, 0x5c, 0x57, 0x35, 0xc7, 0x88, 0x50, 0xf5, 0x4c, 0x3a, 0x55, 0xbc, 0xc6, 0x48, 0xd0,
+	0x12, 0xaf, 0x3f, 0xfe, 0x58, 0x80, 0x89, 0x0b, 0xd6, 0xc0, 0xc9, 0xbf, 0x07, 0xf7, 0xc8, 0x90,
+	0xbf, 0x4e, 0x5b, 0xb4, 0xc6, 0xa5, 0xd7, 0x3d, 0x97, 0x78, 0xb4, 0xe1, 0x17, 0xe3, 0x67, 0x94,
+	0xe8, 0x3d, 0xcb, 0xc9, 0x6c, 0xb7, 0xfa, 0x93, 0x70, 0x3f, 0xe8, 0xd4, 0xbe, 0x26, 0xa9, 0xf0,
+	0x28, 0x64, 0x2d, 0x3c, 0x78, 0x62, 0x45, 0x5a, 0x2d, 0xfb, 0xc6, 0x06, 0x69, 0x30, 0xe5, 0x8a,
+	0x82, 0xc4, 0x6a, 0xc9, 0x27, 0xe0, 0x90, 0x07, 0x55, 0x00, 0xac, 0x46, 0xc7, 0x76, 0xa9, 0x90,
+	0x28, 0x8a, 0xee, 0xd2, 0x38, 0xbf, 0x67, 0x2b, 0xc1, 0x28, 0xd6, 0x38, 0xfa, 0x5f, 0xf8, 0xe1,
+	0x2f, 0x70, 0xe1, 0x1f, 0x87, 0x51, 0xab, 0x53, 0x6b, 0x75, 0xeb, 0x74, 0x8d, 0x78, 0x4d, 0x36,
+	0x3b, 0x22, 0xa6, 0x31, 0xb9, 0x77, 0xf3, 0xd8, 0xe8, 0x8a, 0x36, 0x8e, 0x23, 0x5c, 0x5c, 0x8a,
+	0xbe, 0xa1, 0x49, 0x95, 0x42, 0xa9, 0x73, 0x6f, 0xe8, 0x52, 0x3a, 0x97, 0xf9, 0x67, 0x03, 0x8a,
+	0xd2, 0x29, 0xa3, 0xc5, 0x58, 0x13, 0xef, 0x68, 0x4f, 0x13, 0xaf, 0x9c, 0xd4, 0x8b, 0x35, 0xa1,
+	0x68, 0x31, 0xd6, 0xa5, 0x32, 0x15, 0x2d, 0xc9, 0x6b, 0xb7, 0x22, 0x46, 0xb0, 0xa2, 0x20, 0x0b,
+	0x80, 0xf8, 0x5d, 0x38, 0x3f, 0xaf, 0x5c, 0xcc, 0xda, 0xa6, 0x8c, 0xb5, 0x28, 0x03, 0x02, 0xc3,
+	0x1a, 0xb8, 0xf9, 0x73, 0x03, 0xee, 0xe5, 0x97, 0x44, 0xe4, 0x8a, 0x67, 0xa9, 0xc3, 0xef, 0x7d,
+	0xa7, 0xb6, 0xab, 0x7c, 0xb9, 0xf0, 0xa5, 0x8e, 0xcd, 0x2c, 0x91, 0xae, 0x19, 0x71, 0x5f, 0xea,
+	0x53, 0xb0, 0xc6, 0x95, 0xa2, 0x4a, 0x5e, 0x80, 0x92, 0x48, 0x49, 0xf9, 0x96, 0x2a, 0xbb, 0x0e,
+	0xcc, 0x6c, 0xd9, 0x27, 0xe0, 0x90, 0xc7, 0xfc, 0x8b, 0x01, 0x13, 0x03, 0x75, 0xcb, 0x9e, 0x85,
+	0x71, 0x91, 0x0c, 0xb0, 0xf3, 0x56, 0x4b, 0x9c, 0xa0, 0x9a, 0xd5, 0x11, 0xc5, 0x3d, 0x7e, 0x2d,
+	0x42, 0xc5, 0x31, 0x6e, 0xbf, 0xdb, 0x96, 0x3f, 0xa8, 0xdb, 0x56, 0x18, 0xa0, 0xdb, 0xf6, 0x89,
+	0x01, 0x47, 0x92, 0x5d, 0x17, 0x7a, 0x35, 0xd6, 0x75, 0x5b, 0x4c, 0xef, 0x08, 0x53, 0xb4, 0xda,
+	0x78, 0xf8, 0x50, 0xd5, 0x85, 0x4c, 0x3b, 0x9f, 0x4b, 0x0f, 0x9f, 0x68, 0x26, 0xfd, 0x2a, 0x0e,
+	0xf3, 0x37, 0x06, 0xc8, 0xf3, 0xc8, 0xe2, 0x68, 0x4f, 0x02, 0x34, 0x54, 0x42, 0x81, 0x57, 0xd5,
+	0x79, 0x05, 0x36, 0x77, 0x21, 0xa0, 0x60, 0x8d, 0xcb, 0x4f, 0xb5, 0xf2, 0x7d, 0x52, 0xad, 0x07,
+	0xa1, 0x58, 0x97, 0x5d, 0xc1, 0x42, 0xd4, 0x8b, 0xaa, 0x96, 0xa0, 0xa2, 0x9a, 0xef, 0x16, 0x60,
+	0x4a, 0xcc, 0x77, 0xd0, 0x20, 0x31, 0xc8, 0xdc, 0x1d, 0x38, 0x22, 0xce, 0xa5, 0x37, 0xae, 0xc8,
+	0xe5, 0x9c, 0x56, 0xf2, 0x47, 0x56, 0x12, 0xb9, 0x6e, 0xf5, 0xa5, 0xe0, 0x3e, 0xb8, 0xff, 0x29,
+	0xc1, 0xe2, 0x51, 0x18, 0x71, 0x5a, 0xc4, 0xdb, 0xb2, 0xdd, 0xb6, 0x4a, 0x57, 0x83, 0xfe, 0xc4,
+	0x9a, 0x1a, 0xc7, 0x01, 0x47, 0xff, 0xd0, 0x32, 0x32, 0x78, 0x68, 0x31, 0x3b, 0x70, 0x44, 0x4b,
+	0x9a, 0xee, 0x7c, 0x43, 0xfa, 0x6d, 0x03, 0x8e, 0xee, 0x9b, 0xa5, 0xa1, 0x7a, 0xcc, 0x35, 0x3c,
+	0x93, 0x39, 0xf5, 0x4b, 0xd3, 0x8c, 0x7f, 0xd7, 0x80, 0x99, 0xc1, 0xfb, 0xf0, 0xc7, 0xa1, 0xe0,
+	0x84, 0xbe, 0x36, 0x88, 0x00, 0xc2, 0xc3, 0x0a, 0x4a, 0x74, 0x63, 0xf2, 0x29, 0x36, 0xe6, 0x2d,
+	0x03, 0xee, 0xdb, 0x27, 0xa5, 0x44, 0x9b, 0xb1, 0x6d, 0x39, 0x93, 0x31, 0x4b, 0x4d, 0xb3, 0x29,
+	0x3f, 0xcd, 0xc1, 0xf0, 0x9a, 0x6b, 0xbf, 0x46, 0x6b, 0x77, 0xa3, 0x2d, 0xfb, 0x02, 0x14, 0x98,
+	0x43, 0x6b, 0xaa, 0x10, 0x3e, 0x91, 0xb2, 0xa8, 0x90, 0xd3, 0x5b, 0x77, 0x68, 0x4d, 0xe6, 0xbf,
+	0xfc, 0x2f, 0x2c, 0x80, 0xb4, 0x5e, 0x64, 0x3e, 0x4b, 0x6d, 0xed, 0x43, 0x1e, 0xdc, 0x8b, 0x54,
+	0x9c, 0x5f, 0xda, 0x5e, 0xa4, 0x9a, 0x5f, 0x9f, 0x5e, 0xe4, 0x0f, 0xc2, 0x15, 0xf0, 0x4d, 0x43,
+	0xdf, 0x84, 0x29, 0xc7, 0xb7, 0xb3, 0x35, 0xbb, 0x65, 0xd5, 0xac, 0xac, 0xe1, 0x78, 0x2d, 0x22,
+	0xbe, 0x1b, 0x56, 0xf5, 0x6b, 0x71, 0x5c, 0xdc, 0xab, 0xca, 0xb4, 0x61, 0x2c, 0xb2, 0xf5, 0xe8,
+	0x94, 0xff, 0x26, 0x21, 0x9a, 0x6e, 0xca, 0x37, 0x09, 0xb7, 0x6e, 0x1e, 0x1b, 0x55, 0xec, 0xfa,
+	0x1b, 0x85, 0x2c, 0x5f, 0xfe, 0x7f, 0x96, 0x83, 0x52, 0x30, 0xb3, 0xbb, 0x60, 0xe0, 0x5f, 0x89,
+	0x18, 0xf8, 0xa9, 0x8c, 0x7b, 0x9a, 0x68, 0xe2, 0xaf, 0xc6, 0x4c, 0x3c, 0xeb, 0x41, 0x1d, 0x60,
+	0xe4, 0x9f, 0x1b, 0xe2, 0x4c, 0x24, 0xaf, 0x68, 0x6c, 0x1e, 0xdc, 0xab, 0x26, 0x30, 0xbc, 0x25,
+	0xdb, 0x75, 0x6a, 0xa1, 0x4f, 0x64, 0xea, 0xf1, 0x05, 0x6d, 0xf1, 0xf0, 0xe0, 0x7c, 0x8a, 0x8f,
+	0x8b, 0xbe, 0x7a, 0x7b, 0x56, 0x0d, 0x09, 0x2b, 0xfe, 0x40, 0x5f, 0xf1, 0x5d, 0xb8, 0xd8, 0x1b,
+	0xd1, 0x8b, 0xbd, 0x90, 0x71, 0x25, 0x7d, 0xae, 0xf6, 0xf7, 0x73, 0x30, 0xdd, 0x1b, 0x33, 0x18,
+	0x62, 0x30, 0xde, 0xd0, 0x5b, 0x57, 0xfe, 0xfd, 0x3e, 0x95, 0xfa, 0xeb, 0x40, 0x28, 0x1b, 0x96,
+	0x14, 0x91, 0x61, 0x86, 0x63, 0x2a, 0xd0, 0x9b, 0x30, 0x49, 0xa2, 0x2f, 0x2c, 0xfc, 0xd5, 0x66,
+	0xad, 0xf0, 0x94, 0xe2, 0x20, 0x67, 0x8b, 0x11, 0x18, 0xee, 0x51, 0x64, 0xbe, 0x63, 0xc0, 0x44,
+	0xcc, 0x2d, 0xf1, 0x90, 0xce, 0xbc, 0x84, 0x90, 0xae, 0x9a, 0xa9, 0x82, 0x86, 0xd6, 0x60, 0x86,
+	0x74, 0x3d, 0x3b, 0x90, 0x3d, 0xd7, 0x21, 0x9b, 0x2d, 0x5a, 0x57, 0x49, 0x4d, 0xf0, 0x09, 0x7b,
+	0x29, 0x81, 0x07, 0x27, 0x4a, 0x9a, 0x5f, 0xd3, 0x2c, 0x4b, 0x38, 0xdc, 0x54, 0xf3, 0x78, 0x38,
+	0x7a, 0x9d, 0x4a, 0xfd, 0xaf, 0x85, 0xf9, 0x79, 0x4e, 0x5b, 0xab, 0xf2, 0xa1, 0x97, 0x00, 0xb5,
+	0x08, 0xf3, 0x2e, 0x92, 0x4e, 0x9d, 0xcf, 0x8c, 0x6e, 0xb9, 0x94, 0xf9, 0xed, 0xbe, 0x39, 0x85,
+	0x84, 0x56, 0x7b, 0x38, 0x70, 0x82, 0x14, 0x5a, 0x8c, 0xfa, 0xe3, 0x63, 0x71, 0x7f, 0x3c, 0x1e,
+	0x6e, 0xf4, 0x60, 0x1e, 0x19, 0xbd, 0xae, 0xdd, 0xb5, 0x7c, 0x96, 0x4f, 0x13, 0xb1, 0x65, 0x57,
+	0xfc, 0x17, 0x7f, 0xf2, 0xfb, 0x40, 0x70, 0x01, 0xfd, 0xe1, 0xf0, 0x02, 0xce, 0x3d, 0x0d, 0x63,
+	0x11, 0xe6, 0x4c, 0x2f, 0xf4, 0xfe, 0x6a, 0xc0, 0xd1, 0x7d, 0xdb, 0x9a, 0x3c, 0x07, 0x91, 0x0b,
+	0x50, 0xbe, 0xe3, 0xc9, 0xd4, 0x37, 0x2d, 0xda, 0x8b, 0x96, 0xce, 0x4a, 0x0e, 0x63, 0x05, 0xa9,
+	0xc0, 0x5b, 0x64, 0x53, 0x79, 0xda, 0xf4, 0xe0, 0xd1, 0x9e, 0x76, 0x00, 0xbe, 0x4a, 0x24, 0x78,
+	0x8b, 0x6c, 0x9a, 0xef, 0xe5, 0x60, 0x92, 0x5f, 0xe3, 0x48, 0x65, 0xb8, 0x06, 0xf9, 0x86, 0xe5,
+	0xa9, 0xb5, 0x2c, 0xa6, 0x56, 0xa7, 0x63, 0x54, 0x87, 0x79, 0xa5, 0xca, 0x7d, 0x06, 0x87, 0x42,
+	0x2f, 0xf9, 0xf9, 0x75, 0xa6, 0x25, 0xf4, 0xd4, 0xac, 0xd5, 0x52, 0x4f, 0x52, 0xfe, 0x92, 0xff,
+	0x52, 0x25, 0x9f, 0x05, 0xb9, 0xe7, 0xbd, 0x84, 0x44, 0xd6, 0x9f, 0xb7, 0x98, 0x3f, 0xc9, 0x81,
+	0xbc, 0xa4, 0x77, 0x21, 0x69, 0xb8, 0x1c, 0x49, 0x1a, 0x52, 0xc6, 0x07, 0x31, 0xb9, 0xc4, 0x84,
+	0x21, 0x1e, 0x3a, 0x4f, 0x64, 0x01, 0xdc, 0x3f, 0x59, 0xf8, 0x83, 0x01, 0x25, 0xc1, 0x77, 0x17,
+	0xc2, 0xe6, 0x5a, 0x34, 0x6c, 0x3e, 0x92, 0x61, 0x15, 0x7d, 0x42, 0xe6, 0x0f, 0xf3, 0x6a, 0xf6,
+	0x81, 0x6b, 0x6e, 0x12, 0xb7, 0xae, 0x3c, 0x65, 0xe8, 0x9a, 0xf9, 0x20, 0x96, 0x34, 0xd4, 0x82,
+	0x31, 0xa6, 0x19, 0x0a, 0x53, 0xeb, 0x4c, 0x19, 0x4c, 0x75, 0x1b, 0x63, 0xd5, 0x29, 0xf1, 0x72,
+	0x4f, 0x1f, 0xc2, 0x51, 0x70, 0xf4, 0x3d, 0x03, 0xa6, 0x9d, 0xde, 0x98, 0xae, 0x0c, 0xe3, 0xa9,
+	0x8c, 0x7e, 0x32, 0x04, 0xa8, 0xde, 0xb3, 0x77, 0xf3, 0x58, 0x52, 0xb6, 0x80, 0x93, 0xd4, 0xa1,
+	0x26, 0x8c, 0xea, 0x1f, 0x9a, 0x95, 0x19, 0x9d, 0xcc, 0xfe, 0x45, 0x5b, 0x76, 0x9e, 0xf5, 0x11,
+	0x1c, 0x41, 0x36, 0x7f, 0x5c, 0x84, 0xb2, 0x66, 0x77, 0x7d, 0x42, 0x59, 0x79, 0xa0, 0x50, 0x76,
+	0x22, 0x1a, 0xca, 0xee, 0x8b, 0x87, 0x32, 0x10, 0x8a, 0x23, 0x61, 0xcc, 0x85, 0xf1, 0x5a, 0xd7,
+	0x75, 0x69, 0xc7, 0x3b, 0x7f, 0x5b, 0xd2, 0x5b, 0xc4, 0x53, 0xa7, 0xe5, 0x08, 0x22, 0x8e, 0x69,
+	0xe0, 0xb9, 0x74, 0x53, 0xbd, 0x1c, 0xc8, 0x67, 0x79, 0x39, 0xd0, 0x3f, 0x97, 0xf6, 0x5f, 0x0b,
+	0xf8, 0xb8, 0x68, 0x0d, 0x8a, 0xf2, 0x03, 0xab, 0xfa, 0x04, 0xf5, 0x68, 0xda, 0xd6, 0x28, 0x97,
+	0x91, 0x81, 0x43, 0xfe, 0x8d, 0x15, 0x8e, 0x1e, 0xef, 0x4b, 0x07, 0xc4, 0xfb, 0x4b, 0x80, 0xec,
+	0x4d, 0x46, 0xdd, 0x1d, 0x5a, 0xbf, 0x20, 0x7f, 0xe0, 0xc0, 0x4d, 0xaa, 0x78, 0xdc, 0x78, 0x28,
+	0x1f, 0x1e, 0xe9, 0x0b, 0x3d, 0x1c, 0x38, 0x41, 0x0a, 0x75, 0x61, 0x52, 0xed, 0x5e, 0x60, 0xcb,
+	0xea, 0x03, 0x5e, 0xd6, 0x4a, 0x2b, 0x7c, 0xe9, 0xb1, 0x1c, 0x03, 0xc4, 0x3d, 0x2a, 0xb8, 0x13,
+	0xe0, 0xf6, 0x15, 0xea, 0x84, 0xc1, 0x75, 0x0a, 0x27, 0xb0, 0xaa, 0xa3, 0xe1, 0x28, 0xb8, 0xb9,
+	0x08, 0x53, 0xf2, 0x4a, 0xe8, 0x41, 0xf9, 0xe0, 0x97, 0xf7, 0xbf, 0x37, 0x20, 0xea, 0x5c, 0xa2,
+	0x2f, 0x8a, 0x8c, 0x14, 0x2f, 0x8a, 0x6e, 0xc0, 0x78, 0xd7, 0x61, 0x9e, 0x4b, 0x49, 0x5b, 0xcc,
+	0xc0, 0x77, 0xbd, 0x4f, 0x66, 0x09, 0x20, 0x7a, 0x58, 0x0d, 0xca, 0x87, 0xab, 0x11, 0x58, 0x1c,
+	0x53, 0x63, 0xfe, 0x2b, 0x07, 0x11, 0x2f, 0x81, 0xde, 0x31, 0x60, 0x8a, 0xc4, 0x7e, 0x86, 0xe0,
+	0x17, 0x32, 0xcf, 0x65, 0xfb, 0x6d, 0x48, 0xcf, 0xaf, 0x18, 0xc2, 0x96, 0x45, 0x9c, 0x85, 0xe1,
+	0x5e, 0xa5, 0xc2, 0x27, 0x93, 0xde, 0xdf, 0x99, 0x64, 0xf3, 0xc9, 0x09, 0x3f, 0x54, 0x91, 0x3e,
+	0x39, 0x81, 0x80, 0x93, 0xd4, 0xa1, 0x97, 0xa1, 0x40, 0xdc, 0x86, 0xff, 0xdd, 0x2c, 0xbb, 0x5a,
+	0xff, 0xe7, 0x43, 0xa1, 0xed, 0x2c, 0xb9, 0x0d, 0x86, 0x05, 0xa8, 0xf9, 0xb7, 0x3c, 0xf4, 0xbc,
+	0x78, 0x52, 0xaf, 0x45, 0x0a, 0x89, 0xaf, 0x45, 0xee, 0x87, 0x21, 0x52, 0xf3, 0x82, 0x17, 0x17,
+	0xe1, 0xf3, 0x4a, 0x3e, 0x88, 0x25, 0x0d, 0xbd, 0x08, 0x25, 0xe6, 0x11, 0xd7, 0xdb, 0xb0, 0xda,
+	0x54, 0x3d, 0x23, 0xf8, 0x9f, 0x74, 0xf9, 0x01, 0x97, 0x90, 0x1f, 0xd4, 0xd7, 0x7d, 0x00, 0x1c,
+	0x62, 0xa1, 0xd3, 0x51, 0xcf, 0x6e, 0xc6, 0x3d, 0xfb, 0x94, 0xbe, 0x96, 0x41, 0xeb, 0x94, 0x36,
+	0x94, 0xb5, 0x73, 0x50, 0x31, 0xf0, 0x4c, 0xe6, 0x7d, 0xd7, 0xfc, 0xb3, 0xfc, 0x0d, 0x52, 0x48,
+	0xd1, 0xf1, 0xd1, 0x75, 0x80, 0x2d, 0xab, 0x63, 0xb1, 0xa6, 0xd8, 0xad, 0x62, 0xe6, 0xdd, 0x12,
+	0xdf, 0x1c, 0xce, 0x07, 0x08, 0x58, 0x43, 0x33, 0x27, 0x60, 0x2c, 0xf2, 0x82, 0x49, 0x74, 0xc5,
+	0x02, 0x0f, 0xf0, 0x65, 0xed, 0x8a, 0x05, 0x13, 0xbc, 0x9d, 0x5d, 0xb1, 0x10, 0x74, 0xff, 0x44,
+	0xf7, 0x03, 0x03, 0xc6, 0x02, 0xde, 0x2f, 0x6d, 0x8f, 0x28, 0x98, 0x61, 0x9f, 0x84, 0xf7, 0xd7,
+	0xfa, 0x2a, 0xa2, 0x49, 0x6f, 0x6e, 0x9f, 0xa4, 0x97, 0xf5, 0x26, 0xbd, 0x19, 0x12, 0x93, 0x78,
+	0x41, 0xa9, 0xfd, 0x6a, 0x65, 0x9f, 0xdc, 0xd7, 0x7c, 0x3f, 0x07, 0x13, 0xb1, 0xd3, 0xe9, 0x93,
+	0x0e, 0x16, 0x07, 0x4a, 0x07, 0xb5, 0xab, 0x9f, 0x1f, 0x28, 0x65, 0x29, 0x0c, 0x94, 0xb2, 0x58,
+	0x50, 0xe6, 0x93, 0xf1, 0xf3, 0xc9, 0xa1, 0x2f, 0xd6, 0x2e, 0xe5, 0x2e, 0x64, 0x35, 0x84, 0xc3,
+	0x3a, 0x76, 0xf5, 0xd2, 0x87, 0x9f, 0xce, 0x1f, 0xfa, 0xe8, 0xd3, 0xf9, 0x43, 0x1f, 0x7f, 0x3a,
+	0x7f, 0xe8, 0xdb, 0x7b, 0xf3, 0xc6, 0x87, 0x7b, 0xf3, 0xc6, 0x47, 0x7b, 0xf3, 0xc6, 0xc7, 0x7b,
+	0xf3, 0xc6, 0x27, 0x7b, 0xf3, 0xc6, 0x8f, 0xfe, 0x3e, 0x7f, 0xe8, 0xfa, 0x03, 0x69, 0x7e, 0x7f,
+	0xfb, 0xef, 0x00, 0x00, 0x00, 0xff, 0xff, 0x19, 0x4d, 0x94, 0xce, 0xa6, 0x3b, 0x00, 0x00,
 }
 
 func (m *AnalysisRunArgument) Marshal() (dAtA []byte, err error) {
@@ -4199,6 +4202,11 @@ func (m *PromotionStatus) MarshalToSizedBuffer(dAtA []byte) (int, error) {
 	_ = i
 	var l int
 	_ = l
+	i -= len(m.LastHandledRefresh)
+	copy(dAtA[i:], m.LastHandledRefresh)
+	i = encodeVarintGenerated(dAtA, i, uint64(len(m.LastHandledRefresh)))
+	i--
+	dAtA[i] = 0x22
 	if len(m.Metadata) > 0 {
 		keysForMetadata := make([]string, 0, len(m.Metadata))
 		for k := range m.Metadata {
@@ -4528,6 +4536,11 @@ func (m *StageStatus) MarshalToSizedBuffer(dAtA []byte) (int, error) {
 	_ = i
 	var l int
 	_ = l
+	i -= len(m.LastHandledRefresh)
+	copy(dAtA[i:], m.LastHandledRefresh)
+	i = encodeVarintGenerated(dAtA, i, uint64(len(m.LastHandledRefresh)))
+	i--
+	dAtA[i] = 0x5a
 	if m.LastPromotion != nil {
 		{
 			size, err := m.LastPromotion.MarshalToSizedBuffer(dAtA[:i])
@@ -4759,6 +4772,11 @@ func (m *VerificationInfo) MarshalToSizedBuffer(dAtA []byte) (int, error) {
 	_ = i
 	var l int
 	_ = l
+	i -= len(m.Actor)
+	copy(dAtA[i:], m.Actor)
+	i = encodeVarintGenerated(dAtA, i, uint64(len(m.Actor)))
+	i--
+	dAtA[i] = 0x3a
 	if m.FinishTime != nil {
 		{
 			size, err := m.FinishTime.MarshalToSizedBuffer(dAtA[:i])
@@ -5000,6 +5018,11 @@ func (m *WarehouseStatus) MarshalToSizedBuffer(dAtA []byte) (int, error) {
 	_ = i
 	var l int
 	_ = l
+	i -= len(m.LastHandledRefresh)
+	copy(dAtA[i:], m.LastHandledRefresh)
+	i = encodeVarintGenerated(dAtA, i, uint64(len(m.LastHandledRefresh)))
+	i--
+	dAtA[i] = 0x32
 	if m.LastFreight != nil {
 		{
 			size, err := m.LastFreight.MarshalToSizedBuffer(dAtA[:i])
@@ -5869,6 +5892,8 @@ func (m *PromotionStatus) Size() (n int) {
 			n += mapEntrySize + 1 + sovGenerated(uint64(mapEntrySize))
 		}
 	}
+	l = len(m.LastHandledRefresh)
+	n += 1 + l + sovGenerated(uint64(l))
 	return n
 }
 
@@ -6000,6 +6025,8 @@ func (m *StageStatus) Size() (n int) {
 		l = m.LastPromotion.Size()
 		n += 1 + l + sovGenerated(uint64(l))
 	}
+	l = len(m.LastHandledRefresh)
+	n += 1 + l + sovGenerated(uint64(l))
 	return n
 }
 
@@ -6080,6 +6107,8 @@ func (m *VerificationInfo) Size() (n int) {
 		l = m.FinishTime.Size()
 		n += 1 + l + sovGenerated(uint64(l))
 	}
+	l = len(m.Actor)
+	n += 1 + l + sovGenerated(uint64(l))
 	return n
 }
 
@@ -6156,6 +6185,8 @@ func (m *WarehouseStatus) Size() (n int) {
 		l = m.LastFreight.Size()
 		n += 1 + l + sovGenerated(uint64(l))
 	}
+	l = len(m.LastHandledRefresh)
+	n += 1 + l + sovGenerated(uint64(l))
 	return n
 }
 
@@ -6866,6 +6897,7 @@ func (this *PromotionStatus) String() string {
 		`Phase:` + fmt.Sprintf("%v", this.Phase) + `,`,
 		`Message:` + fmt.Sprintf("%v", this.Message) + `,`,
 		`Metadata:` + mapStringForMetadata + `,`,
+		`LastHandledRefresh:` + fmt.Sprintf("%v", this.LastHandledRefresh) + `,`,
 		`}`,
 	}, "")
 	return s
@@ -6952,6 +6984,7 @@ func (this *StageStatus) String() string {
 		`Health:` + strings.Replace(this.Health.String(), "Health", "Health", 1) + `,`,
 		`Message:` + fmt.Sprintf("%v", this.Message) + `,`,
 		`LastPromotion:` + strings.Replace(this.LastPromotion.String(), "PromotionInfo", "PromotionInfo", 1) + `,`,
+		`LastHandledRefresh:` + fmt.Sprintf("%v", this.LastHandledRefresh) + `,`,
 		`}`,
 	}, "")
 	return s
@@ -7015,6 +7048,7 @@ func (this *VerificationInfo) String() string {
 		`ID:` + fmt.Sprintf("%v", this.ID) + `,`,
 		`StartTime:` + strings.Replace(fmt.Sprintf("%v", this.StartTime), "Time", "v1.Time", 1) + `,`,
 		`FinishTime:` + strings.Replace(fmt.Sprintf("%v", this.FinishTime), "Time", "v1.Time", 1) + `,`,
+		`Actor:` + fmt.Sprintf("%v", this.Actor) + `,`,
 		`}`,
 	}, "")
 	return s
@@ -7080,6 +7114,7 @@ func (this *WarehouseStatus) String() string {
 		`Message:` + fmt.Sprintf("%v", this.Message) + `,`,
 		`ObservedGeneration:` + fmt.Sprintf("%v", this.ObservedGeneration) + `,`,
 		`LastFreight:` + strings.Replace(this.LastFreight.String(), "FreightReference", "FreightReference", 1) + `,`,
+		`LastHandledRefresh:` + fmt.Sprintf("%v", this.LastHandledRefresh) + `,`,
 		`}`,
 	}, "")
 	return s
@@ -14200,6 +14235,38 @@ func (m *PromotionStatus) Unmarshal(dAtA []byte) error {
 			}
 			m.Metadata[mapkey] = mapvalue
 			iNdEx = postIndex
+		case 4:
+			if wireType != 2 {
+				return fmt.Errorf("proto: wrong wireType = %d for field LastHandledRefresh", wireType)
+			}
+			var stringLen uint64
+			for shift := uint(0); ; shift += 7 {
+				if shift >= 64 {
+					return ErrIntOverflowGenerated
+				}
+				if iNdEx >= l {
+					return io.ErrUnexpectedEOF
+				}
+				b := dAtA[iNdEx]
+				iNdEx++
+				stringLen |= uint64(b&0x7F) << shift
+				if b < 0x80 {
+					break
+				}
+			}
+			intStringLen := int(stringLen)
+			if intStringLen < 0 {
+				return ErrInvalidLengthGenerated
+			}
+			postIndex := iNdEx + intStringLen
+			if postIndex < 0 {
+				return ErrInvalidLengthGenerated
+			}
+			if postIndex > l {
+				return io.ErrUnexpectedEOF
+			}
+			m.LastHandledRefresh = string(dAtA[iNdEx:postIndex])
+			iNdEx = postIndex
 		default:
 			iNdEx = preIndex
 			skippy, err := skipGenerated(dAtA[iNdEx:])
@@ -15250,6 +15317,38 @@ func (m *StageStatus) Unmarshal(dAtA []byte) error {
 				return err
 			}
 			iNdEx = postIndex
+		case 11:
+			if wireType != 2 {
+				return fmt.Errorf("proto: wrong wireType = %d for field LastHandledRefresh", wireType)
+			}
+			var stringLen uint64
+			for shift := uint(0); ; shift += 7 {
+				if shift >= 64 {
+					return ErrIntOverflowGenerated
+				}
+				if iNdEx >= l {
+					return io.ErrUnexpectedEOF
+				}
+				b := dAtA[iNdEx]
+				iNdEx++
+				stringLen |= uint64(b&0x7F) << shift
+				if b < 0x80 {
+					break
+				}
+			}
+			intStringLen := int(stringLen)
+			if intStringLen < 0 {
+				return ErrInvalidLengthGenerated
+			}
+			postIndex := iNdEx + intStringLen
+			if postIndex < 0 {
+				return ErrInvalidLengthGenerated
+			}
+			if postIndex > l {
+				return io.ErrUnexpectedEOF
+			}
+			m.LastHandledRefresh = string(dAtA[iNdEx:postIndex])
+			iNdEx = postIndex
 		default:
 			iNdEx = preIndex
 			skippy, err := skipGenerated(dAtA[iNdEx:])
@@ -15856,6 +15955,38 @@ func (m *VerificationInfo) Unmarshal(dAtA []byte) error {
 				return err
 			}
 			iNdEx = postIndex
+		case 7:
+			if wireType != 2 {
+				return fmt.Errorf("proto: wrong wireType = %d for field Actor", wireType)
+			}
+			var stringLen uint64
+			for shift := uint(0); ; shift += 7 {
+				if shift >= 64 {
+					return ErrIntOverflowGenerated
+				}
+				if iNdEx >= l {
+					return io.ErrUnexpectedEOF
+				}
+				b := dAtA[iNdEx]
+				iNdEx++
+				stringLen |= uint64(b&0x7F) << shift
+				if b < 0x80 {
+					break
+				}
+			}
+			intStringLen := int(stringLen)
+			if intStringLen < 0 {
+				return ErrInvalidLengthGenerated
+			}
+			postIndex := iNdEx + intStringLen
+			if postIndex < 0 {
+				return ErrInvalidLengthGenerated
+			}
+			if postIndex > l {
+				return io.ErrUnexpectedEOF
+			}
+			m.Actor = string(dAtA[iNdEx:postIndex])
+			iNdEx = postIndex
 		default:
 			iNdEx = preIndex
 			skippy, err := skipGenerated(dAtA[iNdEx:])
@@ -16428,6 +16559,38 @@ func (m *WarehouseStatus) Unmarshal(dAtA []byte) error {
 				return err
 			}
 			iNdEx = postIndex
+		case 6:
+			if wireType != 2 {
+				return fmt.Errorf("proto: wrong wireType = %d for field LastHandledRefresh", wireType)
+			}
+			var stringLen uint64
+			for shift := uint(0); ; shift += 7 {
+				if shift >= 64 {
+					return ErrIntOverflowGenerated
+				}
+				if iNdEx >= l {
+					return io.ErrUnexpectedEOF
+				}
+				b := dAtA[iNdEx]
+				iNdEx++
+				stringLen |= uint64(b&0x7F) << shift
+				if b < 0x80 {
+					break
+				}
+			}
+			intStringLen := int(stringLen)
+			if intStringLen < 0 {
+				return ErrInvalidLengthGenerated
+			}
+			postIndex := iNdEx + intStringLen
+			if postIndex < 0 {
+				return ErrInvalidLengthGenerated
+			}
+			if postIndex > l {
+				return io.ErrUnexpectedEOF
+			}
+			m.LastHandledRefresh = string(dAtA[iNdEx:postIndex])
+			iNdEx = postIndex
 		default:
 			iNdEx = preIndex
 			skippy, err := skipGenerated(dAtA[iNdEx:])
diff --git a/api/v1alpha1/generated.proto b/api/v1alpha1/generated.proto
index 1dcc458d..cf8e13a1 100644
--- a/api/v1alpha1/generated.proto
+++ b/api/v1alpha1/generated.proto
@@ -891,6 +891,12 @@ message PromotionSpec {
 // PromotionStatus describes the current state of the transition represented by
 // a Promotion.
 message PromotionStatus {
+  // LastHandledRefresh holds the value of the most recent AnnotationKeyRefresh
+  // annotation that was handled by the controller. This field can be used to
+  // determine whether the request to refresh the resource has been handled.
+  // +optional
+  optional string lastHandledRefresh = 4;
+
   // Phase describes where the Promotion currently is in its lifecycle.
   optional string phase = 1;
 
@@ -981,6 +987,12 @@ message StageSpec {
 // StageStatus describes a Stages's current and recent Freight, health, and
 // more.
 message StageStatus {
+  // LastHandledRefresh holds the value of the most recent AnnotationKeyRefresh
+  // annotation that was handled by the controller. This field can be used to
+  // determine whether the request to refresh the resource has been handled.
+  // +optional
+  optional string lastHandledRefresh = 11;
+
   // Phase describes where the Stage currently is in its lifecycle.
   optional string phase = 1;
 
@@ -1052,6 +1064,10 @@ message VerificationInfo {
   // ID is the identifier of the Verification process.
   optional string id = 4;
 
+  // Actor is the name of the entity that initiated or aborted the
+  // Verification process.
+  optional string actor = 7;
+
   // StartTime is the time at which the Verification process was started.
   optional k8s.io.apimachinery.pkg.apis.meta.v1.Time startTime = 5;
 
@@ -1119,6 +1135,12 @@ message WarehouseSpec {
 
 // WarehouseStatus describes a Warehouse's most recently observed state.
 message WarehouseStatus {
+  // LastHandledRefresh holds the value of the most recent AnnotationKeyRefresh
+  // annotation that was handled by the controller. This field can be used to
+  // determine whether the request to refresh the resource has been handled.
+  // +optional
+  optional string lastHandledRefresh = 6;
+
   // Message describes any errors that are preventing the Warehouse controller
   // from polling repositories to discover new Freight.
   optional string message = 3;
diff --git a/api/v1alpha1/promotion_types.go b/api/v1alpha1/promotion_types.go
index 438566cb..144848f4 100644
--- a/api/v1alpha1/promotion_types.go
+++ b/api/v1alpha1/promotion_types.go
@@ -86,6 +86,11 @@ type PromotionSpec struct {
 // PromotionStatus describes the current state of the transition represented by
 // a Promotion.
 type PromotionStatus struct {
+	// LastHandledRefresh holds the value of the most recent AnnotationKeyRefresh
+	// annotation that was handled by the controller. This field can be used to
+	// determine whether the request to refresh the resource has been handled.
+	// +optional
+	LastHandledRefresh string `json:"lastHandledRefresh,omitempty" protobuf:"bytes,4,opt,name=lastHandledRefresh"`
 	// Phase describes where the Promotion currently is in its lifecycle.
 	Phase PromotionPhase `json:"phase,omitempty" protobuf:"bytes,1,opt,name=phase"`
 	// Message is a display message about the promotion, including any errors
diff --git a/api/v1alpha1/stage_helpers.go b/api/v1alpha1/stage_helpers.go
index b1a6f764..8a2f4e3d 100644
--- a/api/v1alpha1/stage_helpers.go
+++ b/api/v1alpha1/stage_helpers.go
@@ -2,18 +2,78 @@ package v1alpha1
 
 import (
 	"context"
+	"encoding/json"
 	"errors"
 	"fmt"
 	"time"
 
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/apimachinery/pkg/types"
-	"k8s.io/utils/ptr"
 	"sigs.k8s.io/controller-runtime/pkg/client"
 
 	"github.com/akuity/kargo/internal/api/user"
 )
 
+// VerificationRequest is a request payload with an optional actor field which
+// can be used to annotate a Stage using the AnnotationKeyReverify or
+// AnnotationKeyAbort annotations.
+//
+// The ID field is used to specify the VerificationInfo to be re-verified or
+// aborted. If the ID is empty, the request is considered invalid. The Actor
+// field is optional and can be used to track the user who initiated the
+// re-verification or abort request. The ControlPlane field is optional and
+// indicates if the request was initiated by a control plane.
+//
+// +protobuf=false
+// +k8s:deepcopy-gen=false
+// +k8s:openapi-gen=false
+type VerificationRequest struct {
+	// ID is the identifier of the VerificationInfo for which the request is
+	// being made.
+	ID string `json:"id,omitempty"`
+	// Actor is the user who initiated the request.
+	Actor string `json:"actor,omitempty"`
+	// ControlPlane is a flag to indicate if the request has been initiated by
+	// a control plane.
+	ControlPlane bool `json:"controlPlane,omitempty"`
+}
+
+// Equals returns true if the VerificationRequest is equal to the other
+// VerificationRequest, false otherwise. Two VerificationRequests are equal if
+// their ID, Actor, and ControlPlane fields are equal.
+func (r *VerificationRequest) Equals(other *VerificationRequest) bool {
+	if r == nil && other == nil {
+		return true
+	}
+	if r == nil || other == nil {
+		return false
+	}
+	return r.ID == other.ID && r.Actor == other.Actor && r.ControlPlane == other.ControlPlane
+}
+
+// HasID returns true if the VerificationRequest has a non-empty ID.
+func (r *VerificationRequest) HasID() bool {
+	return r != nil && r.ID != ""
+}
+
+// ForID returns true if the VerificationRequest has the specified ID.
+func (r *VerificationRequest) ForID(id string) bool {
+	return r != nil && r.ID != "" && r.ID == id
+}
+
+// String returns the JSON string representation of the VerificationRequest,
+// or an empty string if the VerificationRequest is nil or has an empty ID.
+func (r *VerificationRequest) String() string {
+	if r == nil || r.ID == "" {
+		return ""
+	}
+	b, _ := json.Marshal(r)
+	if b == nil {
+		return ""
+	}
+	return string(b)
+}
+
 // GetStage returns a pointer to the Stage resource specified by the
 // namespacedName argument. If no such resource is found, nil is returned
 // instead.
@@ -87,14 +147,14 @@ func ReverifyStageFreight(
 		return fmt.Errorf("stage verification info has no ID")
 	}
 
-	kvs := map[string]*string{
-		AnnotationKeyReverify: ptr.To(curFreight.VerificationInfo.ID),
+	rr := VerificationRequest{
+		ID: curFreight.VerificationInfo.ID,
 	}
 	// Put actor information to track on the controller side
 	if u, ok := user.InfoFromContext(ctx); ok {
-		kvs[AnnotationKeyReverifyActor] = ptr.To(FormatEventUserActor(u))
+		rr.Actor = FormatEventUserActor(u)
 	}
-	return patchAnnotations(ctx, c, stage, kvs)
+	return patchAnnotation(ctx, c, stage, AnnotationKeyReverify, rr.String())
 }
 
 // AbortStageFreightVerification forces aborting the verification of the
@@ -131,5 +191,12 @@ func AbortStageFreightVerification(
 		return fmt.Errorf("stage verification info has no ID")
 	}
 
-	return patchAnnotation(ctx, c, stage, AnnotationKeyAbort, curFreight.VerificationInfo.ID)
+	ar := VerificationRequest{
+		ID: curFreight.VerificationInfo.ID,
+	}
+	// Put actor information to track on the controller side
+	if u, ok := user.InfoFromContext(ctx); ok {
+		ar.Actor = FormatEventUserActor(u)
+	}
+	return patchAnnotation(ctx, c, stage, AnnotationKeyAbort, ar.String())
 }
diff --git a/api/v1alpha1/stage_types.go b/api/v1alpha1/stage_types.go
index b62a85fe..20f519d5 100644
--- a/api/v1alpha1/stage_types.go
+++ b/api/v1alpha1/stage_types.go
@@ -504,6 +504,11 @@ type ArgoCDHelmImageUpdate struct {
 // StageStatus describes a Stages's current and recent Freight, health, and
 // more.
 type StageStatus struct {
+	// LastHandledRefresh holds the value of the most recent AnnotationKeyRefresh
+	// annotation that was handled by the controller. This field can be used to
+	// determine whether the request to refresh the resource has been handled.
+	// +optional
+	LastHandledRefresh string `json:"lastHandledRefresh,omitempty" protobuf:"bytes,11,opt,name=lastHandledRefresh"`
 	// Phase describes where the Stage currently is in its lifecycle.
 	Phase StagePhase `json:"phase,omitempty" protobuf:"bytes,1,opt,name=phase"`
 	// CurrentFreight is a simplified representation of the Stage's current
@@ -728,6 +733,9 @@ type AnalysisRunArgument struct {
 type VerificationInfo struct {
 	// ID is the identifier of the Verification process.
 	ID string `json:"id,omitempty" protobuf:"bytes,4,opt,name=id"`
+	// Actor is the name of the entity that initiated or aborted the
+	// Verification process.
+	Actor string `json:"actor,omitempty" protobuf:"bytes,7,opt,name=actor"`
 	// StartTime is the time at which the Verification process was started.
 	StartTime *metav1.Time `json:"startTime,omitempty" protobuf:"bytes,5,opt,name=startTime"`
 	// Phase describes the current phase of the Verification process. Generally,
@@ -753,6 +761,14 @@ func (v *VerificationInfo) HasAnalysisRun() bool {
 
 type VerificationInfoStack []VerificationInfo
 
+// Current returns the VerificationInfo at the top of the stack.
+func (v *VerificationInfoStack) Current() *VerificationInfo {
+	if len(*v) == 0 {
+		return nil
+	}
+	return &(*v)[0]
+}
+
 // UpdateOrPush updates the VerificationInfo with the same ID as the provided
 // VerificationInfo or appends the provided VerificationInfo to the stack if no
 // such VerificationInfo is found.
diff --git a/api/v1alpha1/warehouse_types.go b/api/v1alpha1/warehouse_types.go
index 2ff2990d..d43dbf67 100644
--- a/api/v1alpha1/warehouse_types.go
+++ b/api/v1alpha1/warehouse_types.go
@@ -247,6 +247,11 @@ type ChartSubscription struct {
 
 // WarehouseStatus describes a Warehouse's most recently observed state.
 type WarehouseStatus struct {
+	// LastHandledRefresh holds the value of the most recent AnnotationKeyRefresh
+	// annotation that was handled by the controller. This field can be used to
+	// determine whether the request to refresh the resource has been handled.
+	// +optional
+	LastHandledRefresh string `json:"lastHandledRefresh,omitempty" protobuf:"bytes,6,opt,name=lastHandledRefresh"`
 	// Message describes any errors that are preventing the Warehouse controller
 	// from polling repositories to discover new Freight.
 	Message string `json:"message,omitempty" protobuf:"bytes,3,opt,name=message"`
diff --git a/charts/kargo/resources/crds/kargo.akuity.io_promotions.yaml b/charts/kargo/resources/crds/kargo.akuity.io_promotions.yaml
index 0da701b5..1eb4dab9 100644
--- a/charts/kargo/resources/crds/kargo.akuity.io_promotions.yaml
+++ b/charts/kargo/resources/crds/kargo.akuity.io_promotions.yaml
@@ -85,6 +85,12 @@ spec:
               Status describes the current state of the transition represented by this
               Promotion.
             properties:
+              lastHandledRefresh:
+                description: |-
+                  LastHandledRefresh holds the value of the most recent AnnotationKeyRefresh
+                  annotation that was handled by the controller. This field can be used to
+                  determine whether the request to refresh the resource has been handled.
+                type: string
               message:
                 description: |-
                   Message is a display message about the promotion, including any errors
diff --git a/charts/kargo/resources/crds/kargo.akuity.io_stages.yaml b/charts/kargo/resources/crds/kargo.akuity.io_stages.yaml
index 60fafcff..2c3787d5 100644
--- a/charts/kargo/resources/crds/kargo.akuity.io_stages.yaml
+++ b/charts/kargo/resources/crds/kargo.akuity.io_stages.yaml
@@ -674,6 +674,11 @@ spec:
                         VerificationInfo contains information about the currently running
                         Verification process.
                       properties:
+                        actor:
+                          description: |-
+                            Actor is the name of the entity that initiated or aborted the
+                            Verification process.
+                          type: string
                         analysisRun:
                           description: |-
                             AnalysisRun is a reference to the Argo Rollouts AnalysisRun that implements
@@ -726,6 +731,11 @@ spec:
                       VerificationInfo is information about any verification process that was
                       associated with this Freight for this Stage.
                     properties:
+                      actor:
+                        description: |-
+                          Actor is the name of the entity that initiated or aborted the
+                          Verification process.
+                        type: string
                       analysisRun:
                         description: |-
                           AnalysisRun is a reference to the Argo Rollouts AnalysisRun that implements
@@ -894,6 +904,11 @@ spec:
                             VerificationInfo contains information about the currently running
                             Verification process.
                           properties:
+                            actor:
+                              description: |-
+                                Actor is the name of the entity that initiated or aborted the
+                                Verification process.
+                              type: string
                             analysisRun:
                               description: |-
                                 AnalysisRun is a reference to the Argo Rollouts AnalysisRun that implements
@@ -947,6 +962,11 @@ spec:
                           VerificationInfo is information about any verification process that was
                           associated with this Freight for this Stage.
                         properties:
+                          actor:
+                            description: |-
+                              Actor is the name of the entity that initiated or aborted the
+                              Verification process.
+                            type: string
                           analysisRun:
                             description: |-
                               AnalysisRun is a reference to the Argo Rollouts AnalysisRun that implements
@@ -1005,6 +1025,12 @@ spec:
                   status:
                     description: Status is the (optional) status of the promotion
                     properties:
+                      lastHandledRefresh:
+                        description: |-
+                          LastHandledRefresh holds the value of the most recent AnnotationKeyRefresh
+                          annotation that was handled by the controller. This field can be used to
+                          determine whether the request to refresh the resource has been handled.
+                        type: string
                       message:
                         description: |-
                           Message is a display message about the promotion, including any errors
@@ -1204,6 +1230,11 @@ spec:
                           VerificationInfo contains information about the currently running
                           Verification process.
                         properties:
+                          actor:
+                            description: |-
+                              Actor is the name of the entity that initiated or aborted the
+                              Verification process.
+                            type: string
                           analysisRun:
                             description: |-
                               AnalysisRun is a reference to the Argo Rollouts AnalysisRun that implements
@@ -1257,6 +1288,11 @@ spec:
                         VerificationInfo is information about any verification process that was
                         associated with this Freight for this Stage.
                       properties:
+                        actor:
+                          description: |-
+                            Actor is the name of the entity that initiated or aborted the
+                            Verification process.
+                          type: string
                         analysisRun:
                           description: |-
                             AnalysisRun is a reference to the Argo Rollouts AnalysisRun that implements
@@ -1309,6 +1345,12 @@ spec:
                       type: string
                   type: object
                 type: array
+              lastHandledRefresh:
+                description: |-
+                  LastHandledRefresh holds the value of the most recent AnnotationKeyRefresh
+                  annotation that was handled by the controller. This field can be used to
+                  determine whether the request to refresh the resource has been handled.
+                type: string
               lastPromotion:
                 description: LastPromotion is a reference to the last completed promotion.
                 properties:
@@ -1425,6 +1467,11 @@ spec:
                             VerificationInfo contains information about the currently running
                             Verification process.
                           properties:
+                            actor:
+                              description: |-
+                                Actor is the name of the entity that initiated or aborted the
+                                Verification process.
+                              type: string
                             analysisRun:
                               description: |-
                                 AnalysisRun is a reference to the Argo Rollouts AnalysisRun that implements
@@ -1478,6 +1525,11 @@ spec:
                           VerificationInfo is information about any verification process that was
                           associated with this Freight for this Stage.
                         properties:
+                          actor:
+                            description: |-
+                              Actor is the name of the entity that initiated or aborted the
+                              Verification process.
+                            type: string
                           analysisRun:
                             description: |-
                               AnalysisRun is a reference to the Argo Rollouts AnalysisRun that implements
@@ -1536,6 +1588,12 @@ spec:
                   status:
                     description: Status is the (optional) status of the promotion
                     properties:
+                      lastHandledRefresh:
+                        description: |-
+                          LastHandledRefresh holds the value of the most recent AnnotationKeyRefresh
+                          annotation that was handled by the controller. This field can be used to
+                          determine whether the request to refresh the resource has been handled.
+                        type: string
                       message:
                         description: |-
                           Message is a display message about the promotion, including any errors
diff --git a/charts/kargo/resources/crds/kargo.akuity.io_warehouses.yaml b/charts/kargo/resources/crds/kargo.akuity.io_warehouses.yaml
index e539a718..70aff895 100644
--- a/charts/kargo/resources/crds/kargo.akuity.io_warehouses.yaml
+++ b/charts/kargo/resources/crds/kargo.akuity.io_warehouses.yaml
@@ -400,6 +400,11 @@ spec:
                         VerificationInfo contains information about the currently running
                         Verification process.
                       properties:
+                        actor:
+                          description: |-
+                            Actor is the name of the entity that initiated or aborted the
+                            Verification process.
+                          type: string
                         analysisRun:
                           description: |-
                             AnalysisRun is a reference to the Argo Rollouts AnalysisRun that implements
@@ -452,6 +457,11 @@ spec:
                       VerificationInfo is information about any verification process that was
                       associated with this Freight for this Stage.
                     properties:
+                      actor:
+                        description: |-
+                          Actor is the name of the entity that initiated or aborted the
+                          Verification process.
+                        type: string
                       analysisRun:
                         description: |-
                           AnalysisRun is a reference to the Argo Rollouts AnalysisRun that implements
@@ -503,6 +513,12 @@ spec:
                       this Freight.
                     type: string
                 type: object
+              lastHandledRefresh:
+                description: |-
+                  LastHandledRefresh holds the value of the most recent AnnotationKeyRefresh
+                  annotation that was handled by the controller. This field can be used to
+                  determine whether the request to refresh the resource has been handled.
+                type: string
               message:
                 description: |-
                   Message describes any errors that are preventing the Warehouse controller
diff --git a/internal/api/config/config.go b/internal/api/config/config.go
index 220d16f9..9b7a20ed 100644
--- a/internal/api/config/config.go
+++ b/internal/api/config/config.go
@@ -15,7 +15,6 @@ import (
 
 type StandardConfig struct {
 	GracefulShutdownTimeout time.Duration `envconfig:"GRACEFUL_SHUTDOWN_TIMEOUT" default:"30s"`
-	UIDirectory             string        `envconfig:"UI_DIR" default:"./ui/build"`
 }
 
 type ServerConfig struct {
diff --git a/internal/api/server.go b/internal/api/server.go
index 4f246142..20179061 100644
--- a/internal/api/server.go
+++ b/internal/api/server.go
@@ -2,11 +2,12 @@ package api
 
 import (
 	"context"
+	"embed"
 	"errors"
 	"fmt"
+	"io/fs"
 	"net"
 	"net/http"
-	goos "os"
 	"time"
 
 	"connectrpc.com/grpchealth"
@@ -26,13 +27,15 @@ import (
 	"github.com/akuity/kargo/internal/api/option"
 	"github.com/akuity/kargo/internal/api/validation"
 	rollouts "github.com/akuity/kargo/internal/controller/rollouts/api/v1alpha1"
-	httputil "github.com/akuity/kargo/internal/http"
 	"github.com/akuity/kargo/internal/logging"
 	"github.com/akuity/kargo/pkg/api/service/v1alpha1/svcv1alpha1connect"
 )
 
 var (
 	_ svcv1alpha1connect.KargoServiceHandler = &server{}
+
+	//go:embed all:ui
+	ui embed.FS
 )
 
 type server struct {
@@ -192,7 +195,11 @@ func (s *server) Serve(ctx context.Context, l net.Listener) error {
 	mux.Handle(grpchealth.NewHandler(NewHealthChecker(), opts))
 	path, svcHandler := svcv1alpha1connect.NewKargoServiceHandler(s, opts)
 	mux.Handle(path, svcHandler)
-	mux.Handle("/", s.newDashboardRequestHandler())
+	uiFS := fs.FS(ui)
+	if uiFS, err = fs.Sub(uiFS, "ui"); err != nil {
+		return fmt.Errorf("error initializing UI file system: %w", err)
+	}
+	mux.Handle("/", http.FileServer(http.FS(uiFS)))
 	if s.cfg.DexProxyConfig != nil {
 		dexProxyCfg := dex.ProxyConfigFromEnv()
 		dexProxy, err := dex.NewProxy(dexProxyCfg)
@@ -248,19 +255,3 @@ func (s *server) Serve(ctx context.Context, l net.Listener) error {
 		return err
 	}
 }
-
-func (s *server) newDashboardRequestHandler() http.HandlerFunc {
-	fs := http.FileServer(http.Dir(s.cfg.UIDirectory))
-	return func(w http.ResponseWriter, req *http.Request) {
-		path := s.cfg.UIDirectory + req.URL.Path
-		info, err := goos.Stat(path)
-		if goos.IsNotExist(err) || info.IsDir() {
-			if w != nil {
-				httputil.SetNoCacheHeaders(w)
-				http.ServeFile(w, req, s.cfg.UIDirectory+"/index.html")
-			}
-		} else {
-			fs.ServeHTTP(w, req)
-		}
-	}
-}
diff --git a/internal/api/ui/.keep b/internal/api/ui/.keep
new file mode 100644
index 00000000..e69de29b
diff --git a/internal/cli/cmd/refresh/stage.go b/internal/cli/cmd/refresh/stage.go
index 879897d6..eb2ebfbf 100644
--- a/internal/cli/cmd/refresh/stage.go
+++ b/internal/cli/cmd/refresh/stage.go
@@ -81,8 +81,14 @@ func waitForStage(
 		if msg == nil || msg.Stage == nil {
 			return errors.New("unexpected response")
 		}
-		if msg.Stage.GetAnnotations() == nil ||
-			msg.Stage.GetAnnotations()[kargoapi.AnnotationKeyRefresh] == "" {
+		token, ok := kargoapi.RefreshAnnotationValue(msg.Stage.GetAnnotations())
+		if !ok {
+			return fmt.Errorf(
+				"Stage %q in Project %q has no %q annotation",
+				name, project, kargoapi.AnnotationKeyRefresh,
+			)
+		}
+		if msg.Stage.Status.LastHandledRefresh == token {
 			return nil
 		}
 	}
diff --git a/internal/cli/cmd/refresh/warehouse.go b/internal/cli/cmd/refresh/warehouse.go
index 48294019..d6202e3e 100644
--- a/internal/cli/cmd/refresh/warehouse.go
+++ b/internal/cli/cmd/refresh/warehouse.go
@@ -81,8 +81,14 @@ func waitForWarehouse(
 		if msg == nil || msg.Warehouse == nil {
 			return errors.New("unexpected response")
 		}
-		if msg.Warehouse.GetAnnotations() == nil ||
-			msg.Warehouse.GetAnnotations()[kargoapi.AnnotationKeyRefresh] == "" {
+		token, ok := kargoapi.RefreshAnnotationValue(msg.Warehouse.GetAnnotations())
+		if !ok {
+			return fmt.Errorf(
+				"Warehouse %q in Project %q has no %q annotation",
+				name, project, kargoapi.AnnotationKeyRefresh,
+			)
+		}
+		if msg.Warehouse.Status.LastHandledRefresh == token {
 			return nil
 		}
 	}
diff --git a/internal/controller/promotions/promotions.go b/internal/controller/promotions/promotions.go
index b646750d..9ec9eff8 100644
--- a/internal/controller/promotions/promotions.go
+++ b/internal/controller/promotions/promotions.go
@@ -99,20 +99,13 @@ func SetupReconcilerWithManager(
 		cfg,
 	)
 
-	changePredicate := predicate.Or(
-		predicate.GenerationChangedPredicate{},
-		predicate.AnnotationChangedPredicate{},
-	)
-
 	c, err := ctrl.NewControllerManagedBy(kargoMgr).
 		For(&kargoapi.Promotion{}).
-		WithEventFilter(changePredicate).
+		WithEventFilter(predicate.Or(
+			predicate.GenerationChangedPredicate{},
+			kargo.RefreshRequested{},
+		)).
 		WithEventFilter(shardPredicate).
-		WithEventFilter(kargo.IgnoreAnnotationRemoval{
-			Annotations: []string{
-				kargoapi.AnnotationKeyRefresh,
-			},
-		}).
 		WithOptions(controller.CommonOptions()).
 		Build(reconciler)
 	if err != nil {
@@ -292,6 +285,11 @@ func (r *reconciler) Reconcile(
 		logger.Infof("promotion %s", newStatus.Phase)
 	}
 
+	// Record the current refresh token as having been handled.
+	if token, ok := kargoapi.RefreshAnnotationValue(promo.GetAnnotations()); ok {
+		newStatus.LastHandledRefresh = token
+	}
+
 	err = kubeclient.PatchStatus(ctx, r.kargoClient, promo, func(status *kargoapi.PromotionStatus) {
 		*status = *newStatus
 	})
@@ -346,15 +344,6 @@ func (r *reconciler) Reconcile(
 		r.recorder.AnnotatedEventf(promo, eventAnnotations, corev1.EventTypeNormal, reason, msg)
 	}
 
-	if clearRefreshErr := kargoapi.ClearAnnotations(
-		ctx,
-		r.kargoClient,
-		promo,
-		kargoapi.AnnotationKeyRefresh,
-	); clearRefreshErr != nil {
-		logger.Errorf("error clearing Promotion refresh annotation: %s", clearRefreshErr)
-	}
-
 	if err != nil {
 		// Controller runtime automatically gives us a progressive backoff if err is
 		// not nil
diff --git a/internal/controller/stages/stages.go b/internal/controller/stages/stages.go
index 6ada3b0d..2e7f4e06 100644
--- a/internal/controller/stages/stages.go
+++ b/internal/controller/stages/stages.go
@@ -2,7 +2,6 @@ package stages
 
 import (
 	"context"
-	"errors"
 	"fmt"
 	"sort"
 	"time"
@@ -316,18 +315,12 @@ func SetupReconcilerWithManager(
 		WithEventFilter(
 			predicate.Or(
 				predicate.GenerationChangedPredicate{},
-				predicate.AnnotationChangedPredicate{},
+				kargo.RefreshRequested{},
+				kargo.ReverifyRequested{},
+				kargo.AbortRequested{},
 			),
 		).
 		WithEventFilter(shardPredicate).
-		WithEventFilter(kargo.IgnoreAnnotationRemoval{
-			Annotations: []string{
-				kargoapi.AnnotationKeyRefresh,
-				kargoapi.AnnotationKeyReverify,
-				kargoapi.AnnotationKeyReverifyActor,
-				kargoapi.AnnotationKeyAbort,
-			},
-		}).
 		WithOptions(controller.CommonOptions()).
 		Build(
 			newReconciler(
@@ -553,30 +546,23 @@ func (r *reconciler) Reconcile(
 		newStatus.Message = ""
 	}
 
+	// Record the current refresh token as having been handled.
+	if token, ok := kargoapi.RefreshAnnotationValue(stage.GetAnnotations()); ok {
+		newStatus.LastHandledRefresh = token
+	}
+
 	updateErr := kubeclient.PatchStatus(ctx, r.kargoClient, stage, func(status *kargoapi.StageStatus) {
 		*status = newStatus
 	})
 	if updateErr != nil {
 		logger.Errorf("error updating Stage status: %s", updateErr)
 	}
-	clearErr := kargoapi.ClearAnnotations(
-		ctx,
-		r.kargoClient,
-		stage,
-		kargoapi.AnnotationKeyRefresh,
-		kargoapi.AnnotationKeyReverify,
-		kargoapi.AnnotationKeyReverifyActor,
-		kargoapi.AnnotationKeyAbort,
-	)
-	if clearErr != nil {
-		logger.Errorf("error clearing Stage annotations: %s", clearErr)
-	}
 
 	// If we had no error, but couldn't update, then we DO have an error. But we
 	// do it this way so that a failure to update is never counted as THE failure
 	// when something else more serious occurred first.
 	if err == nil {
-		err = errors.Join(updateErr, clearErr)
+		err = updateErr
 	}
 	logger.Debug("done reconciling Stage")
 
@@ -773,8 +759,8 @@ func (r *reconciler) syncNormalStage(
 			// Confirm if a reverification is requested. If so, clear the
 			// verification info to start the verification process again.
 			info := status.CurrentFreight.VerificationInfo
-			if info != nil && info.ID != "" && info.Phase.IsTerminal() {
-				if v, ok := stage.GetAnnotations()[kargoapi.AnnotationKeyReverify]; ok && v == info.ID {
+			if info != nil && info.Phase.IsTerminal() {
+				if req, _ := kargoapi.ReverifyAnnotationValue(stage.GetAnnotations()); req.ForID(info.ID) {
 					logger.Debug("rerunning verification")
 					status.Phase = kargoapi.StagePhaseVerifying
 					status.CurrentFreight.VerificationInfo = nil
@@ -816,8 +802,8 @@ func (r *reconciler) syncNormalStage(
 					// Abort the verification if it's still running and the Stage has
 					// been marked to do so.
 					newInfo := status.CurrentFreight.VerificationInfo
-					if newInfo.ID != "" && !newInfo.Phase.IsTerminal() {
-						if v, ok := stage.GetAnnotations()[kargoapi.AnnotationKeyAbort]; ok && v == newInfo.ID {
+					if !newInfo.Phase.IsTerminal() {
+						if req, _ := kargoapi.AbortAnnotationValue(stage.GetAnnotations()); req.ForID(newInfo.ID) {
 							log.Debug("aborting verification")
 							status.CurrentFreight.VerificationInfo = r.abortVerificationFn(ctx, stage)
 						}
@@ -1556,19 +1542,9 @@ func (r *reconciler) recordFreightVerificationEvent(
 	}
 
 	// If the verification is manually triggered (e.g. reverify),
-	// override the actor if the Stage or AnalysisRun knows about them.
-	{
-		// Check if the stage has reverify actor annotation
-		if actor, ok := s.Annotations[kargoapi.AnnotationKeyReverifyActor]; ok {
-			annotations[kargoapi.AnnotationKeyEventActor] = actor
-		}
-
-		// Or if the analysis run has it, then set it as the actor
-		if ar != nil {
-			if actor, ok := ar.Annotations[kargoapi.AnnotationKeyReverifyActor]; ok {
-				annotations[kargoapi.AnnotationKeyEventActor] = actor
-			}
-		}
+	// override the actor with the one who triggered the verification.
+	if vi.Actor != "" {
+		annotations[kargoapi.AnnotationKeyEventActor] = vi.Actor
 	}
 
 	reason := kargoapi.EventReasonFreightVerificationUnknown
diff --git a/internal/controller/stages/verification.go b/internal/controller/stages/verification.go
index 568ab230..9a08365f 100644
--- a/internal/controller/stages/verification.go
+++ b/internal/controller/stages/verification.go
@@ -34,23 +34,36 @@ func (r *reconciler) startVerification(
 	stage *kargoapi.Stage,
 ) (*kargoapi.VerificationInfo, error) {
 	startTime := r.nowFn()
+
+	newInfo := &kargoapi.VerificationInfo{
+		ID:        uuid.NewString(),
+		StartTime: ptr.To(metav1.NewTime(startTime)),
+	}
+
+	// Gather information about the verification being a re-verification request,
+	// and extract the actor who requested the re-verification to be used in the
+	// new verification info.
+	var isReverify bool
+	if curVer := stage.Status.CurrentFreight.VerificationHistory.Current(); curVer != nil {
+		if req, _ := kargoapi.ReverifyAnnotationValue(stage.GetAnnotations()); req.ForID(curVer.ID) {
+			isReverify = true
+			newInfo.Actor = req.Actor
+		}
+	}
+
 	if !r.cfg.RolloutsIntegrationEnabled {
-		return &kargoapi.VerificationInfo{
-			ID:         uuid.NewString(),
-			StartTime:  ptr.To(metav1.NewTime(startTime)),
-			FinishTime: ptr.To(metav1.NewTime(r.nowFn())),
-			Phase:      kargoapi.VerificationPhaseError,
-			Message: "Rollouts integration is disabled on this controller; " +
-				"cannot start verification",
-		}, nil
+		newInfo.FinishTime = ptr.To(metav1.NewTime(r.nowFn()))
+		newInfo.Phase = kargoapi.VerificationPhaseError
+		newInfo.Message = "Rollouts integration is disabled on this controller; cannot start verification"
+		return newInfo, nil
 	}
 
 	logger := logging.LoggerFromContext(ctx)
 
-	// If the stage does not have a reverification annotation, check if there is
-	// an existing AnalysisRun for the Stage and Freight. If there is, return
-	// the status of this AnalysisRun.
-	if _, ok := stage.GetAnnotations()[kargoapi.AnnotationKeyReverify]; !ok {
+	// If this is not a re-verification request, check if there is an existing
+	// AnalysisRun for the Stage and Freight. If there is, return the status of
+	// this AnalysisRun.
+	if !isReverify {
 		analysisRuns := rollouts.AnalysisRunList{}
 		if err := r.listAnalysisRunsFn(
 			ctx,
@@ -65,19 +78,16 @@ func (r *reconciler) startVerification(
 				),
 			},
 		); err != nil {
-			return &kargoapi.VerificationInfo{
-				ID:         uuid.NewString(),
-				StartTime:  ptr.To(metav1.NewTime(startTime)),
-				FinishTime: ptr.To(metav1.NewTime(r.nowFn())),
-				Phase:      kargoapi.VerificationPhaseError,
-				Message: fmt.Errorf(
-					"error listing AnalysisRuns for Stage %q and Freight %q in namespace %q: %w",
-					stage.Name,
-					stage.Status.CurrentFreight.Name,
-					stage.Namespace,
-					err,
-				).Error(),
-			}, err
+			newInfo.FinishTime = ptr.To(metav1.NewTime(r.nowFn()))
+			newInfo.Phase = kargoapi.VerificationPhaseError
+			newInfo.Message = fmt.Errorf(
+				"error listing AnalysisRuns for Stage %q and Freight %q in namespace %q: %w",
+				stage.Name,
+				stage.Status.CurrentFreight.Name,
+				stage.Namespace,
+				err,
+			).Error()
+			return newInfo, err
 		}
 		if len(analysisRuns.Items) > 0 {
 			// Sort the AnalysisRuns by creation timestamp, so that the most recent
@@ -88,17 +98,15 @@ func (r *reconciler) startVerification(
 
 			logger.Debug("AnalysisRun already exists for Freight")
 			latestAnalysisRun := analysisRuns.Items[0]
-			return &kargoapi.VerificationInfo{
-				ID:         uuid.NewString(),
-				StartTime:  ptr.To(latestAnalysisRun.CreationTimestamp),
-				FinishTime: latestAnalysisRun.Status.CompletedAt(),
-				Phase:      kargoapi.VerificationPhase(latestAnalysisRun.Status.Phase),
-				AnalysisRun: &kargoapi.AnalysisRunReference{
-					Name:      latestAnalysisRun.Name,
-					Namespace: latestAnalysisRun.Namespace,
-					Phase:     string(latestAnalysisRun.Status.Phase),
-				},
-			}, nil
+
+			newInfo.FinishTime = latestAnalysisRun.Status.CompletedAt()
+			newInfo.Phase = kargoapi.VerificationPhase(latestAnalysisRun.Status.Phase)
+			newInfo.AnalysisRun = &kargoapi.AnalysisRunReference{
+				Name:      latestAnalysisRun.Name,
+				Namespace: latestAnalysisRun.Namespace,
+				Phase:     string(latestAnalysisRun.Status.Phase),
+			}
+			return newInfo, nil
 		}
 	}
 
@@ -115,31 +123,25 @@ func (r *reconciler) startVerification(
 			},
 		)
 		if err != nil {
-			return &kargoapi.VerificationInfo{
-				ID:         uuid.NewString(),
-				StartTime:  ptr.To(metav1.NewTime(startTime)),
-				FinishTime: ptr.To(metav1.NewTime(r.nowFn())),
-				Phase:      kargoapi.VerificationPhaseError,
-				Message: fmt.Errorf(
-					"error getting AnalysisTemplate %q in namespace %q: %w",
-					templateRef.Name,
-					stage.Namespace,
-					err,
-				).Error(),
-			}, err
+			newInfo.FinishTime = ptr.To(metav1.NewTime(r.nowFn()))
+			newInfo.Phase = kargoapi.VerificationPhaseError
+			newInfo.Message = fmt.Errorf(
+				"error getting AnalysisTemplate %q in namespace %q: %w",
+				templateRef.Name,
+				stage.Namespace,
+				err,
+			).Error()
+			return newInfo, err
 		}
 		if template == nil {
-			return &kargoapi.VerificationInfo{
-				ID:         uuid.NewString(),
-				StartTime:  ptr.To(metav1.NewTime(startTime)),
-				FinishTime: ptr.To(metav1.NewTime(r.nowFn())),
-				Phase:      kargoapi.VerificationPhaseError,
-				Message: fmt.Errorf(
-					"AnalysisTemplate %q in namespace %q not found",
-					templateRef.Name,
-					stage.Namespace,
-				).Error(),
-			}, nil
+			newInfo.FinishTime = ptr.To(metav1.NewTime(r.nowFn()))
+			newInfo.Phase = kargoapi.VerificationPhaseError
+			newInfo.Message = fmt.Errorf(
+				"AnalysisTemplate %q in namespace %q not found",
+				templateRef.Name,
+				stage.Namespace,
+			).Error()
+			return newInfo, nil
 		}
 		templates[i] = template
 	}
@@ -153,74 +155,60 @@ func (r *reconciler) startVerification(
 		},
 	)
 	if err != nil {
-		return &kargoapi.VerificationInfo{
-			ID:         uuid.NewString(),
-			StartTime:  ptr.To(metav1.NewTime(startTime)),
-			FinishTime: ptr.To(metav1.NewTime(r.nowFn())),
-			Phase:      kargoapi.VerificationPhaseError,
-			Message: fmt.Errorf(
-				"error getting Freight %q in namespace %q: %w",
-				stage.Status.CurrentFreight.Name,
-				stage.Namespace,
-				err,
-			).Error(),
-		}, err
+		newInfo.FinishTime = ptr.To(metav1.NewTime(r.nowFn()))
+		newInfo.Phase = kargoapi.VerificationPhaseError
+		newInfo.Message = fmt.Errorf(
+			"error getting Freight %q in namespace %q: %w",
+			stage.Status.CurrentFreight.Name,
+			stage.Namespace,
+			err,
+		).Error()
+		return newInfo, err
 	}
 	if freight == nil {
-		return &kargoapi.VerificationInfo{
-			ID:         uuid.NewString(),
-			StartTime:  ptr.To(metav1.NewTime(startTime)),
-			FinishTime: ptr.To(metav1.NewTime(r.nowFn())),
-			Phase:      kargoapi.VerificationPhaseError,
-			Message: fmt.Errorf(
-				"Freight %q in namespace %q not found",
-				stage.Status.CurrentFreight.Name,
-				stage.Namespace,
-			).Error(),
-		}, nil
+		newInfo.FinishTime = ptr.To(metav1.NewTime(r.nowFn()))
+		newInfo.Phase = kargoapi.VerificationPhaseError
+		newInfo.Message = fmt.Errorf(
+			"Freight %q in namespace %q not found",
+			stage.Status.CurrentFreight.Name,
+			stage.Namespace,
+		).Error()
+		return newInfo, nil
 	}
 
 	run, err := r.buildAnalysisRunFn(stage, freight, templates)
 	if err != nil {
-		return &kargoapi.VerificationInfo{
-			ID:         uuid.NewString(),
-			StartTime:  ptr.To(metav1.NewTime(startTime)),
-			FinishTime: ptr.To(metav1.NewTime(r.nowFn())),
-			Phase:      kargoapi.VerificationPhaseError,
-			Message: fmt.Errorf(
-				"error building AnalysisRun for Stage %q and Freight %q in namespace %q: %w",
-				stage.Name,
-				stage.Status.CurrentFreight.Name,
-				stage.Namespace,
-				err,
-			).Error(),
-		}, nil
+		newInfo.FinishTime = ptr.To(metav1.NewTime(r.nowFn()))
+		newInfo.Phase = kargoapi.VerificationPhaseError
+		newInfo.Message = fmt.Errorf(
+			"error building AnalysisRun for Stage %q and Freight %q in namespace %q: %w",
+			stage.Name,
+			stage.Status.CurrentFreight.Name,
+			stage.Namespace,
+			err,
+		).Error()
+		return newInfo, nil
 	}
 
 	if err := r.createAnalysisRunFn(ctx, run); err != nil {
-		return &kargoapi.VerificationInfo{
-			ID:         uuid.NewString(),
-			StartTime:  ptr.To(metav1.NewTime(startTime)),
-			FinishTime: ptr.To(metav1.NewTime(r.nowFn())),
-			Phase:      kargoapi.VerificationPhaseError,
-			Message: fmt.Errorf(
-				"error creating AnalysisRun %q in namespace %q: %w",
-				run.Name,
-				run.Namespace,
-				err,
-			).Error(),
-		}, kubeclient.IgnoreInvalid(err) // Ignore errors which are due to validation issues
-	}
-
-	return &kargoapi.VerificationInfo{
-		ID:        uuid.NewString(),
-		StartTime: ptr.To(run.CreationTimestamp),
-		Phase:     kargoapi.VerificationPhasePending,
-		AnalysisRun: &kargoapi.AnalysisRunReference{
-			Name:      run.Name,
-			Namespace: run.Namespace,
-		},
-	}, nil
+		newInfo.FinishTime = ptr.To(metav1.NewTime(r.nowFn()))
+		newInfo.Phase = kargoapi.VerificationPhaseError
+		newInfo.Message = fmt.Errorf(
+			"error creating AnalysisRun %q in namespace %q: %w",
+			run.Name,
+			run.Namespace,
+			err,
+		).Error()
+		return newInfo, kubeclient.IgnoreInvalid(err) // Ignore errors which are due to validation issues
+	}
+
+	newInfo.StartTime = ptr.To(run.CreationTimestamp)
+	newInfo.Phase = kargoapi.VerificationPhasePending
+	newInfo.AnalysisRun = &kargoapi.AnalysisRunReference{
+		Name:      run.Name,
+		Namespace: run.Namespace,
+	}
+	return newInfo, nil
 }
 
 // getVerificationInfo returns the status of the AnalysisRun for the given Stage.
@@ -239,6 +227,7 @@ func (r *reconciler) getVerificationInfo(
 	if !r.cfg.RolloutsIntegrationEnabled {
 		return &kargoapi.VerificationInfo{
 			ID:         stage.Status.CurrentFreight.VerificationInfo.ID,
+			Actor:      stage.Status.CurrentFreight.VerificationInfo.Actor,
 			StartTime:  stage.Status.CurrentFreight.VerificationInfo.StartTime,
 			FinishTime: stage.Status.CurrentFreight.VerificationInfo.FinishTime,
 			Phase:      kargoapi.VerificationPhaseError,
@@ -259,6 +248,7 @@ func (r *reconciler) getVerificationInfo(
 	if err != nil {
 		return &kargoapi.VerificationInfo{
 			ID:         stage.Status.CurrentFreight.VerificationInfo.ID,
+			Actor:      stage.Status.CurrentFreight.VerificationInfo.Actor,
 			StartTime:  stage.Status.CurrentFreight.VerificationInfo.StartTime,
 			FinishTime: stage.Status.CurrentFreight.VerificationInfo.FinishTime,
 			Phase:      kargoapi.VerificationPhaseError,
@@ -274,6 +264,7 @@ func (r *reconciler) getVerificationInfo(
 	if analysisRun == nil {
 		return &kargoapi.VerificationInfo{
 			ID:         stage.Status.CurrentFreight.VerificationInfo.ID,
+			Actor:      stage.Status.CurrentFreight.VerificationInfo.Actor,
 			StartTime:  stage.Status.CurrentFreight.VerificationInfo.StartTime,
 			FinishTime: stage.Status.CurrentFreight.VerificationInfo.FinishTime,
 			Phase:      kargoapi.VerificationPhaseError,
@@ -287,6 +278,7 @@ func (r *reconciler) getVerificationInfo(
 
 	return &kargoapi.VerificationInfo{
 		ID:         stage.Status.CurrentFreight.VerificationInfo.ID,
+		Actor:      stage.Status.CurrentFreight.VerificationInfo.Actor,
 		StartTime:  ptr.To(analysisRun.CreationTimestamp),
 		FinishTime: analysisRun.Status.CompletedAt(),
 		Phase:      kargoapi.VerificationPhase(analysisRun.Status.Phase),
@@ -302,15 +294,23 @@ func (r *reconciler) abortVerification(
 	ctx context.Context,
 	stage *kargoapi.Stage,
 ) *kargoapi.VerificationInfo {
+	currentInfo := stage.Status.CurrentFreight.VerificationInfo
+	newInfo := &kargoapi.VerificationInfo{
+		ID:        currentInfo.ID,
+		StartTime: currentInfo.StartTime,
+	}
+
+	// Extract the actor who requested the abort to be used in the new
+	// verification info.
+	if req, _ := kargoapi.AbortAnnotationValue(stage.GetAnnotations()); req.ForID(currentInfo.ID) {
+		newInfo.Actor = req.Actor
+	}
+
 	if !r.cfg.RolloutsIntegrationEnabled {
-		return &kargoapi.VerificationInfo{
-			ID:         stage.Status.CurrentFreight.VerificationInfo.ID,
-			StartTime:  stage.Status.CurrentFreight.VerificationInfo.StartTime,
-			FinishTime: ptr.To(metav1.NewTime(r.nowFn())),
-			Phase:      kargoapi.VerificationPhaseError,
-			Message: "Rollouts integration is disabled on this controller; cannot " +
-				"abort verification",
-		}
+		newInfo.FinishTime = ptr.To(metav1.NewTime(r.nowFn()))
+		newInfo.Phase = kargoapi.VerificationPhaseError
+		newInfo.Message = "Rollouts integration is disabled on this controller; cannot abort verification"
+		return newInfo
 	}
 
 	ar := &rollouts.AnalysisRun{
@@ -324,19 +324,16 @@ func (r *reconciler) abortVerification(
 		ar,
 		client.RawPatch(types.MergePatchType, []byte(`{"spec":{"terminate":true}}`)),
 	); err != nil {
-		return &kargoapi.VerificationInfo{
-			ID:         stage.Status.CurrentFreight.VerificationInfo.ID,
-			StartTime:  stage.Status.CurrentFreight.VerificationInfo.StartTime,
-			FinishTime: ptr.To(metav1.NewTime(r.nowFn())),
-			Phase:      kargoapi.VerificationPhaseError,
-			Message: fmt.Errorf(
-				"error terminating AnalysisRun %q in namespace %q: %w",
-				ar.Name,
-				ar.Namespace,
-				err,
-			).Error(),
-			AnalysisRun: stage.Status.CurrentFreight.VerificationInfo.AnalysisRun.DeepCopy(),
-		}
+		newInfo.FinishTime = ptr.To(metav1.NewTime(r.nowFn()))
+		newInfo.Phase = kargoapi.VerificationPhaseError
+		newInfo.Message = fmt.Errorf(
+			"error terminating AnalysisRun %q in namespace %q: %w",
+			ar.Name,
+			ar.Namespace,
+			err,
+		).Error()
+		newInfo.AnalysisRun = currentInfo.AnalysisRun.DeepCopy()
+		return newInfo
 	}
 
 	// Return a new VerificationInfo with the same ID and a message indicating
@@ -344,14 +341,12 @@ func (r *reconciler) abortVerification(
 	// the verification was not successful.
 	// We do not use the further information from the AnalysisRun, as this
 	// will indicate a "Succeeded" phase due to Argo Rollouts behavior.
-	return &kargoapi.VerificationInfo{
-		ID:          stage.Status.CurrentFreight.VerificationInfo.ID,
-		StartTime:   ptr.To(ar.CreationTimestamp),
-		FinishTime:  ptr.To(metav1.NewTime(r.nowFn())),
-		Phase:       kargoapi.VerificationPhaseAborted,
-		Message:     "Verification aborted by user",
-		AnalysisRun: stage.Status.CurrentFreight.VerificationInfo.AnalysisRun.DeepCopy(),
-	}
+	newInfo.StartTime = ptr.To(ar.CreationTimestamp)
+	newInfo.FinishTime = ptr.To(metav1.NewTime(r.nowFn()))
+	newInfo.Phase = kargoapi.VerificationPhaseAborted
+	newInfo.Message = "Verification aborted by user"
+	newInfo.AnalysisRun = currentInfo.AnalysisRun.DeepCopy()
+	return newInfo
 }
 
 func (r *reconciler) buildAnalysisRun(
@@ -396,13 +391,13 @@ func (r *reconciler) buildAnalysisRun(
 	lbls[kargoapi.StageLabelKey] = stage.Name
 	lbls[kargoapi.FreightLabelKey] = stage.Status.CurrentFreight.Name
 
-	// Check if the AnalysisRun is triggered manually.
-	// When the promotion controller requests re-verification for the re-promotion,
-	// there should be no Reverify Actor key in annotations. However, if the re-verification
-	// is requested by the user, the Reverify Actor key should be added to annotations.
-	if actor, ok := stage.GetAnnotations()[kargoapi.AnnotationKeyReverifyActor]; ok {
-		annotations[kargoapi.AnnotationKeyReverifyActor] = actor
-	} else {
+	// Add Promotion name if the AnalysisRun is triggered by Promotion.
+	// This is the case when there is no existing verification information,
+	// or the re-verification request has been made by the control plane
+	// without an actor specified.
+	curInfo := stage.Status.CurrentFreight.VerificationHistory.Current()
+	reverifyReq, _ := kargoapi.ReverifyAnnotationValue(stage.GetAnnotations())
+	if curInfo == nil || reverifyReq.ForID(curInfo.ID) && reverifyReq.ControlPlane && reverifyReq.Actor == "" {
 		// Add Promotion name if the AnalysisRun is triggered by Promotion.
 		if stage.Status.LastPromotion != nil {
 			lbls[kargoapi.PromotionLabelKey] = stage.Status.LastPromotion.Name
diff --git a/internal/controller/warehouses/warehouses.go b/internal/controller/warehouses/warehouses.go
index 775d9b15..8cb073c7 100644
--- a/internal/controller/warehouses/warehouses.go
+++ b/internal/controller/warehouses/warehouses.go
@@ -119,15 +119,10 @@ func SetupReconcilerWithManager(
 		WithEventFilter(
 			predicate.Or(
 				predicate.GenerationChangedPredicate{},
-				predicate.AnnotationChangedPredicate{},
+				kargo.RefreshRequested{},
 			),
 		).
 		WithEventFilter(shardPredicate).
-		WithEventFilter(kargo.IgnoreAnnotationRemoval{
-			Annotations: []string{
-				kargoapi.AnnotationKeyRefresh,
-			},
-		}).
 		WithOptions(controller.CommonOptions()).
 		Complete(newReconciler(mgr.GetClient(), credentialsDB)); err != nil {
 		return fmt.Errorf("error building Warehouse reconciler: %w", err)
@@ -204,14 +199,6 @@ func (r *reconciler) Reconcile(
 	if updateErr != nil {
 		logger.Errorf("error updating Warehouse status: %s", updateErr)
 	}
-	if clearRefreshErr := kargoapi.ClearAnnotations(
-		ctx,
-		r.client,
-		warehouse,
-		kargoapi.AnnotationKeyRefresh,
-	); clearRefreshErr != nil {
-		logger.Errorf("error clearing Warehouse refresh annotation: %s", clearRefreshErr)
-	}
 
 	// If we had no error, but couldn't update, then we DO have an error. But we
 	// do it this way so that a failure to update is never counted as THE failure
@@ -241,6 +228,11 @@ func (r *reconciler) syncWarehouse(
 	status.ObservedGeneration = warehouse.Generation
 	status.Message = "" // Clear any previous error
 
+	// Record the current refresh token as having been handled.
+	if token, ok := kargoapi.RefreshAnnotationValue(warehouse.GetAnnotations()); ok {
+		status.LastHandledRefresh = token
+	}
+
 	logger := logging.LoggerFromContext(ctx)
 
 	freight, err := r.getLatestFreightFromReposFn(ctx, warehouse)
diff --git a/internal/kargo/kargo.go b/internal/kargo/kargo.go
index 0b3816da..68c32dd1 100644
--- a/internal/kargo/kargo.go
+++ b/internal/kargo/kargo.go
@@ -106,34 +106,71 @@ func (p PromoWentTerminal) Update(e event.UpdateEvent) bool {
 	return false
 }
 
-// IgnoreAnnotationRemoval is a predicate that ignores the removal of specific
-// Annotations in an update event. This is useful when we want to ignore
-// updates that only remove an annotation and nothing else, which may be the
-// case when the annotation(s) are cleared by the controller itself.
-type IgnoreAnnotationRemoval struct {
+// RefreshRequested is a predicate that returns true if the refresh annotation
+// has been set on a resource, or the value of the annotation has changed
+// compared to the previous state.
+type RefreshRequested struct {
 	predicate.Funcs
+}
+
+// Update returns true if the refresh annotation has been set on the new object,
+// or if the value of the annotation has changed compared to the old object.
+func (p RefreshRequested) Update(e event.UpdateEvent) bool {
+	if e.ObjectOld == nil || e.ObjectNew == nil {
+		return false
+	}
 
-	Annotations []string
+	if newVal, newOk := kargoapi.RefreshAnnotationValue(e.ObjectNew.GetAnnotations()); newOk {
+		if oldVal, oldOk := kargoapi.RefreshAnnotationValue(e.ObjectOld.GetAnnotations()); oldOk {
+			return newVal != oldVal
+		}
+		return true
+	}
+	return false
+}
+
+// ReverifyRequested is a predicate that returns true if the reverify annotation
+// has been set on a resource, or the ID of the request has changed compared to
+// the previous state.
+type ReverifyRequested struct {
+	predicate.Funcs
 }
 
-// Update returns false if the update event only removed any of the Annotations,
-// true otherwise.
-//
-// NOTE: this predicate assumes that the update was to clear the annotation and
-// nothing else. This is safe to assume as long as the helpers of the API are
-// always used to clear the respective annotation.
-func (i IgnoreAnnotationRemoval) Update(e event.UpdateEvent) bool {
+// Update returns true if the reverify annotation has been set on the new object,
+// or if the ID of the request has changed compared to the old object.
+func (r ReverifyRequested) Update(e event.UpdateEvent) bool {
 	if e.ObjectOld == nil || e.ObjectNew == nil {
+		return false
+	}
+
+	if newVal, newOk := kargoapi.ReverifyAnnotationValue(e.ObjectNew.GetAnnotations()); newOk {
+		if oldVal, oldOk := kargoapi.ReverifyAnnotationValue(e.ObjectOld.GetAnnotations()); oldOk {
+			return !newVal.ForID(oldVal.ID)
+		}
 		return true
 	}
-	oldAnnotations := e.ObjectOld.GetAnnotations()
-	newAnnotations := e.ObjectNew.GetAnnotations()
-	for _, annotation := range i.Annotations {
-		_, oldHasAnnotation := oldAnnotations[annotation]
-		_, newHasAnnotation := newAnnotations[annotation]
-		if oldHasAnnotation && !newHasAnnotation {
-			return false
+	return false
+}
+
+// AbortRequested is a predicate that returns true if the abort annotation has
+// been set on a resource, or the ID of the request has changed compared to the
+// previous state.
+type AbortRequested struct {
+	predicate.Funcs
+}
+
+// Update returns true if the abort annotation has been set on the new object,
+// or if the ID of the request has changed compared to the old object.
+func (p AbortRequested) Update(e event.UpdateEvent) bool {
+	if e.ObjectOld == nil || e.ObjectNew == nil {
+		return false
+	}
+
+	if newVal, newOk := kargoapi.AbortAnnotationValue(e.ObjectNew.GetAnnotations()); newOk {
+		if oldVal, oldOk := kargoapi.AbortAnnotationValue(e.ObjectOld.GetAnnotations()); oldOk {
+			return !newVal.ForID(oldVal.ID)
 		}
+		return true
 	}
-	return true
+	return false
 }
diff --git a/internal/webhook/stage/webhook.go b/internal/webhook/stage/webhook.go
index 230578dc..1da05ea5 100644
--- a/internal/webhook/stage/webhook.go
+++ b/internal/webhook/stage/webhook.go
@@ -96,20 +96,43 @@ func (w *webhook) Default(ctx context.Context, obj runtime.Object) error {
 		oldStage = req.OldObject.Object.(*kargoapi.Stage) // nolint: forcetypeassert
 	}
 
-	if req.Operation == admissionv1.Create ||
-		req.Operation == admissionv1.Update {
-		if id, ok := stage.Annotations[kargoapi.AnnotationKeyReverify]; ok {
-			// Set actor as an admission request's user info when reverification is requested
-			// to allow controllers to track who triggered it.
-			if !w.isRequestFromKargoControlplaneFn(req) &&
-				(oldStage == nil ||
-					(oldStage != nil && oldStage.Annotations[kargoapi.AnnotationKeyReverify] != id)) {
-				stage.Annotations[kargoapi.AnnotationKeyReverifyActor] =
-					kargoapi.FormatEventKubernetesUserActor(req.UserInfo)
+	if req.Operation == admissionv1.Create || req.Operation == admissionv1.Update {
+		if verReq, ok := kargoapi.ReverifyAnnotationValue(stage.Annotations); ok {
+			var oldVerReq *kargoapi.VerificationRequest
+			if oldStage != nil {
+				oldVerReq, _ = kargoapi.ReverifyAnnotationValue(oldStage.Annotations)
+			}
+			// If the re-verification request has changed, enrich the annotation
+			// with the actor and control plane information.
+			if oldStage == nil || oldVerReq == nil || !verReq.Equals(oldVerReq) {
+				verReq.ControlPlane = w.isRequestFromKargoControlplaneFn(req)
+				if !verReq.ControlPlane {
+					// If the re-verification request is not from the control plane, then
+					// it's from a specific Kubernetes user. Without this check we would
+					// overwrite the actor field set by the control plane.
+					verReq.Actor = kargoapi.FormatEventKubernetesUserActor(req.UserInfo)
+				}
+				stage.Annotations[kargoapi.AnnotationKeyReverify] = verReq.String()
+			}
+		}
+
+		if verReq, ok := kargoapi.AbortAnnotationValue(stage.Annotations); ok {
+			var oldVerReq *kargoapi.VerificationRequest
+			if oldStage != nil {
+				oldVerReq, _ = kargoapi.AbortAnnotationValue(oldStage.Annotations)
+			}
+			// If the abort request has changed, enrich the annotation with the
+			// actor and control plane information.
+			if oldStage == nil || oldVerReq == nil || !verReq.Equals(oldVerReq) {
+				verReq.ControlPlane = w.isRequestFromKargoControlplaneFn(req)
+				if !verReq.ControlPlane {
+					// If the abort request is not from the control plane, then
+					// it's from a specific Kubernetes user. Without this check we would
+					// overwrite the actor field set by the control plane.
+					verReq.Actor = kargoapi.FormatEventKubernetesUserActor(req.UserInfo)
+				}
+				stage.Annotations[kargoapi.AnnotationKeyAbort] = verReq.String()
 			}
-		} else {
-			// Ensure actor annotation is not set when not reverifying
-			delete(stage.Annotations, kargoapi.AnnotationKeyReverifyActor)
 		}
 	}
 	return nil
diff --git a/ui/package.json b/ui/package.json
index 401a6a0e..6cfa186d 100644
--- a/ui/package.json
+++ b/ui/package.json
@@ -43,7 +43,7 @@
     "vitest": "^0.34.6",
     "zx": "^7.2.3"
   },
-  "packageManager": "pnpm@8.1.0",
+  "packageManager": "pnpm@9.0.2",
   "dependencies": {
     "@bufbuild/protobuf": "1.8.0",
     "@connectrpc/connect": "1.4.0",
diff --git a/ui/src/features/project/pipelines/pipelines.tsx b/ui/src/features/project/pipelines/pipelines.tsx
index f7451096..a8b50374 100644
--- a/ui/src/features/project/pipelines/pipelines.tsx
+++ b/ui/src/features/project/pipelines/pipelines.tsx
@@ -196,7 +196,9 @@ export const Pipelines = () => {
           }
         }
 
-        const refreshing = e.warehouse?.metadata?.annotations['kargo.akuity.io/refresh'];
+        const refreshRequest = e.warehouse?.metadata?.annotations['kargo.akuity.io/refresh'];
+        const refreshStatus = e.warehouse?.status?.lastHandledRefresh;
+        const refreshing = refreshRequest !== undefined && refreshRequest !== refreshStatus;
         if (refreshing) {
           refresh[e.warehouse?.metadata?.name || ''] = true;
         } else if (refresh[e.warehouse?.metadata?.name || '']) {
diff --git a/ui/src/features/project/pipelines/types.ts b/ui/src/features/project/pipelines/types.ts
index 57746934..c9ba45c6 100644
--- a/ui/src/features/project/pipelines/types.ts
+++ b/ui/src/features/project/pipelines/types.ts
@@ -50,11 +50,13 @@ export type NodesItemType =
 
 export const NewWarehouseNode = (warehouse: Warehouse, stageNames?: string[]): NodesRepoType => {
   const name = warehouse?.metadata?.name || '';
+  const refreshRequest = warehouse?.metadata?.annotations['kargo.akuity.io/refresh'];
+  const refreshStatus = warehouse?.status?.refreshStatus?.lastHandledRefresh;
   return {
     data: warehouse,
     stageNames: stageNames || [],
     warehouseName: name,
-    refreshing: !!warehouse?.metadata?.annotations['kargo.akuity.io/refresh'],
+    refreshing: refreshRequest !== undefined && refreshRequest !== refreshStatus,
     type: NodeType.WAREHOUSE
   };
 };
diff --git a/ui/src/features/stage/stage-actions.tsx b/ui/src/features/stage/stage-actions.tsx
index 12d3aa30..787580e2 100644
--- a/ui/src/features/stage/stage-actions.tsx
+++ b/ui/src/features/stage/stage-actions.tsx
@@ -58,11 +58,13 @@ export const StageActions = ({ stage }: { stage: Stage }) => {
 
   // Once the Refresh process is done, refetch Freight list
   React.useEffect(() => {
-    if (stage?.metadata?.annotations['kargo.akuity.io/refresh']) {
+    const refreshRequest = stage?.metadata?.annotations['kargo.akuity.io/refresh'];
+    const refreshStatus = stage?.status?.lastHandledRefresh;
+    if (refreshRequest !== undefined && refreshRequest !== refreshStatus) {
       setShouldRefetchFreights(true);
     }
 
-    if (!stage?.metadata?.annotations['kargo.akuity.io/refresh'] && shouldRefetchFreights) {
+    if (refreshRequest === refreshStatus && shouldRefetchFreights) {
       queryClient.invalidateQueries({ queryKey: createConnectQueryKey(queryFreight) });
       setShouldRefetchFreights(false);
     }
@@ -128,7 +130,7 @@ export const StageActions = ({ stage }: { stage: Stage }) => {
         type='default'
         icon={<FontAwesomeIcon icon={faRefresh} size='1x' />}
         onClick={onRefresh}
-        loading={isRefreshLoading || !!stage?.metadata?.annotations['kargo.akuity.io/refresh']}
+        loading={isRefreshLoading || (!!stage?.metadata?.annotations['kargo.akuity.io/refresh'] && stage?.metadata?.annotations?.['kargo.akuity.io/refresh'] !== stage?.status?.lastHandledRefresh)}
       >
         Refresh
       </Button>
diff --git a/ui/src/gen/schema/promotions.kargo.akuity.io_v1alpha1.json b/ui/src/gen/schema/promotions.kargo.akuity.io_v1alpha1.json
index 3a096a96..dcdcb31e 100644
--- a/ui/src/gen/schema/promotions.kargo.akuity.io_v1alpha1.json
+++ b/ui/src/gen/schema/promotions.kargo.akuity.io_v1alpha1.json
@@ -37,6 +37,10 @@
     "status": {
       "description": "Status describes the current state of the transition represented by this\nPromotion.",
       "properties": {
+        "lastHandledRefresh": {
+          "description": "LastHandledRefresh holds the value of the most recent AnnotationKeyRefresh\nannotation that was handled by the controller. This field can be used to\ndetermine whether the request to refresh the resource has been handled.",
+          "type": "string"
+        },
         "message": {
           "description": "Message is a display message about the promotion, including any errors\npreventing the Promotion controller from executing this Promotion.\ni.e. If the Phase field has a value of Failed, this field can be expected\nto explain why.",
           "type": "string"
diff --git a/ui/src/gen/schema/stages.kargo.akuity.io_v1alpha1.json b/ui/src/gen/schema/stages.kargo.akuity.io_v1alpha1.json
index ae5a51ca..56462368 100644
--- a/ui/src/gen/schema/stages.kargo.akuity.io_v1alpha1.json
+++ b/ui/src/gen/schema/stages.kargo.akuity.io_v1alpha1.json
@@ -550,6 +550,10 @@
               "items": {
                 "description": "VerificationInfo contains information about the currently running\nVerification process.",
                 "properties": {
+                  "actor": {
+                    "description": "Actor is the name of the entity that initiated or aborted the\nVerification process.",
+                    "type": "string"
+                  },
                   "analysisRun": {
                     "description": "AnalysisRun is a reference to the Argo Rollouts AnalysisRun that implements\nthe Verification process.",
                     "properties": {
@@ -603,6 +607,10 @@
             "verificationInfo": {
               "description": "VerificationInfo is information about any verification process that was\nassociated with this Freight for this Stage.",
               "properties": {
+                "actor": {
+                  "description": "Actor is the name of the entity that initiated or aborted the\nVerification process.",
+                  "type": "string"
+                },
                 "analysisRun": {
                   "description": "AnalysisRun is a reference to the Argo Rollouts AnalysisRun that implements\nthe Verification process.",
                   "properties": {
@@ -759,6 +767,10 @@
                   "items": {
                     "description": "VerificationInfo contains information about the currently running\nVerification process.",
                     "properties": {
+                      "actor": {
+                        "description": "Actor is the name of the entity that initiated or aborted the\nVerification process.",
+                        "type": "string"
+                      },
                       "analysisRun": {
                         "description": "AnalysisRun is a reference to the Argo Rollouts AnalysisRun that implements\nthe Verification process.",
                         "properties": {
@@ -812,6 +824,10 @@
                 "verificationInfo": {
                   "description": "VerificationInfo is information about any verification process that was\nassociated with this Freight for this Stage.",
                   "properties": {
+                    "actor": {
+                      "description": "Actor is the name of the entity that initiated or aborted the\nVerification process.",
+                      "type": "string"
+                    },
                     "analysisRun": {
                       "description": "AnalysisRun is a reference to the Argo Rollouts AnalysisRun that implements\nthe Verification process.",
                       "properties": {
@@ -874,6 +890,10 @@
             "status": {
               "description": "Status is the (optional) status of the promotion",
               "properties": {
+                "lastHandledRefresh": {
+                  "description": "LastHandledRefresh holds the value of the most recent AnnotationKeyRefresh\nannotation that was handled by the controller. This field can be used to\ndetermine whether the request to refresh the resource has been handled.",
+                  "type": "string"
+                },
                 "message": {
                   "description": "Message is a display message about the promotion, including any errors\npreventing the Promotion controller from executing this Promotion.\ni.e. If the Phase field has a value of Failed, this field can be expected\nto explain why.",
                   "type": "string"
@@ -1074,6 +1094,10 @@
                 "items": {
                   "description": "VerificationInfo contains information about the currently running\nVerification process.",
                   "properties": {
+                    "actor": {
+                      "description": "Actor is the name of the entity that initiated or aborted the\nVerification process.",
+                      "type": "string"
+                    },
                     "analysisRun": {
                       "description": "AnalysisRun is a reference to the Argo Rollouts AnalysisRun that implements\nthe Verification process.",
                       "properties": {
@@ -1127,6 +1151,10 @@
               "verificationInfo": {
                 "description": "VerificationInfo is information about any verification process that was\nassociated with this Freight for this Stage.",
                 "properties": {
+                  "actor": {
+                    "description": "Actor is the name of the entity that initiated or aborted the\nVerification process.",
+                    "type": "string"
+                  },
                   "analysisRun": {
                     "description": "AnalysisRun is a reference to the Argo Rollouts AnalysisRun that implements\nthe Verification process.",
                     "properties": {
@@ -1184,6 +1212,10 @@
           },
           "type": "array"
         },
+        "lastHandledRefresh": {
+          "description": "LastHandledRefresh holds the value of the most recent AnnotationKeyRefresh\nannotation that was handled by the controller. This field can be used to\ndetermine whether the request to refresh the resource has been handled.",
+          "type": "string"
+        },
         "lastPromotion": {
           "description": "LastPromotion is a reference to the last completed promotion.",
           "properties": {
@@ -1285,6 +1317,10 @@
                   "items": {
                     "description": "VerificationInfo contains information about the currently running\nVerification process.",
                     "properties": {
+                      "actor": {
+                        "description": "Actor is the name of the entity that initiated or aborted the\nVerification process.",
+                        "type": "string"
+                      },
                       "analysisRun": {
                         "description": "AnalysisRun is a reference to the Argo Rollouts AnalysisRun that implements\nthe Verification process.",
                         "properties": {
@@ -1338,6 +1374,10 @@
                 "verificationInfo": {
                   "description": "VerificationInfo is information about any verification process that was\nassociated with this Freight for this Stage.",
                   "properties": {
+                    "actor": {
+                      "description": "Actor is the name of the entity that initiated or aborted the\nVerification process.",
+                      "type": "string"
+                    },
                     "analysisRun": {
                       "description": "AnalysisRun is a reference to the Argo Rollouts AnalysisRun that implements\nthe Verification process.",
                       "properties": {
@@ -1400,6 +1440,10 @@
             "status": {
               "description": "Status is the (optional) status of the promotion",
               "properties": {
+                "lastHandledRefresh": {
+                  "description": "LastHandledRefresh holds the value of the most recent AnnotationKeyRefresh\nannotation that was handled by the controller. This field can be used to\ndetermine whether the request to refresh the resource has been handled.",
+                  "type": "string"
+                },
                 "message": {
                   "description": "Message is a display message about the promotion, including any errors\npreventing the Promotion controller from executing this Promotion.\ni.e. If the Phase field has a value of Failed, this field can be expected\nto explain why.",
                   "type": "string"
diff --git a/ui/src/gen/schema/warehouses.kargo.akuity.io_v1alpha1.json b/ui/src/gen/schema/warehouses.kargo.akuity.io_v1alpha1.json
index 177b42e4..b5d8816f 100644
--- a/ui/src/gen/schema/warehouses.kargo.akuity.io_v1alpha1.json
+++ b/ui/src/gen/schema/warehouses.kargo.akuity.io_v1alpha1.json
@@ -280,6 +280,10 @@
               "items": {
                 "description": "VerificationInfo contains information about the currently running\nVerification process.",
                 "properties": {
+                  "actor": {
+                    "description": "Actor is the name of the entity that initiated or aborted the\nVerification process.",
+                    "type": "string"
+                  },
                   "analysisRun": {
                     "description": "AnalysisRun is a reference to the Argo Rollouts AnalysisRun that implements\nthe Verification process.",
                     "properties": {
@@ -333,6 +337,10 @@
             "verificationInfo": {
               "description": "VerificationInfo is information about any verification process that was\nassociated with this Freight for this Stage.",
               "properties": {
+                "actor": {
+                  "description": "Actor is the name of the entity that initiated or aborted the\nVerification process.",
+                  "type": "string"
+                },
                 "analysisRun": {
                   "description": "AnalysisRun is a reference to the Argo Rollouts AnalysisRun that implements\nthe Verification process.",
                   "properties": {
@@ -388,6 +396,10 @@
           },
           "type": "object"
         },
+        "lastHandledRefresh": {
+          "description": "LastHandledRefresh holds the value of the most recent AnnotationKeyRefresh\nannotation that was handled by the controller. This field can be used to\ndetermine whether the request to refresh the resource has been handled.",
+          "type": "string"
+        },
         "message": {
           "description": "Message describes any errors that are preventing the Warehouse controller\nfrom polling repositories to discover new Freight.",
           "type": "string"
diff --git a/ui/src/gen/v1alpha1/generated_pb.ts b/ui/src/gen/v1alpha1/generated_pb.ts
index e6b7ae24..4a724d57 100644
--- a/ui/src/gen/v1alpha1/generated_pb.ts
+++ b/ui/src/gen/v1alpha1/generated_pb.ts
@@ -2887,6 +2887,16 @@ export class PromotionSpec extends Message<PromotionSpec> {
  * @generated from message github.com.akuity.kargo.api.v1alpha1.PromotionStatus
  */
 export class PromotionStatus extends Message<PromotionStatus> {
+  /**
+   * LastHandledRefresh holds the value of the most recent AnnotationKeyRefresh
+   * annotation that was handled by the controller. This field can be used to
+   * determine whether the request to refresh the resource has been handled.
+   * +optional
+   *
+   * @generated from field: optional string lastHandledRefresh = 4;
+   */
+  lastHandledRefresh?: string;
+
   /**
    * Phase describes where the Promotion currently is in its lifecycle.
    *
@@ -2920,6 +2930,7 @@ export class PromotionStatus extends Message<PromotionStatus> {
   static readonly runtime: typeof proto2 = proto2;
   static readonly typeName = "github.com.akuity.kargo.api.v1alpha1.PromotionStatus";
   static readonly fields: FieldList = proto2.util.newFieldList(() => [
+    { no: 4, name: "lastHandledRefresh", kind: "scalar", T: 9 /* ScalarType.STRING */, opt: true },
     { no: 1, name: "phase", kind: "scalar", T: 9 /* ScalarType.STRING */, opt: true },
     { no: 2, name: "message", kind: "scalar", T: 9 /* ScalarType.STRING */, opt: true },
     { no: 3, name: "metadata", kind: "map", K: 9 /* ScalarType.STRING */, V: {kind: "scalar", T: 9 /* ScalarType.STRING */} },
@@ -3239,6 +3250,16 @@ export class StageSpec extends Message<StageSpec> {
  * @generated from message github.com.akuity.kargo.api.v1alpha1.StageStatus
  */
 export class StageStatus extends Message<StageStatus> {
+  /**
+   * LastHandledRefresh holds the value of the most recent AnnotationKeyRefresh
+   * annotation that was handled by the controller. This field can be used to
+   * determine whether the request to refresh the resource has been handled.
+   * +optional
+   *
+   * @generated from field: optional string lastHandledRefresh = 11;
+   */
+  lastHandledRefresh?: string;
+
   /**
    * Phase describes where the Stage currently is in its lifecycle.
    *
@@ -3307,6 +3328,7 @@ export class StageStatus extends Message<StageStatus> {
   static readonly runtime: typeof proto2 = proto2;
   static readonly typeName = "github.com.akuity.kargo.api.v1alpha1.StageStatus";
   static readonly fields: FieldList = proto2.util.newFieldList(() => [
+    { no: 11, name: "lastHandledRefresh", kind: "scalar", T: 9 /* ScalarType.STRING */, opt: true },
     { no: 1, name: "phase", kind: "scalar", T: 9 /* ScalarType.STRING */, opt: true },
     { no: 2, name: "currentFreight", kind: "message", T: FreightReference, opt: true },
     { no: 3, name: "history", kind: "message", T: FreightReference, repeated: true },
@@ -3504,6 +3526,14 @@ export class VerificationInfo extends Message<VerificationInfo> {
    */
   id?: string;
 
+  /**
+   * Actor is the name of the entity that initiated or aborted the
+   * Verification process.
+   *
+   * @generated from field: optional string actor = 7;
+   */
+  actor?: string;
+
   /**
    * StartTime is the time at which the Verification process was started.
    *
@@ -3553,6 +3583,7 @@ export class VerificationInfo extends Message<VerificationInfo> {
   static readonly typeName = "github.com.akuity.kargo.api.v1alpha1.VerificationInfo";
   static readonly fields: FieldList = proto2.util.newFieldList(() => [
     { no: 4, name: "id", kind: "scalar", T: 9 /* ScalarType.STRING */, opt: true },
+    { no: 7, name: "actor", kind: "scalar", T: 9 /* ScalarType.STRING */, opt: true },
     { no: 5, name: "startTime", kind: "message", T: Time, opt: true },
     { no: 1, name: "phase", kind: "scalar", T: 9 /* ScalarType.STRING */, opt: true },
     { no: 2, name: "message", kind: "scalar", T: 9 /* ScalarType.STRING */, opt: true },
@@ -3778,6 +3809,16 @@ export class WarehouseSpec extends Message<WarehouseSpec> {
  * @generated from message github.com.akuity.kargo.api.v1alpha1.WarehouseStatus
  */
 export class WarehouseStatus extends Message<WarehouseStatus> {
+  /**
+   * LastHandledRefresh holds the value of the most recent AnnotationKeyRefresh
+   * annotation that was handled by the controller. This field can be used to
+   * determine whether the request to refresh the resource has been handled.
+   * +optional
+   *
+   * @generated from field: optional string lastHandledRefresh = 6;
+   */
+  lastHandledRefresh?: string;
+
   /**
    * Message describes any errors that are preventing the Warehouse controller
    * from polling repositories to discover new Freight.
@@ -3809,6 +3850,7 @@ export class WarehouseStatus extends Message<WarehouseStatus> {
   static readonly runtime: typeof proto2 = proto2;
   static readonly typeName = "github.com.akuity.kargo.api.v1alpha1.WarehouseStatus";
   static readonly fields: FieldList = proto2.util.newFieldList(() => [
+    { no: 6, name: "lastHandledRefresh", kind: "scalar", T: 9 /* ScalarType.STRING */, opt: true },
     { no: 3, name: "message", kind: "scalar", T: 9 /* ScalarType.STRING */, opt: true },
     { no: 4, name: "observedGeneration", kind: "scalar", T: 3 /* ScalarType.INT64 */, opt: true },
     { no: 5, name: "lastFreight", kind: "message", T: FreightReference, opt: true },


Test patch:
diff --git a/api/v1alpha1/annotations_test.go b/api/v1alpha1/annotations_test.go
new file mode 100644
index 00000000..6215e3f9
--- /dev/null
+++ b/api/v1alpha1/annotations_test.go
@@ -0,0 +1,95 @@
+package v1alpha1
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/require"
+)
+
+func TestRefreshAnnotationValue(t *testing.T) {
+	t.Run("has refresh annotation", func(t *testing.T) {
+		result, ok := RefreshAnnotationValue(map[string]string{
+			AnnotationKeyRefresh: "foo",
+		})
+		require.True(t, ok)
+		require.Equal(t, "foo", result)
+	})
+
+	t.Run("does not have refresh annotation", func(t *testing.T) {
+		result, ok := RefreshAnnotationValue(nil)
+		require.False(t, ok)
+		require.Empty(t, result)
+	})
+
+	t.Run("has refresh annotation with empty value", func(t *testing.T) {
+		result, ok := RefreshAnnotationValue(map[string]string{
+			AnnotationKeyRefresh: "",
+		})
+		require.True(t, ok)
+		require.Empty(t, result)
+	})
+}
+
+func TestReverifyAnnotationValue(t *testing.T) {
+	t.Run("has reverify annotation with valid JSON", func(t *testing.T) {
+		result, ok := ReverifyAnnotationValue(map[string]string{
+			AnnotationKeyReverify: `{"id":"foo"}`,
+		})
+		require.True(t, ok)
+		require.Equal(t, "foo", result.ID)
+	})
+
+	t.Run("has reverify annotation with ID string", func(t *testing.T) {
+		result, ok := ReverifyAnnotationValue(map[string]string{
+			AnnotationKeyReverify: "foo",
+		})
+		require.True(t, ok)
+		require.Equal(t, "foo", result.ID)
+	})
+
+	t.Run("does not have reverify annotation", func(t *testing.T) {
+		result, ok := ReverifyAnnotationValue(nil)
+		require.False(t, ok)
+		require.Nil(t, result)
+	})
+
+	t.Run("has reverify annotation with empty ID", func(t *testing.T) {
+		result, ok := ReverifyAnnotationValue(map[string]string{
+			AnnotationKeyAbort: "",
+		})
+		require.False(t, ok)
+		require.Nil(t, result)
+	})
+}
+
+func TestAbortAnnotationValue(t *testing.T) {
+	t.Run("has abort annotation with valid JSON", func(t *testing.T) {
+		result, ok := AbortAnnotationValue(map[string]string{
+			AnnotationKeyAbort: `{"id":"foo"}`,
+		})
+		require.True(t, ok)
+		require.Equal(t, "foo", result.ID)
+	})
+
+	t.Run("has abort annotation with ID string", func(t *testing.T) {
+		result, ok := AbortAnnotationValue(map[string]string{
+			AnnotationKeyAbort: "foo",
+		})
+		require.True(t, ok)
+		require.Equal(t, "foo", result.ID)
+	})
+
+	t.Run("does not have abort annotation", func(t *testing.T) {
+		result, ok := AbortAnnotationValue(nil)
+		require.False(t, ok)
+		require.Nil(t, result)
+	})
+
+	t.Run("has abort annotation with empty ID", func(t *testing.T) {
+		result, ok := AbortAnnotationValue(map[string]string{
+			AnnotationKeyAbort: "",
+		})
+		require.False(t, ok)
+		require.Nil(t, result)
+	})
+}
diff --git a/api/v1alpha1/stage_helpers_test.go b/api/v1alpha1/stage_helpers_test.go
index b8bf3e77..e7bc627d 100644
--- a/api/v1alpha1/stage_helpers_test.go
+++ b/api/v1alpha1/stage_helpers_test.go
@@ -12,6 +12,136 @@ import (
 	"sigs.k8s.io/controller-runtime/pkg/client/fake"
 )
 
+func TestVerificationRequest_Equals(t *testing.T) {
+	tests := []struct {
+		name     string
+		r1       *VerificationRequest
+		r2       *VerificationRequest
+		expected bool
+	}{
+		{
+			name:     "both nil",
+			r1:       nil,
+			r2:       nil,
+			expected: true,
+		},
+		{
+			name:     "one nil",
+			r1:       &VerificationRequest{ID: "fake-id", Actor: "fake-actor", ControlPlane: false},
+			r2:       nil,
+			expected: false,
+		},
+		{
+			name:     "other nil",
+			r1:       nil,
+			r2:       &VerificationRequest{ID: "fake-id", Actor: "fake-actor", ControlPlane: false},
+			expected: false,
+		},
+		{
+			name:     "different IDs",
+			r1:       &VerificationRequest{ID: "fake-id", Actor: "fake-actor", ControlPlane: false},
+			r2:       &VerificationRequest{ID: "other-id", Actor: "fake-actor", ControlPlane: false},
+			expected: false,
+		},
+		{
+			name:     "different actors",
+			r1:       &VerificationRequest{ID: "fake-id", Actor: "fake-actor", ControlPlane: true},
+			r2:       &VerificationRequest{ID: "fake-id", Actor: "other-actor", ControlPlane: true},
+			expected: false,
+		},
+		{
+			name:     "different control plane flags",
+			r1:       &VerificationRequest{ID: "fake-id", Actor: "fake-actor", ControlPlane: true},
+			r2:       &VerificationRequest{ID: "fake-id", Actor: "fake-actor", ControlPlane: false},
+			expected: false,
+		},
+		{
+			name:     "equal",
+			r1:       &VerificationRequest{ID: "fake-id", Actor: "fake-actor", ControlPlane: true},
+			r2:       &VerificationRequest{ID: "fake-id", Actor: "fake-actor", ControlPlane: true},
+			expected: true,
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			require.Equal(t, tt.r1.Equals(tt.r2), tt.expected)
+		})
+	}
+}
+
+func TestVerificationRequest_HasID(t *testing.T) {
+	t.Run("verification request is nil", func(t *testing.T) {
+		var r *VerificationRequest
+		require.False(t, r.HasID())
+	})
+
+	t.Run("verification request has empty ID", func(t *testing.T) {
+		r := &VerificationRequest{
+			ID: "",
+		}
+		require.False(t, r.HasID())
+	})
+
+	t.Run("verification request has ID", func(t *testing.T) {
+		r := &VerificationRequest{
+			ID: "foo",
+		}
+		require.True(t, r.HasID())
+	})
+}
+
+func TestVerificationRequest_ForID(t *testing.T) {
+	t.Run("verification request is nil", func(t *testing.T) {
+		var r *VerificationRequest
+		require.False(t, r.ForID("foo"))
+	})
+
+	t.Run("verification request has ID", func(t *testing.T) {
+		r := &VerificationRequest{
+			ID: "foo",
+		}
+		require.True(t, r.ForID("foo"))
+		require.False(t, r.ForID("bar"))
+	})
+
+	t.Run("verification request has empty ID", func(t *testing.T) {
+		r := &VerificationRequest{
+			ID: "",
+		}
+		require.False(t, r.ForID(""))
+		require.False(t, r.ForID("foo"))
+	})
+}
+
+func TestVerificationRequest_String(t *testing.T) {
+	t.Run("verification request is nil", func(t *testing.T) {
+		var r *VerificationRequest
+		require.Empty(t, r.String())
+	})
+
+	t.Run("verification request is empty", func(t *testing.T) {
+		r := &VerificationRequest{}
+		require.Empty(t, r.String())
+	})
+
+	t.Run("verification request has empty ID", func(t *testing.T) {
+		r := &VerificationRequest{
+			ID: "",
+		}
+		require.Empty(t, r.String())
+	})
+
+	t.Run("verification request has data", func(t *testing.T) {
+		r := &VerificationRequest{
+			ID:           "foo",
+			Actor:        "fake-actor",
+			ControlPlane: true,
+		}
+		require.Equal(t, `{"id":"foo","actor":"fake-actor","controlPlane":true}`, r.String())
+	})
+}
+
 func TestGetStage(t *testing.T) {
 	scheme := k8sruntime.NewScheme()
 	require.NoError(t, SchemeBuilder.AddToScheme(scheme))
@@ -164,7 +294,9 @@ func TestReverifyStageFreight(t *testing.T) {
 			Name:      "fake-stage",
 		})
 		require.NoError(t, err)
-		require.Equal(t, "fake-id", stage.Annotations[AnnotationKeyReverify])
+		require.Equal(t, (&VerificationRequest{
+			ID: "fake-id",
+		}).String(), stage.Annotations[AnnotationKeyReverify])
 	})
 }
 
@@ -302,6 +434,8 @@ func TestAbortStageFreightVerification(t *testing.T) {
 			Name:      "fake-stage",
 		})
 		require.NoError(t, err)
-		require.Equal(t, "fake-id", stage.Annotations[AnnotationKeyAbort])
+		require.Equal(t, (&VerificationRequest{
+			ID: "fake-id",
+		}).String(), stage.Annotations[AnnotationKeyAbort])
 	})
 }
diff --git a/api/v1alpha1/stage_types_test.go b/api/v1alpha1/stage_types_test.go
index 15fbfc2a..b528d5d9 100644
--- a/api/v1alpha1/stage_types_test.go
+++ b/api/v1alpha1/stage_types_test.go
@@ -86,6 +86,46 @@ func TestFreightReferenceStackUpdateOrPush(t *testing.T) {
 	}
 }
 
+func TestVerificationInfoStack_Current(t *testing.T) {
+	testCases := []struct {
+		name           string
+		stack          VerificationInfoStack
+		expectedResult *VerificationInfo
+	}{
+		{
+			name:           "stack is nil",
+			stack:          nil,
+			expectedResult: nil,
+		},
+		{
+			name:           "stack is empty",
+			stack:          VerificationInfoStack{},
+			expectedResult: nil,
+		},
+		{
+			name: "stack has one element",
+			stack: VerificationInfoStack{
+				{ID: "foo"},
+			},
+			expectedResult: &VerificationInfo{ID: "foo"},
+		},
+		{
+			name: "stack has multiple elements",
+			stack: VerificationInfoStack{
+				{ID: "foo"},
+				{ID: "bar"},
+				{ID: "baz"},
+			},
+			expectedResult: &VerificationInfo{ID: "foo"},
+		},
+	}
+	for _, testCase := range testCases {
+		t.Run(testCase.name, func(t *testing.T) {
+			require.Equal(t, testCase.expectedResult, testCase.stack.Current())
+		})
+	}
+}
+
 func TestVerificationInfoStack_UpdateOrPush(t *testing.T) {
 	testCases := []struct {
 		name          string
diff --git a/internal/controller/stages/verification_test.go b/internal/controller/stages/verification_test.go
index 20ecd87f..4ff7bd4f 100644
--- a/internal/controller/stages/verification_test.go
+++ b/internal/controller/stages/verification_test.go
@@ -31,6 +31,11 @@ func TestStartVerification(t *testing.T) {
 			reconciler: &reconciler{
 				nowFn: fakeNow,
 			},
+			stage: &kargoapi.Stage{
+				Status: kargoapi.StageStatus{
+					CurrentFreight: &kargoapi.FreightReference{},
+				},
+			},
 			assertions: func(t *testing.T, vi *kargoapi.VerificationInfo, err error) {
 				require.NoError(t, err)
 				require.Contains(
@@ -117,6 +122,9 @@ func TestStartVerification(t *testing.T) {
 				Status: kargoapi.StageStatus{
 					CurrentFreight: &kargoapi.FreightReference{
 						Name: "fake-id",
+						VerificationHistory: []kargoapi.VerificationInfo{{
+							ID: "fake-id",
+						}},
 					},
 				},
 			},
@@ -809,6 +817,14 @@ func TestAbortVerification(t *testing.T) {
 				nowFn: fakeNow,
 			},
 			stage: &kargoapi.Stage{
+				ObjectMeta: metav1.ObjectMeta{
+					Annotations: map[string]string{
+						kargoapi.AnnotationKeyAbort: (&kargoapi.VerificationRequest{
+							ID:    "fake-id",
+							Actor: "fake-actor",
+						}).String(),
+					},
+				},
 				Status: kargoapi.StageStatus{
 					CurrentFreight: &kargoapi.FreightReference{
 						VerificationInfo: &kargoapi.VerificationInfo{
@@ -820,6 +836,7 @@ func TestAbortVerification(t *testing.T) {
 			assertions: func(t *testing.T, vi *kargoapi.VerificationInfo) {
 				require.NotNil(t, vi)
 				require.Equal(t, vi.ID, "fake-id")
+				require.Equal(t, vi.Actor, "fake-actor")
 				require.Contains(
 					t,
 					vi.Message,
@@ -867,6 +884,14 @@ func TestAbortVerification(t *testing.T) {
 		{
 			name: "success",
 			stage: &kargoapi.Stage{
+				ObjectMeta: metav1.ObjectMeta{
+					Annotations: map[string]string{
+						kargoapi.AnnotationKeyAbort: (&kargoapi.VerificationRequest{
+							ID:    "fake-id",
+							Actor: "fake-actor",
+						}).String(),
+					},
+				},
 				Status: kargoapi.StageStatus{
 					CurrentFreight: &kargoapi.FreightReference{
 						VerificationInfo: &kargoapi.VerificationInfo{
@@ -897,6 +922,7 @@ func TestAbortVerification(t *testing.T) {
 			assertions: func(t *testing.T, vi *kargoapi.VerificationInfo) {
 				require.NotNil(t, vi)
 				require.Equal(t, "fake-id", vi.ID)
+				require.Equal(t, "fake-actor", vi.Actor)
 				require.Equal(t, kargoapi.VerificationPhaseAborted, vi.Phase)
 				require.Equal(t, "Verification aborted by user", vi.Message)
 				require.Equal(t, &kargoapi.AnalysisRunReference{
@@ -1204,20 +1230,30 @@ func TestBuildAnalysisRun(t *testing.T) {
 			},
 		},
 		{
-			name:       "Set actor annotation only if the user triggers re-verification",
+			name:       "Does not set promotion name if user triggers re-verification",
 			reconciler: &reconciler{},
 			stage: &kargoapi.Stage{
 				ObjectMeta: metav1.ObjectMeta{
 					Annotations: map[string]string{
-						kargoapi.AnnotationKeyReverify:      "fake-id",
-						kargoapi.AnnotationKeyReverifyActor: "fake-user",
+						kargoapi.AnnotationKeyReverify: (&kargoapi.VerificationRequest{
+							ID:           "fake-id",
+							Actor:        kargoapi.EventActorAdmin,
+							ControlPlane: true,
+						}).String(),
 					},
 				},
 				Spec: &kargoapi.StageSpec{
 					Verification: &kargoapi.Verification{},
 				},
 				Status: kargoapi.StageStatus{
-					CurrentFreight: &kargoapi.FreightReference{Name: "fake-id"},
+					CurrentFreight: &kargoapi.FreightReference{
+						Name: "fake-id",
+						VerificationHistory: []kargoapi.VerificationInfo{
+							{
+								ID: "fake-id",
+							},
+						},
+					},
 					LastPromotion: &kargoapi.PromotionInfo{
 						Name: "fake-id",
 						Status: &kargoapi.PromotionStatus{
@@ -1237,23 +1273,32 @@ func TestBuildAnalysisRun(t *testing.T) {
 				require.NoError(t, err)
 				require.NotNil(t, ar)
 				require.NotContains(t, ar.Labels, kargoapi.PromotionLabelKey)
-				require.Equal(t, "fake-user", ar.Annotations[kargoapi.AnnotationKeyReverifyActor])
 			},
 		},
 		{
-			name:       "Set promotion name only if the controlplane components trigger re-verification",
+			name:       "Set promotion name only if the control plane triggers re-verification",
 			reconciler: &reconciler{},
 			stage: &kargoapi.Stage{
 				ObjectMeta: metav1.ObjectMeta{
 					Annotations: map[string]string{
-						kargoapi.AnnotationKeyReverify: "fake-id",
+						kargoapi.AnnotationKeyReverify: (&kargoapi.VerificationRequest{
+							ID:           "fake-id",
+							ControlPlane: true,
+						}).String(),
 					},
 				},
 				Spec: &kargoapi.StageSpec{
 					Verification: &kargoapi.Verification{},
 				},
 				Status: kargoapi.StageStatus{
-					CurrentFreight: &kargoapi.FreightReference{Name: "fake-id"},
+					CurrentFreight: &kargoapi.FreightReference{
+						Name: "fake-id",
+						VerificationHistory: []kargoapi.VerificationInfo{
+							{
+								ID: "fake-id",
+							},
+						},
+					},
 					LastPromotion: &kargoapi.PromotionInfo{
 						Name: "fake-id",
 						Status: &kargoapi.PromotionStatus{
diff --git a/internal/git/git_test.go b/internal/git/git_test.go
index 53f5c555..5ae498a6 100644
--- a/internal/git/git_test.go
+++ b/internal/git/git_test.go
@@ -6,7 +6,7 @@ import (
 	"github.com/stretchr/testify/require"
 )
 
-func TestNormalizeGitURL(t *testing.T) {
+func TestNormalizeURL(t *testing.T) {
 	testCases := map[string]string{
 		// Anything we can't normalize should be returned as-is
 		"https://not a url":                      "https://not a url",
diff --git a/internal/kargo/kargo_test.go b/internal/kargo/kargo_test.go
index c01d9ef9..7ef06e47 100644
--- a/internal/kargo/kargo_test.go
+++ b/internal/kargo/kargo_test.go
@@ -3,7 +3,6 @@ package kargo
 import (
 	"strings"
 	"testing"
-	"time"
 
 	"github.com/stretchr/testify/require"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@@ -75,63 +74,505 @@ func TestNewPromotion(t *testing.T) {
 	}
 }
 
-func TestIgnoreAnnotationRemovalUpdates(t *testing.T) {
-	testCases := []struct {
-		name     string
-		old      client.Object
-		new      client.Object
-		expected bool
+func TestRefreshRequested_Update(t *testing.T) {
+	tests := []struct {
+		name      string
+		oldObject client.Object
+		newObject client.Object
+		want      bool
 	}{
 		{
-			name:     "nil",
-			old:      nil,
-			new:      nil,
-			expected: true,
+			name:      "no old or new object",
+			oldObject: nil,
+			newObject: nil,
+			want:      false,
 		},
 		{
-			name: "annotation removed",
-			old: &kargoapi.Stage{
+			name:      "no old object",
+			oldObject: nil,
+			newObject: &kargoapi.Stage{
 				ObjectMeta: metav1.ObjectMeta{
 					Annotations: map[string]string{
-						kargoapi.AnnotationKeyRefresh: time.Now().Format(time.RFC3339),
+						kargoapi.AnnotationKeyRefresh: "foo",
 					},
 				},
 			},
-			new: &kargoapi.Stage{
+			want: false,
+		},
+		{
+			name: "no new object",
+			oldObject: &kargoapi.Stage{
+				ObjectMeta: metav1.ObjectMeta{
+					Annotations: map[string]string{
+						kargoapi.AnnotationKeyRefresh: "foo",
+					},
+				},
+			},
+			newObject: nil,
+			want:      false,
+		},
+		{
+			name: "no refresh annotation",
+			oldObject: &kargoapi.Stage{
 				ObjectMeta: metav1.ObjectMeta{
 					Annotations: map[string]string{},
 				},
 			},
-			expected: false,
+			newObject: &kargoapi.Stage{
+				ObjectMeta: metav1.ObjectMeta{
+					Annotations: map[string]string{
+						"other": "annotation",
+					},
+				},
+			},
+			want: false,
 		},
 		{
-			name: "annotation set",
-			old: &kargoapi.Stage{
+			name: "refresh annotation set on new object",
+			oldObject: &kargoapi.Stage{
 				ObjectMeta: metav1.ObjectMeta{
 					Annotations: map[string]string{},
 				},
 			},
-			new: &kargoapi.Stage{
+			newObject: &kargoapi.Stage{
 				ObjectMeta: metav1.ObjectMeta{
 					Annotations: map[string]string{
-						kargoapi.AnnotationKeyRefresh: time.Now().Format(time.RFC3339),
+						kargoapi.AnnotationKeyRefresh: "foo",
 					},
 				},
 			},
-			expected: true,
+			want: true,
+		},
+		{
+			name: "refresh annotation removed from new object",
+			oldObject: &kargoapi.Stage{
+				ObjectMeta: metav1.ObjectMeta{
+					Annotations: map[string]string{
+						kargoapi.AnnotationKeyRefresh: "foo",
+					},
+				},
+			},
+			newObject: &kargoapi.Stage{
+				ObjectMeta: metav1.ObjectMeta{
+					Annotations: map[string]string{},
+				},
+			},
+			want: false,
+		},
+		{
+			name: "refresh annotation value changed",
+			oldObject: &kargoapi.Stage{
+				ObjectMeta: metav1.ObjectMeta{
+					Annotations: map[string]string{
+						kargoapi.AnnotationKeyRefresh: "foo",
+					},
+				},
+			},
+			newObject: &kargoapi.Stage{
+				ObjectMeta: metav1.ObjectMeta{
+					Annotations: map[string]string{
+						kargoapi.AnnotationKeyRefresh: "bar",
+					},
+				},
+			},
+			want: true,
+		},
+		{
+			name: "refresh annotation value equal",
+			oldObject: &kargoapi.Stage{
+				ObjectMeta: metav1.ObjectMeta{
+					Annotations: map[string]string{
+						kargoapi.AnnotationKeyRefresh: "foo",
+					},
+				},
+			},
+			newObject: &kargoapi.Stage{
+				ObjectMeta: metav1.ObjectMeta{
+					Annotations: map[string]string{
+						kargoapi.AnnotationKeyRefresh: "foo",
+					},
+				},
+			},
+			want: false,
 		},
 	}
-	for _, tc := range testCases {
-		tc := tc
-		t.Run(tc.name, func(t *testing.T) {
-			p := IgnoreAnnotationRemoval{
-				Annotations: []string{kargoapi.AnnotationKeyRefresh},
-			}
-			e := event.UpdateEvent{
-				ObjectOld: tc.old,
-				ObjectNew: tc.new,
-			}
-			require.Equal(t, tc.expected, p.Update(e))
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			p := RefreshRequested{}
+			require.Equal(t, tt.want, p.Update(event.UpdateEvent{
+				ObjectOld: tt.oldObject,
+				ObjectNew: tt.newObject,
+			}))
+		})
+	}
+}
+
+func TestReverifyRequested_Update(t *testing.T) {
+	tests := []struct {
+		name      string
+		oldObject client.Object
+		newObject client.Object
+		want      bool
+	}{
+		{
+			name:      "no old or new object",
+			oldObject: nil,
+			newObject: nil,
+			want:      false,
+		},
+		{
+			name:      "no old object",
+			oldObject: nil,
+			newObject: &kargoapi.Stage{
+				ObjectMeta: metav1.ObjectMeta{
+					Annotations: map[string]string{
+						kargoapi.AnnotationKeyReverify: "foo",
+					},
+				},
+			},
+			want: false,
+		},
+		{
+			name: "no new object",
+			oldObject: &kargoapi.Stage{
+				ObjectMeta: metav1.ObjectMeta{
+					Annotations: map[string]string{
+						kargoapi.AnnotationKeyReverify: "foo",
+					},
+				},
+			},
+			newObject: nil,
+			want:      false,
+		},
+		{
+			name: "no reverify annotation",
+			oldObject: &kargoapi.Stage{
+				ObjectMeta: metav1.ObjectMeta{
+					Annotations: map[string]string{},
+				},
+			},
+			newObject: &kargoapi.Stage{
+				ObjectMeta: metav1.ObjectMeta{
+					Annotations: map[string]string{
+						"other": "annotation",
+					},
+				},
+			},
+			want: false,
+		},
+		{
+			name: "reverify annotation set on new object",
+			oldObject: &kargoapi.Stage{
+				ObjectMeta: metav1.ObjectMeta{
+					Annotations: map[string]string{},
+				},
+			},
+			newObject: &kargoapi.Stage{
+				ObjectMeta: metav1.ObjectMeta{
+					Annotations: map[string]string{
+						kargoapi.AnnotationKeyReverify: "foo",
+					},
+				},
+			},
+			want: true,
+		},
+		{
+			name: "reverify annotation removed from new object",
+			oldObject: &kargoapi.Stage{
+				ObjectMeta: metav1.ObjectMeta{
+					Annotations: map[string]string{
+						kargoapi.AnnotationKeyReverify: "foo",
+					},
+				},
+			},
+			newObject: &kargoapi.Stage{
+				ObjectMeta: metav1.ObjectMeta{
+					Annotations: map[string]string{},
+				},
+			},
+			want: false,
+		},
+		{
+			name: "empty reverify annotation value",
+			oldObject: &kargoapi.Stage{
+				ObjectMeta: metav1.ObjectMeta{
+					Annotations: map[string]string{
+						kargoapi.AnnotationKeyReverify: (&kargoapi.VerificationRequest{
+							ID: "foo",
+						}).String(),
+					},
+				},
+			},
+			newObject: &kargoapi.Stage{
+				ObjectMeta: metav1.ObjectMeta{
+					Annotations: map[string]string{
+						kargoapi.AnnotationKeyAbort: "",
+					},
+				},
+			},
+			want: false,
+		},
+		{
+			name: "reverify annotation ID changed",
+			oldObject: &kargoapi.Stage{
+				ObjectMeta: metav1.ObjectMeta{
+					Annotations: map[string]string{
+						kargoapi.AnnotationKeyReverify: (&kargoapi.VerificationRequest{
+							ID: "foo",
+						}).String(),
+					},
+				},
+			},
+			newObject: &kargoapi.Stage{
+				ObjectMeta: metav1.ObjectMeta{
+					Annotations: map[string]string{
+						kargoapi.AnnotationKeyReverify: (&kargoapi.VerificationRequest{
+							ID: "bar",
+						}).String(),
+					},
+				},
+			},
+			want: true,
+		},
+		{
+			name: "reverify annotation actor changed with same ID",
+			oldObject: &kargoapi.Stage{
+				ObjectMeta: metav1.ObjectMeta{
+					Annotations: map[string]string{
+						kargoapi.AnnotationKeyReverify: (&kargoapi.VerificationRequest{
+							ID:    "foo",
+							Actor: "fake-actor",
+						}).String(),
+					},
+				},
+			},
+			newObject: &kargoapi.Stage{
+				ObjectMeta: metav1.ObjectMeta{
+					Annotations: map[string]string{
+						kargoapi.AnnotationKeyReverify: (&kargoapi.VerificationRequest{
+							ID:    "foo",
+							Actor: "real-actor",
+						}).String(),
+					},
+				},
+			},
+			want: false,
+		},
+		{
+			name: "reverify annotation ID equal",
+			oldObject: &kargoapi.Stage{
+				ObjectMeta: metav1.ObjectMeta{
+					Annotations: map[string]string{
+						kargoapi.AnnotationKeyReverify: (&kargoapi.VerificationRequest{
+							ID: "foo",
+						}).String(),
+					},
+				},
+			},
+			newObject: &kargoapi.Stage{
+				ObjectMeta: metav1.ObjectMeta{
+					Annotations: map[string]string{
+						kargoapi.AnnotationKeyReverify: (&kargoapi.VerificationRequest{
+							ID: "foo",
+						}).String(),
+					},
+				},
+			},
+			want: false,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			p := ReverifyRequested{}
+			require.Equal(t, tt.want, p.Update(event.UpdateEvent{
+				ObjectOld: tt.oldObject,
+				ObjectNew: tt.newObject,
+			}))
+		})
+	}
+}
+
+func TestAbortRequested_Update(t *testing.T) {
+	tests := []struct {
+		name      string
+		oldObject client.Object
+		newObject client.Object
+		want      bool
+	}{
+		{
+			name:      "no old or new object",
+			oldObject: nil,
+			newObject: nil,
+			want:      false,
+		},
+		{
+			name:      "no old object",
+			oldObject: nil,
+			newObject: &kargoapi.Stage{
+				ObjectMeta: metav1.ObjectMeta{
+					Annotations: map[string]string{
+						kargoapi.AnnotationKeyAbort: "foo",
+					},
+				},
+			},
+			want: false,
+		},
+		{
+			name: "no new object",
+			oldObject: &kargoapi.Stage{
+				ObjectMeta: metav1.ObjectMeta{
+					Annotations: map[string]string{
+						kargoapi.AnnotationKeyAbort: "foo",
+					},
+				},
+			},
+			newObject: nil,
+			want:      false,
+		},
+		{
+			name: "no abort annotation",
+			oldObject: &kargoapi.Stage{
+				ObjectMeta: metav1.ObjectMeta{
+					Annotations: map[string]string{},
+				},
+			},
+			newObject: &kargoapi.Stage{
+				ObjectMeta: metav1.ObjectMeta{
+					Annotations: map[string]string{
+						"other": "annotation",
+					},
+				},
+			},
+			want: false,
+		},
+		{
+			name: "abort annotation set on new object",
+			oldObject: &kargoapi.Stage{
+				ObjectMeta: metav1.ObjectMeta{
+					Annotations: map[string]string{},
+				},
+			},
+			newObject: &kargoapi.Stage{
+				ObjectMeta: metav1.ObjectMeta{
+					Annotations: map[string]string{
+						kargoapi.AnnotationKeyAbort: "foo",
+					},
+				},
+			},
+			want: true,
+		},
+		{
+			name: "abort annotation removed from new object",
+			oldObject: &kargoapi.Stage{
+				ObjectMeta: metav1.ObjectMeta{
+					Annotations: map[string]string{
+						kargoapi.AnnotationKeyAbort: "foo",
+					},
+				},
+			},
+			newObject: &kargoapi.Stage{
+				ObjectMeta: metav1.ObjectMeta{
+					Annotations: map[string]string{},
+				},
+			},
+			want: false,
+		},
+		{
+			name: "empty abort annotation value",
+			oldObject: &kargoapi.Stage{
+				ObjectMeta: metav1.ObjectMeta{
+					Annotations: map[string]string{
+						kargoapi.AnnotationKeyAbort: (&kargoapi.VerificationRequest{
+							ID: "foo",
+						}).String(),
+					},
+				},
+			},
+			newObject: &kargoapi.Stage{
+				ObjectMeta: metav1.ObjectMeta{
+					Annotations: map[string]string{
+						kargoapi.AnnotationKeyAbort: "",
+					},
+				},
+			},
+			want: false,
+		},
+		{
+			name: "abort annotation ID changed",
+			oldObject: &kargoapi.Stage{
+				ObjectMeta: metav1.ObjectMeta{
+					Annotations: map[string]string{
+						kargoapi.AnnotationKeyAbort: (&kargoapi.VerificationRequest{
+							ID: "foo",
+						}).String(),
+					},
+				},
+			},
+			newObject: &kargoapi.Stage{
+				ObjectMeta: metav1.ObjectMeta{
+					Annotations: map[string]string{
+						kargoapi.AnnotationKeyAbort: (&kargoapi.VerificationRequest{
+							ID: "bar",
+						}).String(),
+					},
+				},
+			},
+			want: true,
+		},
+		{
+			name: "abort annotation actor changed with same ID",
+			oldObject: &kargoapi.Stage{
+				ObjectMeta: metav1.ObjectMeta{
+					Annotations: map[string]string{
+						kargoapi.AnnotationKeyAbort: (&kargoapi.VerificationRequest{
+							ID:    "foo",
+							Actor: "fake-actor",
+						}).String(),
+					},
+				},
+			},
+			newObject: &kargoapi.Stage{
+				ObjectMeta: metav1.ObjectMeta{
+					Annotations: map[string]string{
+						kargoapi.AnnotationKeyAbort: (&kargoapi.VerificationRequest{
+							ID:    "foo",
+							Actor: "real-actor",
+						}).String(),
+					},
+				},
+			},
+			want: false,
+		},
+		{
+			name: "abort annotation ID equal",
+			oldObject: &kargoapi.Stage{
+				ObjectMeta: metav1.ObjectMeta{
+					Annotations: map[string]string{
+						kargoapi.AnnotationKeyAbort: (&kargoapi.VerificationRequest{
+							ID: "foo",
+						}).String(),
+					},
+				},
+			},
+			newObject: &kargoapi.Stage{
+				ObjectMeta: metav1.ObjectMeta{
+					Annotations: map[string]string{
+						kargoapi.AnnotationKeyAbort: (&kargoapi.VerificationRequest{
+							ID: "foo",
+						}).String(),
+					},
+				},
+			},
+			want: false,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			p := AbortRequested{}
+			require.Equal(t, tt.want, p.Update(event.UpdateEvent{
+				ObjectOld: tt.oldObject,
+				ObjectNew: tt.newObject,
+			}))
 		})
 	}
 }
diff --git a/internal/webhook/stage/webhook_test.go b/internal/webhook/stage/webhook_test.go
index 7528c146..5a01938e 100644
--- a/internal/webhook/stage/webhook_test.go
+++ b/internal/webhook/stage/webhook_test.go
@@ -118,7 +118,7 @@ func TestDefault(t *testing.T) {
 			},
 		},
 		{
-			name: "set reverify actor when request doesn't come from kargo controlplane",
+			name: "set reverify actor when request doesn't come from kargo control plane",
 			webhook: &webhook{
 				admissionRequestFromContextFn: admission.RequestFromContext,
 				isRequestFromKargoControlplaneFn: func(admission.Request) bool {
@@ -147,15 +147,15 @@ func TestDefault(t *testing.T) {
 			assertions: func(t *testing.T, stage *kargoapi.Stage, err error) {
 				require.NoError(t, err)
 				require.Contains(t, stage.Annotations, kargoapi.AnnotationKeyReverify)
-				require.Equal(t, "fake-id", stage.Annotations[kargoapi.AnnotationKeyReverify])
-				require.Contains(t, stage.Annotations, kargoapi.AnnotationKeyReverifyActor)
-				require.Equal(
-					t,
-					kargoapi.FormatEventKubernetesUserActor(authnv1.UserInfo{
+				rr, ok := kargoapi.ReverifyAnnotationValue(stage.Annotations)
+				require.True(t, ok)
+				require.Equal(t, &kargoapi.VerificationRequest{
+					ID: "fake-id",
+					Actor: kargoapi.FormatEventKubernetesUserActor(authnv1.UserInfo{
 						Username: "real-user",
 					}),
-					stage.Annotations[kargoapi.AnnotationKeyReverifyActor],
-				)
+					ControlPlane: false,
+				}, rr)
 			},
 		},
 		{
@@ -180,8 +180,10 @@ func TestDefault(t *testing.T) {
 			stage: &kargoapi.Stage{
 				ObjectMeta: metav1.ObjectMeta{
 					Annotations: map[string]string{
-						kargoapi.AnnotationKeyReverify:      "fake-id",
-						kargoapi.AnnotationKeyReverifyActor: "fake-user",
+						kargoapi.AnnotationKeyReverify: (&kargoapi.VerificationRequest{
+							ID:    "fake-id",
+							Actor: "fake-user",
+						}).String(),
 					},
 				},
 				Spec: &kargoapi.StageSpec{},
@@ -189,18 +191,363 @@ func TestDefault(t *testing.T) {
 			assertions: func(t *testing.T, stage *kargoapi.Stage, err error) {
 				require.NoError(t, err)
 				require.Contains(t, stage.Annotations, kargoapi.AnnotationKeyReverify)
-				require.Equal(t, "fake-id", stage.Annotations[kargoapi.AnnotationKeyReverify])
-				require.Equal(
-					t,
-					kargoapi.FormatEventKubernetesUserActor(authnv1.UserInfo{
+				rr, ok := kargoapi.ReverifyAnnotationValue(stage.Annotations)
+				require.True(t, ok)
+				require.Equal(t, &kargoapi.VerificationRequest{
+					ID: "fake-id",
+					Actor: kargoapi.FormatEventKubernetesUserActor(authnv1.UserInfo{
 						Username: "real-user",
 					}),
-					stage.Annotations[kargoapi.AnnotationKeyReverifyActor],
-				)
+					ControlPlane: false,
+				}, rr)
+			},
+		},
+		{
+			name: "do not overwrite reverify actor when request comes from control plane",
+			webhook: &webhook{
+				admissionRequestFromContextFn: admission.RequestFromContext,
+				isRequestFromKargoControlplaneFn: func(admission.Request) bool {
+					return true
+				},
+			},
+			req: admission.Request{
+				AdmissionRequest: admissionv1.AdmissionRequest{
+					Operation: admissionv1.Update,
+					UserInfo: authnv1.UserInfo{
+						Username: "control-plane-user",
+					},
+					OldObject: runtime.RawExtension{
+						Object: &kargoapi.Stage{},
+					},
+				},
+			},
+			stage: &kargoapi.Stage{
+				ObjectMeta: metav1.ObjectMeta{
+					Annotations: map[string]string{
+						kargoapi.AnnotationKeyReverify: (&kargoapi.VerificationRequest{
+							ID:    "fake-id",
+							Actor: kargoapi.EventActorAdmin,
+						}).String(),
+					},
+				},
+				Spec: &kargoapi.StageSpec{},
+			},
+			assertions: func(t *testing.T, stage *kargoapi.Stage, err error) {
+				require.NoError(t, err)
+				require.Contains(t, stage.Annotations, kargoapi.AnnotationKeyReverify)
+				rr, ok := kargoapi.ReverifyAnnotationValue(stage.Annotations)
+				require.True(t, ok)
+				require.Equal(t, &kargoapi.VerificationRequest{
+					ID:           "fake-id",
+					Actor:        kargoapi.EventActorAdmin,
+					ControlPlane: true,
+				}, rr)
+			},
+		},
+		{
+			name: "overwite reverify actor when it has changed for the same ID",
+			webhook: &webhook{
+				admissionRequestFromContextFn: admission.RequestFromContext,
+				isRequestFromKargoControlplaneFn: func(admission.Request) bool {
+					return false
+				},
+			},
+			req: admission.Request{
+				AdmissionRequest: admissionv1.AdmissionRequest{
+					Operation: admissionv1.Update,
+					UserInfo: authnv1.UserInfo{
+						Username: "real-user",
+					},
+					OldObject: runtime.RawExtension{
+						Object: &kargoapi.Stage{
+							ObjectMeta: metav1.ObjectMeta{
+								Annotations: map[string]string{
+									kargoapi.AnnotationKeyReverify: (&kargoapi.VerificationRequest{
+										ID:    "fake-id",
+										Actor: "fake-user",
+									}).String(),
+								},
+							},
+						},
+					},
+				},
+			},
+			stage: &kargoapi.Stage{
+				ObjectMeta: metav1.ObjectMeta{
+					Annotations: map[string]string{
+						kargoapi.AnnotationKeyReverify: (&kargoapi.VerificationRequest{
+							ID:    "fake-id",
+							Actor: "illegitimate-user",
+						}).String(),
+					},
+				},
+				Spec: &kargoapi.StageSpec{},
+			},
+			assertions: func(t *testing.T, stage *kargoapi.Stage, err error) {
+				require.NoError(t, err)
+				require.Contains(t, stage.Annotations, kargoapi.AnnotationKeyReverify)
+				rr, ok := kargoapi.ReverifyAnnotationValue(stage.Annotations)
+				require.True(t, ok)
+				require.Equal(t, &kargoapi.VerificationRequest{
+					ID:    "fake-id",
+					Actor: kargoapi.FormatEventKubernetesUserActor(authnv1.UserInfo{Username: "real-user"}),
+				}, rr)
+			},
+		},
+		{
+			name: "overwrite reverify control plane flag when it has changed for the same ID",
+			webhook: &webhook{
+				admissionRequestFromContextFn: admission.RequestFromContext,
+				isRequestFromKargoControlplaneFn: func(admission.Request) bool {
+					return false
+				},
+			},
+			req: admission.Request{
+				AdmissionRequest: admissionv1.AdmissionRequest{
+					Operation: admissionv1.Update,
+					UserInfo: authnv1.UserInfo{
+						Username: "real-user",
+					},
+					OldObject: runtime.RawExtension{
+						Object: &kargoapi.Stage{
+							ObjectMeta: metav1.ObjectMeta{
+								Annotations: map[string]string{
+									kargoapi.AnnotationKeyReverify: (&kargoapi.VerificationRequest{
+										ID:    "fake-id",
+										Actor: "fake-user",
+									}).String(),
+								},
+							},
+						},
+					},
+				},
+			},
+			stage: &kargoapi.Stage{
+				ObjectMeta: metav1.ObjectMeta{
+					Annotations: map[string]string{
+						kargoapi.AnnotationKeyReverify: (&kargoapi.VerificationRequest{
+							ID:           "fake-id",
+							Actor:        "fake-user",
+							ControlPlane: true,
+						}).String(),
+					},
+				},
+				Spec: &kargoapi.StageSpec{},
+			},
+			assertions: func(t *testing.T, stage *kargoapi.Stage, err error) {
+				require.NoError(t, err)
+				require.Contains(t, stage.Annotations, kargoapi.AnnotationKeyReverify)
+				rr, ok := kargoapi.ReverifyAnnotationValue(stage.Annotations)
+				require.True(t, ok)
+				require.Equal(t, &kargoapi.VerificationRequest{
+					ID:           "fake-id",
+					Actor:        kargoapi.FormatEventKubernetesUserActor(authnv1.UserInfo{Username: "real-user"}),
+					ControlPlane: false,
+				}, rr)
+			},
+		},
+		{
+			name: "ignore empty reverify annotation",
+			webhook: &webhook{
+				admissionRequestFromContextFn: admission.RequestFromContext,
+				isRequestFromKargoControlplaneFn: func(admission.Request) bool {
+					return false
+				},
+			},
+			req: admission.Request{
+				AdmissionRequest: admissionv1.AdmissionRequest{
+					Operation: admissionv1.Update,
+					UserInfo: authnv1.UserInfo{
+						Username: "real-user",
+					},
+				},
+			},
+			stage: &kargoapi.Stage{
+				ObjectMeta: metav1.ObjectMeta{
+					Annotations: map[string]string{
+						kargoapi.AnnotationKeyReverify: "",
+					},
+				},
+				Spec: &kargoapi.StageSpec{},
+			},
+			assertions: func(t *testing.T, stage *kargoapi.Stage, err error) {
+				require.NoError(t, err)
+				v, ok := stage.Annotations[kargoapi.AnnotationKeyReverify]
+				require.True(t, ok)
+				require.Empty(t, v)
+			},
+		},
+		{
+			name: "ignore reverify annotation with empty ID",
+			webhook: &webhook{
+				admissionRequestFromContextFn: admission.RequestFromContext,
+				isRequestFromKargoControlplaneFn: func(admission.Request) bool {
+					return false
+				},
+			},
+			req: admission.Request{
+				AdmissionRequest: admissionv1.AdmissionRequest{
+					Operation: admissionv1.Update,
+					UserInfo: authnv1.UserInfo{
+						Username: "real-user",
+					},
+				},
+			},
+			stage: &kargoapi.Stage{
+				ObjectMeta: metav1.ObjectMeta{
+					Annotations: map[string]string{
+						kargoapi.AnnotationKeyReverify: (&kargoapi.VerificationRequest{
+							ID: "",
+						}).String(),
+					},
+				},
+				Spec: &kargoapi.StageSpec{},
+			},
+			assertions: func(t *testing.T, stage *kargoapi.Stage, err error) {
+				require.NoError(t, err)
+				v, ok := stage.Annotations[kargoapi.AnnotationKeyReverify]
+				require.True(t, ok)
+				require.Equal(t, (&kargoapi.VerificationRequest{
+					ID: "",
+				}).String(), v)
+			},
+		},
+		{
+			name: "ignore unchanged reverify annotation",
+			webhook: &webhook{
+				admissionRequestFromContextFn: admission.RequestFromContext,
+				isRequestFromKargoControlplaneFn: func(admission.Request) bool {
+					return false
+				},
+			},
+			req: admission.Request{
+				AdmissionRequest: admissionv1.AdmissionRequest{
+					Operation: admissionv1.Update,
+					UserInfo: authnv1.UserInfo{
+						Username: "real-user",
+					},
+					OldObject: runtime.RawExtension{
+						Object: &kargoapi.Stage{
+							ObjectMeta: metav1.ObjectMeta{
+								Annotations: map[string]string{
+									kargoapi.AnnotationKeyReverify: (&kargoapi.VerificationRequest{
+										ID:    "fake-id",
+										Actor: "fake-user",
+									}).String(),
+								},
+							},
+						},
+					},
+				},
+			},
+			stage: &kargoapi.Stage{
+				ObjectMeta: metav1.ObjectMeta{
+					Annotations: map[string]string{
+						kargoapi.AnnotationKeyReverify: (&kargoapi.VerificationRequest{
+							ID:    "fake-id",
+							Actor: "fake-user",
+						}).String(),
+					},
+				},
+				Spec: &kargoapi.StageSpec{},
+			},
+			assertions: func(t *testing.T, stage *kargoapi.Stage, err error) {
+				require.NoError(t, err)
+				v, ok := stage.Annotations[kargoapi.AnnotationKeyReverify]
+				require.True(t, ok)
+				require.Equal(t, (&kargoapi.VerificationRequest{
+					ID:    "fake-id",
+					Actor: "fake-user",
+				}).String(), v)
+			},
+		},
+		{
+			name: "set abort actor when request doesn't come from kargo control plane",
+			webhook: &webhook{
+				admissionRequestFromContextFn: admission.RequestFromContext,
+				isRequestFromKargoControlplaneFn: func(admission.Request) bool {
+					return false
+				},
+			},
+			req: admission.Request{
+				AdmissionRequest: admissionv1.AdmissionRequest{
+					Operation: admissionv1.Update,
+					UserInfo: authnv1.UserInfo{
+						Username: "real-user",
+					},
+					OldObject: runtime.RawExtension{
+						Object: &kargoapi.Stage{},
+					},
+				},
+			},
+			stage: &kargoapi.Stage{
+				ObjectMeta: metav1.ObjectMeta{
+					Annotations: map[string]string{
+						kargoapi.AnnotationKeyAbort: "fake-id",
+					},
+				},
+				Spec: &kargoapi.StageSpec{},
+			},
+			assertions: func(t *testing.T, stage *kargoapi.Stage, err error) {
+				require.NoError(t, err)
+				require.Contains(t, stage.Annotations, kargoapi.AnnotationKeyAbort)
+				rr, ok := kargoapi.AbortAnnotationValue(stage.Annotations)
+				require.True(t, ok)
+				require.Equal(t, &kargoapi.VerificationRequest{
+					ID: "fake-id",
+					Actor: kargoapi.FormatEventKubernetesUserActor(authnv1.UserInfo{
+						Username: "real-user",
+					}),
+					ControlPlane: false,
+				}, rr)
 			},
 		},
 		{
-			name: "always clear reverify actor annotation when reverify annotation removed",
+			name: "overwrite with admission request user info if abort actor annotation exists",
+			webhook: &webhook{
+				admissionRequestFromContextFn: admission.RequestFromContext,
+				isRequestFromKargoControlplaneFn: func(admission.Request) bool {
+					return false
+				},
+			},
+			req: admission.Request{
+				AdmissionRequest: admissionv1.AdmissionRequest{
+					Operation: admissionv1.Update,
+					UserInfo: authnv1.UserInfo{
+						Username: "real-user",
+					},
+					OldObject: runtime.RawExtension{
+						Object: &kargoapi.Stage{},
+					},
+				},
+			},
+			stage: &kargoapi.Stage{
+				ObjectMeta: metav1.ObjectMeta{
+					Annotations: map[string]string{
+						kargoapi.AnnotationKeyAbort: (&kargoapi.VerificationRequest{
+							ID:    "fake-id",
+							Actor: "fake-user",
+						}).String(),
+					},
+				},
+				Spec: &kargoapi.StageSpec{},
+			},
+			assertions: func(t *testing.T, stage *kargoapi.Stage, err error) {
+				require.NoError(t, err)
+				require.Contains(t, stage.Annotations, kargoapi.AnnotationKeyAbort)
+				rr, ok := kargoapi.AbortAnnotationValue(stage.Annotations)
+				require.True(t, ok)
+				require.Equal(t, &kargoapi.VerificationRequest{
+					ID: "fake-id",
+					Actor: kargoapi.FormatEventKubernetesUserActor(authnv1.UserInfo{
+						Username: "real-user",
+					}),
+					ControlPlane: false,
+				}, rr)
+			},
+		},
+		{
+			name: "do not overwrite abort actor when request comes from control plane",
 			webhook: &webhook{
 				admissionRequestFromContextFn: admission.RequestFromContext,
 				isRequestFromKargoControlplaneFn: func(admission.Request) bool {
@@ -210,12 +557,227 @@ func TestDefault(t *testing.T) {
 			req: admission.Request{
 				AdmissionRequest: admissionv1.AdmissionRequest{
 					Operation: admissionv1.Update,
+					UserInfo: authnv1.UserInfo{
+						Username: "control-plane-user",
+					},
+					OldObject: runtime.RawExtension{
+						Object: &kargoapi.Stage{},
+					},
+				},
+			},
+			stage: &kargoapi.Stage{
+				ObjectMeta: metav1.ObjectMeta{
+					Annotations: map[string]string{
+						kargoapi.AnnotationKeyAbort: (&kargoapi.VerificationRequest{
+							ID:    "fake-id",
+							Actor: kargoapi.EventActorAdmin,
+						}).String(),
+					},
+				},
+				Spec: &kargoapi.StageSpec{},
+			},
+			assertions: func(t *testing.T, stage *kargoapi.Stage, err error) {
+				require.NoError(t, err)
+				require.Contains(t, stage.Annotations, kargoapi.AnnotationKeyAbort)
+				rr, ok := kargoapi.AbortAnnotationValue(stage.Annotations)
+				require.True(t, ok)
+				require.Equal(t, &kargoapi.VerificationRequest{
+					ID:           "fake-id",
+					Actor:        kargoapi.EventActorAdmin,
+					ControlPlane: true,
+				}, rr)
+			},
+		},
+		{
+			name: "overwite abort actor when it has changed for the same ID",
+			webhook: &webhook{
+				admissionRequestFromContextFn: admission.RequestFromContext,
+				isRequestFromKargoControlplaneFn: func(admission.Request) bool {
+					return false
+				},
+			},
+			req: admission.Request{
+				AdmissionRequest: admissionv1.AdmissionRequest{
+					Operation: admissionv1.Update,
+					UserInfo: authnv1.UserInfo{
+						Username: "real-user",
+					},
+					OldObject: runtime.RawExtension{
+						Object: &kargoapi.Stage{
+							ObjectMeta: metav1.ObjectMeta{
+								Annotations: map[string]string{
+									kargoapi.AnnotationKeyAbort: (&kargoapi.VerificationRequest{
+										ID:    "fake-id",
+										Actor: "fake-user",
+									}).String(),
+								},
+							},
+						},
+					},
+				},
+			},
+			stage: &kargoapi.Stage{
+				ObjectMeta: metav1.ObjectMeta{
+					Annotations: map[string]string{
+						kargoapi.AnnotationKeyAbort: (&kargoapi.VerificationRequest{
+							ID:    "fake-id",
+							Actor: "illegitimate-user",
+						}).String(),
+					},
+				},
+				Spec: &kargoapi.StageSpec{},
+			},
+			assertions: func(t *testing.T, stage *kargoapi.Stage, err error) {
+				require.NoError(t, err)
+				require.Contains(t, stage.Annotations, kargoapi.AnnotationKeyAbort)
+				rr, ok := kargoapi.AbortAnnotationValue(stage.Annotations)
+				require.True(t, ok)
+				require.Equal(t, &kargoapi.VerificationRequest{
+					ID:    "fake-id",
+					Actor: kargoapi.FormatEventKubernetesUserActor(authnv1.UserInfo{Username: "real-user"}),
+				}, rr)
+			},
+		},
+		{
+			name: "overwrite abort control plane flag when it has changed for the same ID",
+			webhook: &webhook{
+				admissionRequestFromContextFn: admission.RequestFromContext,
+				isRequestFromKargoControlplaneFn: func(admission.Request) bool {
+					return false
+				},
+			},
+			req: admission.Request{
+				AdmissionRequest: admissionv1.AdmissionRequest{
+					Operation: admissionv1.Update,
+					UserInfo: authnv1.UserInfo{
+						Username: "real-user",
+					},
+					OldObject: runtime.RawExtension{
+						Object: &kargoapi.Stage{
+							ObjectMeta: metav1.ObjectMeta{
+								Annotations: map[string]string{
+									kargoapi.AnnotationKeyAbort: (&kargoapi.VerificationRequest{
+										ID:    "fake-id",
+										Actor: "fake-user",
+									}).String(),
+								},
+							},
+						},
+					},
+				},
+			},
+			stage: &kargoapi.Stage{
+				ObjectMeta: metav1.ObjectMeta{
+					Annotations: map[string]string{
+						kargoapi.AnnotationKeyAbort: (&kargoapi.VerificationRequest{
+							ID:           "fake-id",
+							Actor:        "fake-user",
+							ControlPlane: true,
+						}).String(),
+					},
+				},
+				Spec: &kargoapi.StageSpec{},
+			},
+			assertions: func(t *testing.T, stage *kargoapi.Stage, err error) {
+				require.NoError(t, err)
+				require.Contains(t, stage.Annotations, kargoapi.AnnotationKeyAbort)
+				rr, ok := kargoapi.AbortAnnotationValue(stage.Annotations)
+				require.True(t, ok)
+				require.Equal(t, &kargoapi.VerificationRequest{
+					ID:           "fake-id",
+					Actor:        kargoapi.FormatEventKubernetesUserActor(authnv1.UserInfo{Username: "real-user"}),
+					ControlPlane: false,
+				}, rr)
+			},
+		},
+		{
+			name: "ignore empty abort annotation",
+			webhook: &webhook{
+				admissionRequestFromContextFn: admission.RequestFromContext,
+				isRequestFromKargoControlplaneFn: func(admission.Request) bool {
+					return false
+				},
+			},
+			req: admission.Request{
+				AdmissionRequest: admissionv1.AdmissionRequest{
+					Operation: admissionv1.Update,
+					UserInfo: authnv1.UserInfo{
+						Username: "real-user",
+					},
+				},
+			},
+			stage: &kargoapi.Stage{
+				ObjectMeta: metav1.ObjectMeta{
+					Annotations: map[string]string{
+						kargoapi.AnnotationKeyAbort: "",
+					},
+				},
+				Spec: &kargoapi.StageSpec{},
+			},
+			assertions: func(t *testing.T, stage *kargoapi.Stage, err error) {
+				require.NoError(t, err)
+				v, ok := stage.Annotations[kargoapi.AnnotationKeyAbort]
+				require.True(t, ok)
+				require.Empty(t, v)
+			},
+		},
+		{
+			name: "ignore abort annotation with empty ID",
+			webhook: &webhook{
+				admissionRequestFromContextFn: admission.RequestFromContext,
+				isRequestFromKargoControlplaneFn: func(admission.Request) bool {
+					return false
+				},
+			},
+			req: admission.Request{
+				AdmissionRequest: admissionv1.AdmissionRequest{
+					Operation: admissionv1.Update,
+					UserInfo: authnv1.UserInfo{
+						Username: "real-user",
+					},
+				},
+			},
+			stage: &kargoapi.Stage{
+				ObjectMeta: metav1.ObjectMeta{
+					Annotations: map[string]string{
+						kargoapi.AnnotationKeyAbort: (&kargoapi.VerificationRequest{
+							ID: "",
+						}).String(),
+					},
+				},
+				Spec: &kargoapi.StageSpec{},
+			},
+			assertions: func(t *testing.T, stage *kargoapi.Stage, err error) {
+				require.NoError(t, err)
+				v, ok := stage.Annotations[kargoapi.AnnotationKeyAbort]
+				require.True(t, ok)
+				require.Equal(t, (&kargoapi.VerificationRequest{
+					ID: "",
+				}).String(), v)
+			},
+		},
+		{
+			name: "ignore unchanged abort annotation",
+			webhook: &webhook{
+				admissionRequestFromContextFn: admission.RequestFromContext,
+				isRequestFromKargoControlplaneFn: func(admission.Request) bool {
+					return false
+				},
+			},
+			req: admission.Request{
+				AdmissionRequest: admissionv1.AdmissionRequest{
+					Operation: admissionv1.Update,
+					UserInfo: authnv1.UserInfo{
+						Username: "real-user",
+					},
 					OldObject: runtime.RawExtension{
 						Object: &kargoapi.Stage{
 							ObjectMeta: metav1.ObjectMeta{
 								Annotations: map[string]string{
-									kargoapi.AnnotationKeyReverify:      "fake-id",
-									kargoapi.AnnotationKeyReverifyActor: "real-user",
+									kargoapi.AnnotationKeyAbort: (&kargoapi.VerificationRequest{
+										ID:    "fake-id",
+										Actor: "fake-user",
+									}).String(),
 								},
 							},
 						},
@@ -223,12 +785,24 @@ func TestDefault(t *testing.T) {
 				},
 			},
 			stage: &kargoapi.Stage{
+				ObjectMeta: metav1.ObjectMeta{
+					Annotations: map[string]string{
+						kargoapi.AnnotationKeyAbort: (&kargoapi.VerificationRequest{
+							ID:    "fake-id",
+							Actor: "fake-user",
+						}).String(),
+					},
+				},
 				Spec: &kargoapi.StageSpec{},
 			},
 			assertions: func(t *testing.T, stage *kargoapi.Stage, err error) {
 				require.NoError(t, err)
-				require.NotContains(t, stage.Annotations, kargoapi.AnnotationKeyReverify)
-				require.NotContains(t, stage.Annotations, kargoapi.AnnotationKeyReverifyActor)
+				v, ok := stage.Annotations[kargoapi.AnnotationKeyAbort]
+				require.True(t, ok)
+				require.Equal(t, (&kargoapi.VerificationRequest{
+					ID:    "fake-id",
+					Actor: "fake-user",
+				}).String(), v)
 			},
 		},
 	}