Repository: kopia/kopia
Pull request #3260 (2023-09-01 00:30:02)

chore(ci): add OSSF Scorecard workflow
See [workflow results](https://github.com/kopia/kopia/security/code-scanning?query=pr%3A3260+is%3Aopen)

codecov[bot]: ## [Codecov](https://app.codecov.io/gh/kopia/kopia/pull/3260?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kopia) Report
Patch coverage has no change and project coverage change: **`-0.01%`** :warning:
> Comparison is base [(`1320f0c`)](https://app.codecov.io/gh/kopia/kopia/commit/1320f0c8e856ec856ad0b82c52ceed5483a5196d?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kopia) 75.52% compared to head [(`23e5523`)](https://app.codecov.io/gh/kopia/kopia/pull/3260?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kopia) 75.51%.

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


```diff
@@            Coverage Diff             @@
##           master    #3260      +/-   ##
==========================================
- Coverage   75.52%   75.51%   -0.01%     
==========================================
  Files         460      460              
  Lines       36641    36641              
==========================================
- Hits        27674    27671       -3     
+ Misses       7050     7048       -2     
- Partials     1917     1922       +5     
```


[see 7 files with indirect coverage changes](https://app.codecov.io/gh/kopia/kopia/pull/3260/indirect-changes?src=pr&el=tree-more&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kopia)


</details>

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

github-code-scanning[bot]: This pull request sets up GitHub code scanning for this repository. Once the scans have completed and the checks have passed, the analysis results for this pull request branch will appear on [this overview](/kopia/kopia/security/code-scanning?query=pr%3A3260+is%3Aopen). Once you merge this pull request, the 'Security' tab will show more code scanning analysis results (for example, for the default branch). Depending on your configuration and choice of analysis tool, future pull requests will be annotated with code scanning analysis results. For more information about GitHub code scanning, check out [the documentation](https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/about-code-scanning). 

Patch:
diff --git a/.github/workflows/auto-merge.yml b/.github/workflows/auto-merge.yml
index 665ef373..08f3b2d7 100644
--- a/.github/workflows/auto-merge.yml
+++ b/.github/workflows/auto-merge.yml
@@ -7,7 +7,7 @@ jobs:
   auto-merge:
     runs-on: ubuntu-latest
     steps:
-      - uses: actions/checkout@v3
+      - uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4.0.0
       - uses: ahmadnassri/action-dependabot-auto-merge@v2
         with:
           # auto-merge rules are in /.github/auto-merge.yml
diff --git a/.github/workflows/code-coverage.yml b/.github/workflows/code-coverage.yml
index 123b7bba..ed537e99 100644
--- a/.github/workflows/code-coverage.yml
+++ b/.github/workflows/code-coverage.yml
@@ -12,7 +12,7 @@ jobs:
     runs-on: ubuntu-latest
     steps:
     - name: Check out repository
-      uses: actions/checkout@v3
+      uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4.0.0
       with:
         fetch-depth: 0
     - name: Set up Go
diff --git a/.github/workflows/dependency-review.yml b/.github/workflows/dependency-review.yml
index b0dedc42..e935407e 100644
--- a/.github/workflows/dependency-review.yml
+++ b/.github/workflows/dependency-review.yml
@@ -15,6 +15,6 @@ jobs:
     runs-on: ubuntu-latest
     steps:
       - name: 'Checkout Repository'
-        uses: actions/checkout@v3
+        uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4.0.0
       - name: 'Dependency Review'
         uses: actions/dependency-review-action@v3
diff --git a/.github/workflows/license-check.yml b/.github/workflows/license-check.yml
index 65998743..d40235ea 100644
--- a/.github/workflows/license-check.yml
+++ b/.github/workflows/license-check.yml
@@ -12,7 +12,7 @@ jobs:
     runs-on: ubuntu-latest
     steps:
     - name: Check out repository
-      uses: actions/checkout@v3
+      uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4.0.0
       with:
         fetch-depth: 0
     - name: Set up Go
diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml
index bc370e80..da83d5a0 100644
--- a/.github/workflows/lint.yml
+++ b/.github/workflows/lint.yml
@@ -26,7 +26,7 @@ jobs:
     runs-on: ${{ matrix.os }}
     steps:
     - name: Check out repository
-      uses: actions/checkout@v3
+      uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4.0.0
       with:
         fetch-depth: 0
     - name: Set up Go
diff --git a/.github/workflows/make.yml b/.github/workflows/make.yml
index 1b9c827b..c68d69fc 100644
--- a/.github/workflows/make.yml
+++ b/.github/workflows/make.yml
@@ -40,7 +40,7 @@ jobs:
     continue-on-error: ${{ contains(matrix.os, 'self-hosted') }}
     steps:
     - name: Check out repository
-      uses: actions/checkout@v3
+      uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4.0.0
       with:
         fetch-depth: 0
     - name: Set up Go
@@ -137,7 +137,7 @@ jobs:
     needs: build
     if: github.event_name != 'pull_request' && github.repository == 'kopia/kopia'
     steps:
-    - uses: actions/checkout@v3
+    - uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4.0.0
     - name: Set up QEMU
       uses: docker/setup-qemu-action@v2
     - name: Set up Docker Buildx
diff --git a/.github/workflows/ossf-scorecard.yml b/.github/workflows/ossf-scorecard.yml
new file mode 100644
index 00000000..a6925049
--- /dev/null
+++ b/.github/workflows/ossf-scorecard.yml
@@ -0,0 +1,51 @@
+name: OSSF Scorecard
+on:
+  # For Branch-Protection check. Only the default branch is supported. See
+  # https://github.com/ossf/scorecard/blob/main/docs/checks.md#branch-protection
+  branch_protection_rule:
+  push:
+    branches: [ "master" ]
+  # To guarantee Maintained check is occasionally updated. See
+  # https://github.com/ossf/scorecard/blob/main/docs/checks.md#maintained
+  schedule:
+    - cron: '37 19 * * 5'
+
+# Declare default permissions as read only.
+permissions: read-all
+
+jobs:
+  analysis:
+    name: Scorecard analysis
+    runs-on: ubuntu-latest
+    permissions:
+      # Needed to upload the results to code-scanning dashboard.
+      security-events: write
+      # Needed to publish results and get a badge
+      id-token: write
+
+    steps:
+      -
+        name: "Checkout repo"
+        uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8 # v3.1.0
+        with:
+          persist-credentials: false
+      -
+        name: "Run analysis"
+        uses: ossf/scorecard-action@e38b1902ae4f44df626f11ba0734b14fb91f8f86 # v2.1.2
+        with:
+          results_file: results.sarif
+          results_format: sarif
+          publish_results: true
+      -
+        # Upload the results to GitHub's code scanning dashboard.
+        name: "Upload to results to dashboard"
+        uses: github/codeql-action/upload-sarif@17573ee1cc1b9d061760f3a006fc4aac4f944fd5 # v2.2.4
+        with:
+          sarif_file: results.sarif
+      -
+        name: "Upload analysis results as 'Job Artifact'"
+        uses: actions/upload-artifact@3cea5372237819ed00197afe530f5a7ea3e805c8 # v3.1.0
+        with:
+          name: SARIF file
+          path: results.sarif
+          retention-days: 5
diff --git a/.github/workflows/providers-core.yml b/.github/workflows/providers-core.yml
index 6aca7161..acbbe875 100644
--- a/.github/workflows/providers-core.yml
+++ b/.github/workflows/providers-core.yml
@@ -24,7 +24,7 @@ jobs:
     runs-on: ubuntu-latest
     steps:
     - name: Check out repository
-      uses: actions/checkout@v3
+      uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4.0.0
       with:
         fetch-depth: 0
         ref: ${{ github.event.inputs.ref_name || github.ref }}
diff --git a/.github/workflows/providers-extra.yml b/.github/workflows/providers-extra.yml
index 4e9384fc..dcad66f5 100644
--- a/.github/workflows/providers-extra.yml
+++ b/.github/workflows/providers-extra.yml
@@ -24,7 +24,7 @@ jobs:
     runs-on: ubuntu-latest
     steps:
     - name: Check out repository
-      uses: actions/checkout@v3
+      uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4.0.0
       with:
         fetch-depth: 0
         ref: ${{ github.event.inputs.ref_name || github.ref }}
diff --git a/.github/workflows/race-detector.yml b/.github/workflows/race-detector.yml
index ee93a736..53a0a664 100644
--- a/.github/workflows/race-detector.yml
+++ b/.github/workflows/race-detector.yml
@@ -12,7 +12,7 @@ jobs:
     runs-on: ubuntu-latest
     steps:
     - name: Check out repository
-      uses: actions/checkout@v3
+      uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4.0.0
       with:
         fetch-depth: 0
     - name: Set up Go
diff --git a/app/package-lock.json b/app/package-lock.json
index 1002c362..d9cdddff 100644
--- a/app/package-lock.json
+++ b/app/package-lock.json
@@ -19,13 +19,13 @@
       },
       "devDependencies": {
         "@electron/notarize": "^2.1.0",
-        "@playwright/test": "^1.36.2",
+        "@playwright/test": "^1.37.1",
         "asar": "^3.2.0",
-        "concurrently": "^8.2.0",
+        "concurrently": "^8.2.1",
         "dotenv": "^16.3.1",
-        "electron": "^25.3.2",
+        "electron": "^26.1.0",
         "electron-builder": "^24.6.3",
-        "playwright": "^1.36.2",
+        "playwright": "^1.37.1",
         "playwright-core": "^1.35.1"
       }
     },
@@ -352,13 +352,13 @@
       }
     },
     "node_modules/@playwright/test": {
-      "version": "1.36.2",
-      "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.36.2.tgz",
-      "integrity": "sha512-2rVZeyPRjxfPH6J0oGJqE8YxiM1IBRyM8hyrXYK7eSiAqmbNhxwcLa7dZ7fy9Kj26V7FYia5fh9XJRq4Dqme+g==",
+      "version": "1.37.1",
+      "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.37.1.tgz",
+      "integrity": "sha512-bq9zTli3vWJo8S3LwB91U0qDNQDpEXnw7knhxLM0nwDvexQAwx9tO8iKDZSqqneVq+URd/WIoz+BALMqUTgdSg==",
       "dev": true,
       "dependencies": {
         "@types/node": "*",
-        "playwright-core": "1.36.2"
+        "playwright-core": "1.37.1"
       },
       "bin": {
         "playwright": "cli.js"
@@ -1176,9 +1176,9 @@
       "dev": true
     },
     "node_modules/concurrently": {
-      "version": "8.2.0",
-      "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-8.2.0.tgz",
-      "integrity": "sha512-nnLMxO2LU492mTUj9qX/az/lESonSZu81UznYDoXtz1IQf996ixVqPAgHXwvHiHCAef/7S8HIK+fTFK7Ifk8YA==",
+      "version": "8.2.1",
+      "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-8.2.1.tgz",
+      "integrity": "sha512-nVraf3aXOpIcNud5pB9M82p1tynmZkrSGQ1p6X/VY8cJ+2LMVqAgXsJxYYefACSHbTYlm92O1xuhdGTjwoEvbQ==",
       "dev": true,
       "dependencies": {
         "chalk": "^4.1.2",
@@ -1490,9 +1490,9 @@
       }
     },
     "node_modules/electron": {
-      "version": "25.3.2",
-      "resolved": "https://registry.npmjs.org/electron/-/electron-25.3.2.tgz",
-      "integrity": "sha512-xiktJvXraaE/ARf2OVHFyTze1TksSbsbJgOaBtdIiBvUduez6ipATEPIec8Msz1n6eQ+xqYb6YF8tDuIZtJSPw==",
+      "version": "26.1.0",
+      "resolved": "https://registry.npmjs.org/electron/-/electron-26.1.0.tgz",
+      "integrity": "sha512-qEh19H09Pysn3ibms5nZ0haIh5pFoOd7/5Ww7gzmAwDQOulRi8Sa2naeueOyIb1GKpf+6L4ix3iceYRAuA5r5Q==",
       "dev": true,
       "hasInstallScript": true,
       "dependencies": {
@@ -2537,13 +2537,13 @@
       "dev": true
     },
     "node_modules/playwright": {
-      "version": "1.36.2",
-      "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.36.2.tgz",
-      "integrity": "sha512-4Fmlq3KWsl85Bl4InJw1NC21aeQV0iSZuFvTDcy1F8zVmXmgQRe89GxF8zMSRt/KIS+2tUolak7EXVl9aC+JdA==",
+      "version": "1.37.1",
+      "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.37.1.tgz",
+      "integrity": "sha512-bgUXRrQKhT48zHdxDYQTpf//0xDfDd5hLeEhjuSw8rXEGoT9YeElpfvs/izonTNY21IQZ7d3s22jLxYaAnubbQ==",
       "dev": true,
       "hasInstallScript": true,
       "dependencies": {
-        "playwright-core": "1.36.2"
+        "playwright-core": "1.37.1"
       },
       "bin": {
         "playwright": "cli.js"
@@ -2553,9 +2553,9 @@
       }
     },
     "node_modules/playwright-core": {
-      "version": "1.36.2",
-      "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.36.2.tgz",
-      "integrity": "sha512-sQYZt31dwkqxOrP7xy2ggDfEzUxM1lodjhsQ3NMMv5uGTRDsLxU0e4xf4wwMkF2gplIxf17QMBCodSFgm6bFVQ==",
+      "version": "1.37.1",
+      "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.37.1.tgz",
+      "integrity": "sha512-17EuQxlSIYCmEMwzMqusJ2ztDgJePjrbttaefgdsiqeLWidjYz9BxXaTaZWxH1J95SHGk6tjE+dwgWILJoUZfA==",
       "dev": true,
       "bin": {
         "playwright-core": "cli.js"
@@ -3518,14 +3518,14 @@
       }
     },
     "@playwright/test": {
-      "version": "1.36.2",
-      "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.36.2.tgz",
-      "integrity": "sha512-2rVZeyPRjxfPH6J0oGJqE8YxiM1IBRyM8hyrXYK7eSiAqmbNhxwcLa7dZ7fy9Kj26V7FYia5fh9XJRq4Dqme+g==",
+      "version": "1.37.1",
+      "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.37.1.tgz",
+      "integrity": "sha512-bq9zTli3vWJo8S3LwB91U0qDNQDpEXnw7knhxLM0nwDvexQAwx9tO8iKDZSqqneVq+URd/WIoz+BALMqUTgdSg==",
       "dev": true,
       "requires": {
         "@types/node": "*",
         "fsevents": "2.3.2",
-        "playwright-core": "1.36.2"
+        "playwright-core": "1.37.1"
       }
     },
     "@sindresorhus/is": {
@@ -4168,9 +4168,9 @@
       "dev": true
     },
     "concurrently": {
-      "version": "8.2.0",
-      "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-8.2.0.tgz",
-      "integrity": "sha512-nnLMxO2LU492mTUj9qX/az/lESonSZu81UznYDoXtz1IQf996ixVqPAgHXwvHiHCAef/7S8HIK+fTFK7Ifk8YA==",
+      "version": "8.2.1",
+      "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-8.2.1.tgz",
+      "integrity": "sha512-nVraf3aXOpIcNud5pB9M82p1tynmZkrSGQ1p6X/VY8cJ+2LMVqAgXsJxYYefACSHbTYlm92O1xuhdGTjwoEvbQ==",
       "dev": true,
       "requires": {
         "chalk": "^4.1.2",
@@ -4396,9 +4396,9 @@
       }
     },
     "electron": {
-      "version": "25.3.2",
-      "resolved": "https://registry.npmjs.org/electron/-/electron-25.3.2.tgz",
-      "integrity": "sha512-xiktJvXraaE/ARf2OVHFyTze1TksSbsbJgOaBtdIiBvUduez6ipATEPIec8Msz1n6eQ+xqYb6YF8tDuIZtJSPw==",
+      "version": "26.1.0",
+      "resolved": "https://registry.npmjs.org/electron/-/electron-26.1.0.tgz",
+      "integrity": "sha512-qEh19H09Pysn3ibms5nZ0haIh5pFoOd7/5Ww7gzmAwDQOulRi8Sa2naeueOyIb1GKpf+6L4ix3iceYRAuA5r5Q==",
       "dev": true,
       "requires": {
         "@electron/get": "^2.0.0",
@@ -5202,18 +5202,18 @@
       "dev": true
     },
     "playwright": {
-      "version": "1.36.2",
-      "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.36.2.tgz",
-      "integrity": "sha512-4Fmlq3KWsl85Bl4InJw1NC21aeQV0iSZuFvTDcy1F8zVmXmgQRe89GxF8zMSRt/KIS+2tUolak7EXVl9aC+JdA==",
+      "version": "1.37.1",
+      "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.37.1.tgz",
+      "integrity": "sha512-bgUXRrQKhT48zHdxDYQTpf//0xDfDd5hLeEhjuSw8rXEGoT9YeElpfvs/izonTNY21IQZ7d3s22jLxYaAnubbQ==",
       "dev": true,
       "requires": {
-        "playwright-core": "1.36.2"
+        "playwright-core": "1.37.1"
       }
     },
     "playwright-core": {
-      "version": "1.36.2",
-      "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.36.2.tgz",
-      "integrity": "sha512-sQYZt31dwkqxOrP7xy2ggDfEzUxM1lodjhsQ3NMMv5uGTRDsLxU0e4xf4wwMkF2gplIxf17QMBCodSFgm6bFVQ==",
+      "version": "1.37.1",
+      "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.37.1.tgz",
+      "integrity": "sha512-17EuQxlSIYCmEMwzMqusJ2ztDgJePjrbttaefgdsiqeLWidjYz9BxXaTaZWxH1J95SHGk6tjE+dwgWILJoUZfA==",
       "dev": true
     },
     "plist": {
diff --git a/app/package.json b/app/package.json
index 3943fc16..422fdef2 100644
--- a/app/package.json
+++ b/app/package.json
@@ -111,13 +111,13 @@
   },
   "devDependencies": {
     "@electron/notarize": "^2.1.0",
-    "@playwright/test": "^1.36.2",
+    "@playwright/test": "^1.37.1",
     "asar": "^3.2.0",
-    "concurrently": "^8.2.0",
+    "concurrently": "^8.2.1",
     "dotenv": "^16.3.1",
-    "electron": "^25.3.2",
+    "electron": "^26.1.0",
     "electron-builder": "^24.6.3",
-    "playwright": "^1.36.2",
+    "playwright": "^1.37.1",
     "playwright-core": "^1.35.1"
   },
   "homepage": "./",
diff --git a/cli/command_acl_add.go b/cli/command_acl_add.go
index a7048ffb..63471024 100644
--- a/cli/command_acl_add.go
+++ b/cli/command_acl_add.go
@@ -11,9 +11,10 @@
 )
 
 type commandACLAdd struct {
-	user   string
-	target string
-	level  string
+	user      string
+	target    string
+	level     string
+	overwrite bool
 }
 
 func (c *commandACLAdd) setup(svc appServices, parent commandParent) {
@@ -21,6 +22,7 @@ func (c *commandACLAdd) setup(svc appServices, parent commandParent) {
 	cmd.Flag("user", "User the ACL targets").Required().StringVar(&c.user)
 	cmd.Flag("target", "Manifests targeted by the rule (type:T,key1:value1,...,keyN:valueN)").Required().StringVar(&c.target)
 	cmd.Flag("access", "Access the user gets to subject").Required().EnumVar(&c.level, acl.SupportedAccessLevels()...)
+	cmd.Flag("overwrite", "Overwrite existing rule with the same user and target").BoolVar(&c.overwrite)
 	cmd.Action(svc.repositoryWriterAction(c.run))
 }
 
@@ -47,5 +49,5 @@ func (c *commandACLAdd) run(ctx context.Context, rep repo.RepositoryWriter) erro
 		Access: al,
 	}
 
-	return errors.Wrap(acl.AddACL(ctx, rep, e), "error adding ACL entry")
+	return errors.Wrap(acl.AddACL(ctx, rep, e, c.overwrite), "error adding ACL entry")
 }
diff --git a/cli/command_acl_enable.go b/cli/command_acl_enable.go
index d7905d1a..16942552 100644
--- a/cli/command_acl_enable.go
+++ b/cli/command_acl_enable.go
@@ -41,7 +41,7 @@ func (c *commandACLEnable) run(ctx context.Context, rep repo.RepositoryWriter) e
 	}
 
 	for _, e := range auth.DefaultACLs {
-		if err := acl.AddACL(ctx, rep, e); err != nil {
+		if err := acl.AddACL(ctx, rep, e, false); err != nil {
 			return errors.Wrap(err, "unable to add default ACL")
 		}
 	}
diff --git a/cli/command_server_start.go b/cli/command_server_start.go
index 7b6d1ceb..72c5841c 100644
--- a/cli/command_server_start.go
+++ b/cli/command_server_start.go
@@ -62,6 +62,7 @@ type commandServerStart struct {
 	uiTitlePrefix                       string
 	uiPreferencesFile                   string
 	asyncRepoConnect                    bool
+	persistentLogs                      bool
 
 	shutdownGracePeriod time.Duration
 
@@ -108,6 +109,7 @@ func (c *commandServerStart) setup(svc advancedAppServices, parent commandParent
 	cmd.Flag("tls-print-server-cert", "Print server certificate").Hidden().BoolVar(&c.serverStartTLSPrintFullServerCert)
 
 	cmd.Flag("async-repo-connect", "Connect to repository asynchronously").Hidden().BoolVar(&c.asyncRepoConnect)
+	cmd.Flag("persistent-logs", "Persist logs in a file").Default("true").BoolVar(&c.persistentLogs)
 	cmd.Flag("ui-title-prefix", "UI title prefix").Hidden().Envar(svc.EnvName("KOPIA_UI_TITLE_PREFIX")).StringVar(&c.uiTitlePrefix)
 	cmd.Flag("ui-preferences-file", "Path to JSON file storing UI preferences").StringVar(&c.uiPreferencesFile)
 
@@ -149,6 +151,7 @@ func (c *commandServerStart) serverStartOptions(ctx context.Context) (*server.Op
 		PasswordPersist:      c.svc.passwordPersistenceStrategy(),
 		UIPreferencesFile:    uiPreferencesFile,
 		UITitlePrefix:        c.uiTitlePrefix,
+		PersistentLogs:       c.persistentLogs,
 
 		DisableCSRFTokenChecks: c.disableCSRFTokenChecks,
 	}, nil
diff --git a/cli/command_snapshot_expire.go b/cli/command_snapshot_expire.go
index c9b1cb45..b2d8d4cd 100644
--- a/cli/command_snapshot_expire.go
+++ b/cli/command_snapshot_expire.go
@@ -71,10 +71,6 @@ func (c *commandSnapshotExpire) run(ctx context.Context, rep repo.RepositoryWrit
 			log(ctx).Infof("Deleted %v snapshots of %v...", len(deleted), src)
 		} else {
 			log(ctx).Infof("%v snapshot(s) of %v would be deleted. Pass --delete to do it.", len(deleted), src)
-
-			for _, it := range deleted {
-				log(ctx).Infof("  %v", formatTimestamp(it.StartTime.ToTime()))
-			}
 		}
 	}
 
diff --git a/cli/observability_flags.go b/cli/observability_flags.go
index 65cb501a..18be8fa3 100644
--- a/cli/observability_flags.go
+++ b/cli/observability_flags.go
@@ -18,7 +18,7 @@
 	"github.com/prometheus/client_golang/prometheus/push"
 	"github.com/prometheus/common/expfmt"
 	"go.opentelemetry.io/otel"
-	"go.opentelemetry.io/otel/exporters/jaeger"
+	"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc"
 	"go.opentelemetry.io/otel/sdk/resource"
 	"go.opentelemetry.io/otel/sdk/trace"
 	semconv "go.opentelemetry.io/otel/semconv/v1.4.0"
@@ -54,6 +54,7 @@ type observabilityFlags struct {
 	outputFilePrefix    string
 
 	enableJaeger bool
+	otlpTrace    bool
 
 	stopPusher chan struct{}
 	pusherWG   sync.WaitGroup
@@ -73,7 +74,9 @@ func (c *observabilityFlags) setup(svc appServices, app *kingpin.Application) {
 	app.Flag("metrics-push-username", "Username for push gateway").Envar(svc.EnvName("KOPIA_METRICS_PUSH_USERNAME")).Hidden().StringVar(&c.metricsPushUsername)
 	app.Flag("metrics-push-password", "Password for push gateway").Envar(svc.EnvName("KOPIA_METRICS_PUSH_PASSWORD")).Hidden().StringVar(&c.metricsPushPassword)
 
-	app.Flag("enable-jaeger-collector", "Emit OpenTelemetry traces to Jaeger collector").Hidden().Envar(svc.EnvName("KOPIA_ENABLE_JAEGER_COLLECTOR")).BoolVar(&c.enableJaeger)
+	// tracing (OTLP) parameters
+	app.Flag("enable-jaeger-collector", "(DEPRECATED) Emit OpenTelemetry traces to Jaeger collector").Hidden().Envar(svc.EnvName("KOPIA_ENABLE_JAEGER_COLLECTOR")).BoolVar(&c.enableJaeger)
+	app.Flag("otlp-trace", "Send OpenTelemetry traces to OTLP collector using gRPC").Hidden().Envar(svc.EnvName("KOPIA_ENABLE_OTLP_TRACE")).BoolVar(&c.otlpTrace)
 
 	var formats []string
 
@@ -123,7 +126,7 @@ func (c *observabilityFlags) startMetrics(ctx context.Context) error {
 		}
 	}
 
-	return c.maybeStartTraceExporter()
+	return c.maybeStartTraceExporter(ctx)
 }
 
 // Starts observability listener when a listener address is specified.
@@ -191,34 +194,37 @@ func (c *observabilityFlags) maybeStartMetricsPusher(ctx context.Context) error
 	return nil
 }
 
-func (c *observabilityFlags) maybeStartTraceExporter() error {
-	if !c.enableJaeger {
-		return nil
+func (c *observabilityFlags) maybeStartTraceExporter(ctx context.Context) error {
+	if c.enableJaeger {
+		return errors.Errorf("Flag '--enable-jaeger-collector' is no longer supported, use '--otlp' instead. See https://github.com/kopia/kopia/pull/3264 for more information")
 	}
 
-	// Create the Jaeger exporter
-	se, err := jaeger.New(jaeger.WithCollectorEndpoint())
-	if err != nil {
-		return errors.Wrap(err, "unable to create Jaeger exporter")
+	if !c.otlpTrace {
+		return nil
 	}
 
-	if se != nil {
-		r := resource.NewWithAttributes(
-			semconv.SchemaURL,
-			semconv.ServiceNameKey.String("kopia"),
-			semconv.ServiceVersionKey.String(repo.BuildVersion),
-		)
+	// Create the OTLP exporter.
+	se := otlptracegrpc.NewUnstarted()
 
-		tp := trace.NewTracerProvider(
-			trace.WithBatcher(se),
-			trace.WithResource(r),
-		)
+	r := resource.NewWithAttributes(
+		semconv.SchemaURL,
+		semconv.ServiceNameKey.String("kopia"),
+		semconv.ServiceVersionKey.String(repo.BuildVersion),
+	)
 
-		otel.SetTracerProvider(tp)
+	tp := trace.NewTracerProvider(
+		trace.WithBatcher(se),
+		trace.WithResource(r),
+	)
 
-		c.traceProvider = tp
+	if err := se.Start(ctx); err != nil {
+		return errors.Wrap(err, "unable to start OTLP exporter")
 	}
 
+	otel.SetTracerProvider(tp)
+
+	c.traceProvider = tp
+
 	return nil
 }
 
diff --git a/go.mod b/go.mod
index 7ab54541..187e83ee 100644
--- a/go.mod
+++ b/go.mod
@@ -20,19 +20,19 @@ require (
 	github.com/gofrs/flock v0.8.1
 	github.com/golang-jwt/jwt/v4 v4.5.0
 	github.com/golang/protobuf v1.5.3
-	github.com/google/fswalker v0.3.0
+	github.com/google/fswalker v0.3.2
 	github.com/google/go-cmp v0.5.9
 	github.com/google/uuid v1.3.1
 	github.com/gorilla/mux v1.8.0
-	github.com/hanwen/go-fuse/v2 v2.3.0
+	github.com/hanwen/go-fuse/v2 v2.4.0
 	github.com/hashicorp/cronexpr v1.1.2
 	github.com/klauspost/compress v1.16.7
 	github.com/klauspost/pgzip v1.2.6
 	github.com/klauspost/reedsolomon v1.11.8
-	github.com/kopia/htmluibuild v0.0.0-20230716183504-d78b44b3a9bd
+	github.com/kopia/htmluibuild v0.0.1-0.20230905013549-27f219e6d165
 	github.com/kylelemons/godebug v1.1.0
 	github.com/mattn/go-colorable v0.1.13
-	github.com/minio/minio-go/v7 v7.0.62
+	github.com/minio/minio-go/v7 v7.0.63
 	github.com/natefinch/atomic v1.0.1
 	github.com/pierrec/lz4 v2.6.1+incompatible
 	github.com/pkg/errors v0.9.1
@@ -48,10 +48,10 @@ require (
 	github.com/tg123/go-htpasswd v1.2.1
 	github.com/zalando/go-keyring v0.2.3
 	github.com/zeebo/blake3 v0.2.3
-	go.opentelemetry.io/otel v1.16.0
-	go.opentelemetry.io/otel/exporters/jaeger v1.16.0
-	go.opentelemetry.io/otel/sdk v1.16.0
-	go.opentelemetry.io/otel/trace v1.16.0
+	go.opentelemetry.io/otel v1.17.0
+	go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.17.0
+	go.opentelemetry.io/otel/sdk v1.17.0
+	go.opentelemetry.io/otel/trace v1.17.0
 	go.uber.org/multierr v1.11.0
 	go.uber.org/zap v1.25.0
 	golang.org/x/crypto v0.12.0
@@ -60,9 +60,9 @@ require (
 	golang.org/x/net v0.14.0
 	golang.org/x/oauth2 v0.11.0
 	golang.org/x/sync v0.3.0
-	golang.org/x/sys v0.11.0
-	golang.org/x/term v0.11.0
-	golang.org/x/text v0.12.0
+	golang.org/x/sys v0.12.0
+	golang.org/x/term v0.12.0
+	golang.org/x/text v0.13.0
 	google.golang.org/api v0.138.0
 	google.golang.org/grpc v1.57.0
 	google.golang.org/protobuf v1.31.0
@@ -81,6 +81,7 @@ require (
 	github.com/GehirnInc/crypt v0.0.0-20230320061759-8cc1b52080c5 // indirect
 	github.com/alessio/shellescape v1.4.1 // indirect
 	github.com/beorn7/perks v1.0.1 // indirect
+	github.com/cenkalti/backoff/v4 v4.2.1 // indirect
 	github.com/cespare/xxhash/v2 v2.2.0 // indirect
 	github.com/chromedp/sysutil v1.0.0 // indirect
 	github.com/danieljoos/wincred v1.2.0 // indirect
@@ -102,6 +103,7 @@ require (
 	github.com/google/s2a-go v0.1.5 // indirect
 	github.com/googleapis/enterprise-certificate-proxy v0.2.5 // indirect
 	github.com/googleapis/gax-go/v2 v2.12.0 // indirect
+	github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0 // indirect
 	github.com/josharian/intern v1.0.0 // indirect
 	github.com/json-iterator/go v1.1.12 // indirect
 	github.com/klauspost/cpuid/v2 v2.2.5 // indirect
@@ -122,7 +124,9 @@ require (
 	github.com/sirupsen/logrus v1.9.3 // indirect
 	github.com/xhit/go-str2duration/v2 v2.1.0 // indirect
 	go.opencensus.io v0.24.0 // indirect
-	go.opentelemetry.io/otel/metric v1.16.0 // indirect
+	go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.17.0 // indirect
+	go.opentelemetry.io/otel/metric v1.17.0 // indirect
+	go.opentelemetry.io/proto/otlp v1.0.0 // indirect
 	golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect
 	google.golang.org/appengine v1.6.7 // indirect
 	google.golang.org/genproto v0.0.0-20230803162519-f966b187b2e5 // indirect
diff --git a/go.sum b/go.sum
index 4de9ee60..7f28ac5e 100644
--- a/go.sum
+++ b/go.sum
@@ -51,6 +51,8 @@ github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kd
 github.com/benbjohnson/clock v1.3.0 h1:ip6w0uFQkncKQ979AypyG0ER7mqUSBdKLOgAle/AT8A=
 github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
 github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
+github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM=
+github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
 github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
 github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
 github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
@@ -146,8 +148,8 @@ github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaS
 github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
 github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
 github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
-github.com/google/fswalker v0.3.0 h1:5zUaE2hPPnTpk+yFrueUz8Vk38EcPSF4vc52tBPvyik=
-github.com/google/fswalker v0.3.0/go.mod h1:ZSEBqY0IHKqWPeAbTyvccv9bb9vCnaQfHe31cm911Ng=
+github.com/google/fswalker v0.3.2 h1:cVUOO7Ct5kb4YFzmxirZKQSfCyTWEe7e6eBvta9h61Y=
+github.com/google/fswalker v0.3.2/go.mod h1:ZSEBqY0IHKqWPeAbTyvccv9bb9vCnaQfHe31cm911Ng=
 github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
 github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
 github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
@@ -179,8 +181,10 @@ github.com/googleapis/gax-go/v2 v2.12.0/go.mod h1:y+aIqrI5eb1YGMVJfuV3185Ts/D7qK
 github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
 github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
 github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
-github.com/hanwen/go-fuse/v2 v2.3.0 h1:t5ivNIH2PK+zw4OBul/iJjsoG9K6kXo4nMDoBpciC8A=
-github.com/hanwen/go-fuse/v2 v2.3.0/go.mod h1:xKwi1cF7nXAOBCXujD5ie0ZKsxc8GGSA1rlMJc+8IJs=
+github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0 h1:YBftPWNWd4WwGqtY2yeZL2ef8rHAxPBD8KFhJpmcqms=
+github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0/go.mod h1:YN5jB8ie0yfIUg6VvR9Kz84aCaG7AsGZnLjhHbUqwPg=
+github.com/hanwen/go-fuse/v2 v2.4.0 h1:12OhD7CkXXQdvxG2osIdBQLdXh+nmLXY9unkUIe/xaU=
+github.com/hanwen/go-fuse/v2 v2.4.0/go.mod h1:xKwi1cF7nXAOBCXujD5ie0ZKsxc8GGSA1rlMJc+8IJs=
 github.com/hashicorp/cronexpr v1.1.2 h1:wG/ZYIKT+RT3QkOdgYc+xsKWVRgnxJ1OJtjjy84fJ9A=
 github.com/hashicorp/cronexpr v1.1.2/go.mod h1:P4wA0KBl9C5q2hABiMO7cp6jcIg96CDh1Efb3g1PWA4=
 github.com/ianlancetaylor/demangle v0.0.0-20210905161508-09a460cdf81d/go.mod h1:aYm2/VgdVmcIU8iMfdMvDMsRAQjcfZSKFby6HOFvi/w=
@@ -198,8 +202,8 @@ github.com/klauspost/pgzip v1.2.6 h1:8RXeL5crjEUFnR2/Sn6GJNWtSQ3Dk8pq4CL3jvdDyjU
 github.com/klauspost/pgzip v1.2.6/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs=
 github.com/klauspost/reedsolomon v1.11.8 h1:s8RpUW5TK4hjr+djiOpbZJB4ksx+TdYbRH7vHQpwPOY=
 github.com/klauspost/reedsolomon v1.11.8/go.mod h1:4bXRN+cVzMdml6ti7qLouuYi32KHJ5MGv0Qd8a47h6A=
-github.com/kopia/htmluibuild v0.0.0-20230716183504-d78b44b3a9bd h1:Vskpc00T65HkkDSWbkiXOG5yYsgWg5LN48daUfGZ+u0=
-github.com/kopia/htmluibuild v0.0.0-20230716183504-d78b44b3a9bd/go.mod h1:eWer4rx9P8lJo2eKc+Q7AZ1dE1x1hJNdkbDFPzMu1Hw=
+github.com/kopia/htmluibuild v0.0.1-0.20230905013549-27f219e6d165 h1:xk6P5PQJeR7S33tORd0tDPx42lcreLbRtLQFiVVG0ys=
+github.com/kopia/htmluibuild v0.0.1-0.20230905013549-27f219e6d165/go.mod h1:cSImbrlwvv2phvj5RfScL2v08ghX6xli0PcK6f+t8S0=
 github.com/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8=
 github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
 github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
@@ -228,8 +232,8 @@ github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zk
 github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4=
 github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34=
 github.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEpN+20RM=
-github.com/minio/minio-go/v7 v7.0.62 h1:qNYsFZHEzl+NfH8UxW4jpmlKav1qUAgfY30YNRneVhc=
-github.com/minio/minio-go/v7 v7.0.62/go.mod h1:Q6X7Qjb7WMhvG65qKf4gUgA5XaiSox74kR1uAEjxRS4=
+github.com/minio/minio-go/v7 v7.0.63 h1:GbZ2oCvaUdgT5640WJOpyDhhDxvknAJU2/T3yurwcbQ=
+github.com/minio/minio-go/v7 v7.0.63/go.mod h1:Q6X7Qjb7WMhvG65qKf4gUgA5XaiSox74kR1uAEjxRS4=
 github.com/minio/sha256-simd v1.0.1 h1:6kaan5IFmwTNynnKKpDHe6FWHohJOHhCPchzK49dzMM=
 github.com/minio/sha256-simd v1.0.1/go.mod h1:Pz6AKMiUdngCLpeTL/RJY1M9rUuPMYujV5xJjtbRSN8=
 github.com/moby/sys/mountinfo v0.6.2 h1:BzJjoreD5BMFNmD9Rus6gdd1pLuecOFPt8wC+Vygl78=
@@ -309,18 +313,22 @@ github.com/zeebo/pcg v1.0.1 h1:lyqfGeWiv4ahac6ttHs+I5hwtH/+1mrhlCtVNQM2kHo=
 github.com/zeebo/pcg v1.0.1/go.mod h1:09F0S9iiKrwn9rlI5yjLkmrug154/YRW6KnnXVDM/l4=
 go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
 go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
-go.opentelemetry.io/otel v1.16.0 h1:Z7GVAX/UkAXPKsy94IU+i6thsQS4nb7LviLpnaNeW8s=
-go.opentelemetry.io/otel v1.16.0/go.mod h1:vl0h9NUa1D5s1nv3A5vZOYWn8av4K8Ml6JDeHrT/bx4=
-go.opentelemetry.io/otel/exporters/jaeger v1.16.0 h1:YhxxmXZ011C0aDZKoNw+juVWAmEfv/0W2XBOv9aHTaA=
-go.opentelemetry.io/otel/exporters/jaeger v1.16.0/go.mod h1:grYbBo/5afWlPpdPZYhyn78Bk04hnvxn2+hvxQhKIQM=
-go.opentelemetry.io/otel/metric v1.16.0 h1:RbrpwVG1Hfv85LgnZ7+txXioPDoh6EdbZHo26Q3hqOo=
-go.opentelemetry.io/otel/metric v1.16.0/go.mod h1:QE47cpOmkwipPiefDwo2wDzwJrlfxxNYodqc4xnGCo4=
-go.opentelemetry.io/otel/sdk v1.16.0 h1:Z1Ok1YsijYL0CSJpHt4cS3wDDh7p572grzNrBMiMWgE=
-go.opentelemetry.io/otel/sdk v1.16.0/go.mod h1:tMsIuKXuuIWPBAOrH+eHtvhTL+SntFtXF9QD68aP6p4=
-go.opentelemetry.io/otel/trace v1.16.0 h1:8JRpaObFoW0pxuVPapkgH8UhHQj+bJW8jJsCZEu5MQs=
-go.opentelemetry.io/otel/trace v1.16.0/go.mod h1:Yt9vYq1SdNz3xdjZZK7wcXv1qv2pwLkqr2QVwea0ef0=
+go.opentelemetry.io/otel v1.17.0 h1:MW+phZ6WZ5/uk2nd93ANk/6yJ+dVrvNWUjGhnnFU5jM=
+go.opentelemetry.io/otel v1.17.0/go.mod h1:I2vmBGtFaODIVMBSTPVDlJSzBDNf93k60E6Ft0nyjo0=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.17.0 h1:U5GYackKpVKlPrd/5gKMlrTlP2dCESAAFU682VCpieY=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.17.0/go.mod h1:aFsJfCEnLzEu9vRRAcUiB/cpRTbVsNdF3OHSPpdjxZQ=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.17.0 h1:iGeIsSYwpYSvh5UGzWrJfTDJvPjrXtxl3GUppj6IXQU=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.17.0/go.mod h1:1j3H3G1SBYpZFti6OI4P0uRQCW20MXkG5v4UWXppLLE=
+go.opentelemetry.io/otel/metric v1.17.0 h1:iG6LGVz5Gh+IuO0jmgvpTB6YVrCGngi8QGm+pMd8Pdc=
+go.opentelemetry.io/otel/metric v1.17.0/go.mod h1:h4skoxdZI17AxwITdmdZjjYJQH5nzijUUjm+wtPph5o=
+go.opentelemetry.io/otel/sdk v1.17.0 h1:FLN2X66Ke/k5Sg3V623Q7h7nt3cHXaW1FOvKKrW0IpE=
+go.opentelemetry.io/otel/sdk v1.17.0/go.mod h1:U87sE0f5vQB7hwUoW98pW5Rz4ZDuCFBZFNUBlSgmDFQ=
+go.opentelemetry.io/otel/trace v1.17.0 h1:/SWhSRHmDPOImIAetP1QAeMnZYiQXrTy4fMMYOdSKWQ=
+go.opentelemetry.io/otel/trace v1.17.0/go.mod h1:I/4vKTgFclIsXRVucpH25X0mpFSczM7aHeaz0ZBLWjY=
 go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI=
-go.uber.org/goleak v1.2.0 h1:xqgm/S+aQvhWFTtR0XK3Jvg7z8kGV8P4X14IzwN3Eqk=
+go.opentelemetry.io/proto/otlp v1.0.0 h1:T0TX0tmXU8a3CbNXzEKGeU5mIVOdf0oykP+u2lIVU/I=
+go.opentelemetry.io/proto/otlp v1.0.0/go.mod h1:Sy6pihPLfYHkr3NkUbEhGHFhINUSI/v80hjKIs5JXpM=
+go.uber.org/goleak v1.2.1 h1:NBol2c7O1ZokfZ0LEU9K6Whx/KnwvepVetCUhtKja4A=
 go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
 go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
 go.uber.org/zap v1.25.0 h1:4Hvk6GtkucQ790dqmj7l1eEnRdKm3k3ZUrUMS2d5+5c=
@@ -392,13 +400,13 @@ golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBc
 golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.11.0 h1:eG7RXZHdqOJ1i+0lgLgCpSXAp6M3LYlAo6osgSi0xOM=
-golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o=
+golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
 golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
 golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
-golang.org/x/term v0.11.0 h1:F9tnn/DA/Im8nCwm+fX+1/eBwi4qFjRT++MhtVC4ZX0=
-golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU=
+golang.org/x/term v0.12.0 h1:/ZfYdc3zq+q02Rv9vGqTeSItdzZTSNDmfTi0mBAuidU=
+golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
 golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
@@ -406,8 +414,8 @@ golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
 golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
 golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
 golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
-golang.org/x/text v0.12.0 h1:k+n5B8goJNdU7hSvEtMUz3d1Q6D/XW4COJSJR6fN0mc=
-golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
+golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k=
+golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
 golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
 golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
 golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
diff --git a/internal/acl/acl_manager.go b/internal/acl/acl_manager.go
index 9bcd8c86..3026f09e 100644
--- a/internal/acl/acl_manager.go
+++ b/internal/acl/acl_manager.go
@@ -6,6 +6,7 @@
 	"strings"
 
 	"github.com/pkg/errors"
+	"golang.org/x/exp/maps"
 
 	"github.com/kopia/kopia/repo"
 	"github.com/kopia/kopia/repo/manifest"
@@ -109,11 +110,28 @@ func LoadEntries(ctx context.Context, rep repo.Repository, old []*Entry) ([]*Ent
 }
 
 // AddACL validates and adds the specified ACL entry to the repository.
-func AddACL(ctx context.Context, w repo.RepositoryWriter, e *Entry) error {
+func AddACL(ctx context.Context, w repo.RepositoryWriter, e *Entry, overwrite bool) error {
 	if err := e.Validate(); err != nil {
 		return errors.Wrap(err, "error validating ACL")
 	}
 
+	entries, err := LoadEntries(ctx, w, nil)
+	if err != nil {
+		return errors.Wrap(err, "unable to load ACL entries")
+	}
+
+	for _, oldE := range entries {
+		if e.User == oldE.User && maps.Equal(e.Target, oldE.Target) {
+			if !overwrite && e.Access < oldE.Access {
+				return errors.Errorf("ACL entry for a given user and target already exists %v: %v", oldE.User, oldE.Target)
+			}
+
+			if err = w.DeleteManifest(ctx, oldE.ManifestID); err != nil {
+				return errors.Wrap(err, "error deleting old")
+			}
+		}
+	}
+
 	manifestID, err := w.PutManifest(ctx, map[string]string{
 		manifest.TypeLabelKey: aclManifestType,
 	}, e)
diff --git a/internal/grpcapi/repository_server.pb.go b/internal/grpcapi/repository_server.pb.go
index b6969d7e..d72e1ef6 100644
--- a/internal/grpcapi/repository_server.pb.go
+++ b/internal/grpcapi/repository_server.pb.go
@@ -1357,6 +1357,108 @@ func (x *PrefetchContentsResponse) GetContentIds() []string {
 	return nil
 }
 
+type ApplyRetentionPolicyRequest struct {
+	state         protoimpl.MessageState
+	sizeCache     protoimpl.SizeCache
+	unknownFields protoimpl.UnknownFields
+
+	SourcePath   string `protobuf:"bytes,1,opt,name=source_path,json=sourcePath,proto3" json:"source_path,omitempty"`
+	ReallyDelete bool   `protobuf:"varint,2,opt,name=really_delete,json=reallyDelete,proto3" json:"really_delete,omitempty"`
+}
+
+func (x *ApplyRetentionPolicyRequest) Reset() {
+	*x = ApplyRetentionPolicyRequest{}
+	if protoimpl.UnsafeEnabled {
+		mi := &file_repository_server_proto_msgTypes[24]
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		ms.StoreMessageInfo(mi)
+	}
+}
+
+func (x *ApplyRetentionPolicyRequest) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*ApplyRetentionPolicyRequest) ProtoMessage() {}
+
+func (x *ApplyRetentionPolicyRequest) ProtoReflect() protoreflect.Message {
+	mi := &file_repository_server_proto_msgTypes[24]
+	if protoimpl.UnsafeEnabled && x != nil {
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		if ms.LoadMessageInfo() == nil {
+			ms.StoreMessageInfo(mi)
+		}
+		return ms
+	}
+	return mi.MessageOf(x)
+}
+
+// Deprecated: Use ApplyRetentionPolicyRequest.ProtoReflect.Descriptor instead.
+func (*ApplyRetentionPolicyRequest) Descriptor() ([]byte, []int) {
+	return file_repository_server_proto_rawDescGZIP(), []int{24}
+}
+
+func (x *ApplyRetentionPolicyRequest) GetSourcePath() string {
+	if x != nil {
+		return x.SourcePath
+	}
+	return ""
+}
+
+func (x *ApplyRetentionPolicyRequest) GetReallyDelete() bool {
+	if x != nil {
+		return x.ReallyDelete
+	}
+	return false
+}
+
+type ApplyRetentionPolicyResponse struct {
+	state         protoimpl.MessageState
+	sizeCache     protoimpl.SizeCache
+	unknownFields protoimpl.UnknownFields
+
+	ManifestIds []string `protobuf:"bytes,1,rep,name=manifest_ids,json=manifestIds,proto3" json:"manifest_ids,omitempty"`
+}
+
+func (x *ApplyRetentionPolicyResponse) Reset() {
+	*x = ApplyRetentionPolicyResponse{}
+	if protoimpl.UnsafeEnabled {
+		mi := &file_repository_server_proto_msgTypes[25]
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		ms.StoreMessageInfo(mi)
+	}
+}
+
+func (x *ApplyRetentionPolicyResponse) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*ApplyRetentionPolicyResponse) ProtoMessage() {}
+
+func (x *ApplyRetentionPolicyResponse) ProtoReflect() protoreflect.Message {
+	mi := &file_repository_server_proto_msgTypes[25]
+	if protoimpl.UnsafeEnabled && x != nil {
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		if ms.LoadMessageInfo() == nil {
+			ms.StoreMessageInfo(mi)
+		}
+		return ms
+	}
+	return mi.MessageOf(x)
+}
+
+// Deprecated: Use ApplyRetentionPolicyResponse.ProtoReflect.Descriptor instead.
+func (*ApplyRetentionPolicyResponse) Descriptor() ([]byte, []int) {
+	return file_repository_server_proto_rawDescGZIP(), []int{25}
+}
+
+func (x *ApplyRetentionPolicyResponse) GetManifestIds() []string {
+	if x != nil {
+		return x.ManifestIds
+	}
+	return nil
+}
+
 type SessionRequest struct {
 	state         protoimpl.MessageState
 	sizeCache     protoimpl.SizeCache
@@ -1379,13 +1481,14 @@ type SessionRequest struct {
 	//	*SessionRequest_FindManifests
 	//	*SessionRequest_DeleteManifest
 	//	*SessionRequest_PrefetchContents
+	//	*SessionRequest_ApplyRetentionPolicy
 	Request isSessionRequest_Request `protobuf_oneof:"request"`
 }
 
 func (x *SessionRequest) Reset() {
 	*x = SessionRequest{}
 	if protoimpl.UnsafeEnabled {
-		mi := &file_repository_server_proto_msgTypes[24]
+		mi := &file_repository_server_proto_msgTypes[26]
 		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
 		ms.StoreMessageInfo(mi)
 	}
@@ -1398,7 +1501,7 @@ func (x *SessionRequest) String() string {
 func (*SessionRequest) ProtoMessage() {}
 
 func (x *SessionRequest) ProtoReflect() protoreflect.Message {
-	mi := &file_repository_server_proto_msgTypes[24]
+	mi := &file_repository_server_proto_msgTypes[26]
 	if protoimpl.UnsafeEnabled && x != nil {
 		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
 		if ms.LoadMessageInfo() == nil {
@@ -1411,7 +1514,7 @@ func (x *SessionRequest) ProtoReflect() protoreflect.Message {
 
 // Deprecated: Use SessionRequest.ProtoReflect.Descriptor instead.
 func (*SessionRequest) Descriptor() ([]byte, []int) {
-	return file_repository_server_proto_rawDescGZIP(), []int{24}
+	return file_repository_server_proto_rawDescGZIP(), []int{26}
 }
 
 func (x *SessionRequest) GetRequestId() int64 {
@@ -1505,6 +1608,13 @@ func (x *SessionRequest) GetPrefetchContents() *PrefetchContentsRequest {
 	return nil
 }
 
+func (x *SessionRequest) GetApplyRetentionPolicy() *ApplyRetentionPolicyRequest {
+	if x, ok := x.GetRequest().(*SessionRequest_ApplyRetentionPolicy); ok {
+		return x.ApplyRetentionPolicy
+	}
+	return nil
+}
+
 type isSessionRequest_Request interface {
 	isSessionRequest_Request()
 }
@@ -1549,6 +1659,10 @@ type SessionRequest_PrefetchContents struct {
 	PrefetchContents *PrefetchContentsRequest `protobuf:"bytes,19,opt,name=prefetch_contents,json=prefetchContents,proto3,oneof"`
 }
 
+type SessionRequest_ApplyRetentionPolicy struct {
+	ApplyRetentionPolicy *ApplyRetentionPolicyRequest `protobuf:"bytes,20,opt,name=apply_retention_policy,json=applyRetentionPolicy,proto3,oneof"`
+}
+
 func (*SessionRequest_InitializeSession) isSessionRequest_Request() {}
 
 func (*SessionRequest_GetContentInfo) isSessionRequest_Request() {}
@@ -1569,13 +1683,15 @@ func (*SessionRequest_DeleteManifest) isSessionRequest_Request() {}
 
 func (*SessionRequest_PrefetchContents) isSessionRequest_Request() {}
 
+func (*SessionRequest_ApplyRetentionPolicy) isSessionRequest_Request() {}
+
 type SessionResponse struct {
 	state         protoimpl.MessageState
 	sizeCache     protoimpl.SizeCache
 	unknownFields protoimpl.UnknownFields
 
 	RequestId int64 `protobuf:"varint,1,opt,name=request_id,json=requestId,proto3" json:"request_id,omitempty"` // corresponds to request ID
-	HasMore   bool  `protobuf:"varint,3,opt,name=has_more,json=hasMore,proto3" json:"has_more,omitempty"`
+	HasMore   bool  `protobuf:"varint,3,opt,name=has_more,json=hasMore,proto3" json:"has_more,omitempty"`       // if set to true, the client should expect more responses with the same request_id.
 	// Types that are assignable to Response:
 	//	*SessionResponse_Error
 	//	*SessionResponse_InitializeSession
@@ -1588,13 +1704,14 @@ type SessionResponse struct {
 	//	*SessionResponse_FindManifests
 	//	*SessionResponse_DeleteManifest
 	//	*SessionResponse_PrefetchContents
+	//	*SessionResponse_ApplyRetentionPolicy
 	Response isSessionResponse_Response `protobuf_oneof:"response"`
 }
 
 func (x *SessionResponse) Reset() {
 	*x = SessionResponse{}
 	if protoimpl.UnsafeEnabled {
-		mi := &file_repository_server_proto_msgTypes[25]
+		mi := &file_repository_server_proto_msgTypes[27]
 		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
 		ms.StoreMessageInfo(mi)
 	}
@@ -1607,7 +1724,7 @@ func (x *SessionResponse) String() string {
 func (*SessionResponse) ProtoMessage() {}
 
 func (x *SessionResponse) ProtoReflect() protoreflect.Message {
-	mi := &file_repository_server_proto_msgTypes[25]
+	mi := &file_repository_server_proto_msgTypes[27]
 	if protoimpl.UnsafeEnabled && x != nil {
 		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
 		if ms.LoadMessageInfo() == nil {
@@ -1620,7 +1737,7 @@ func (x *SessionResponse) ProtoReflect() protoreflect.Message {
 
 // Deprecated: Use SessionResponse.ProtoReflect.Descriptor instead.
 func (*SessionResponse) Descriptor() ([]byte, []int) {
-	return file_repository_server_proto_rawDescGZIP(), []int{25}
+	return file_repository_server_proto_rawDescGZIP(), []int{27}
 }
 
 func (x *SessionResponse) GetRequestId() int64 {
@@ -1721,6 +1838,13 @@ func (x *SessionResponse) GetPrefetchContents() *PrefetchContentsResponse {
 	return nil
 }
 
+func (x *SessionResponse) GetApplyRetentionPolicy() *ApplyRetentionPolicyResponse {
+	if x, ok := x.GetResponse().(*SessionResponse_ApplyRetentionPolicy); ok {
+		return x.ApplyRetentionPolicy
+	}
+	return nil
+}
+
 type isSessionResponse_Response interface {
 	isSessionResponse_Response()
 }
@@ -1769,6 +1893,10 @@ type SessionResponse_PrefetchContents struct {
 	PrefetchContents *PrefetchContentsResponse `protobuf:"bytes,19,opt,name=prefetch_contents,json=prefetchContents,proto3,oneof"`
 }
 
+type SessionResponse_ApplyRetentionPolicy struct {
+	ApplyRetentionPolicy *ApplyRetentionPolicyResponse `protobuf:"bytes,20,opt,name=apply_retention_policy,json=applyRetentionPolicy,proto3,oneof"`
+}
+
 func (*SessionResponse_Error) isSessionResponse_Response() {}
 
 func (*SessionResponse_InitializeSession) isSessionResponse_Response() {}
@@ -1791,6 +1919,8 @@ func (*SessionResponse_DeleteManifest) isSessionResponse_Response() {}
 
 func (*SessionResponse_PrefetchContents) isSessionResponse_Response() {}
 
+func (*SessionResponse_ApplyRetentionPolicy) isSessionResponse_Response() {}
+
 var File_repository_server_proto protoreflect.FileDescriptor
 
 var file_repository_server_proto_rawDesc = []byte{
@@ -1952,139 +2082,162 @@ func (*SessionResponse_PrefetchContents) isSessionResponse_Response() {}
 	0x0a, 0x18, 0x50, 0x72, 0x65, 0x66, 0x65, 0x74, 0x63, 0x68, 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x6e,
 	0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x1f, 0x0a, 0x0b, 0x63, 0x6f,
 	0x6e, 0x74, 0x65, 0x6e, 0x74, 0x5f, 0x69, 0x64, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x09, 0x52,
-	0x0a, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x49, 0x64, 0x73, 0x22, 0xe9, 0x07, 0x0a, 0x0e,
-	0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1d,
-	0x0a, 0x0a, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01,
-	0x28, 0x03, 0x52, 0x09, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x49, 0x64, 0x12, 0x57, 0x0a,
-	0x0d, 0x74, 0x72, 0x61, 0x63, 0x65, 0x5f, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x78, 0x74, 0x18, 0x02,
-	0x20, 0x03, 0x28, 0x0b, 0x32, 0x32, 0x2e, 0x6b, 0x6f, 0x70, 0x69, 0x61, 0x5f, 0x72, 0x65, 0x70,
-	0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x52,
-	0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x2e, 0x54, 0x72, 0x61, 0x63, 0x65, 0x43, 0x6f, 0x6e, 0x74,
-	0x65, 0x78, 0x74, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x0c, 0x74, 0x72, 0x61, 0x63, 0x65, 0x43,
-	0x6f, 0x6e, 0x74, 0x65, 0x78, 0x74, 0x12, 0x5b, 0x0a, 0x12, 0x69, 0x6e, 0x69, 0x74, 0x69, 0x61,
-	0x6c, 0x69, 0x7a, 0x65, 0x5f, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x0a, 0x20, 0x01,
-	0x28, 0x0b, 0x32, 0x2a, 0x2e, 0x6b, 0x6f, 0x70, 0x69, 0x61, 0x5f, 0x72, 0x65, 0x70, 0x6f, 0x73,
-	0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x49, 0x6e, 0x69, 0x74, 0x69, 0x61, 0x6c, 0x69, 0x7a, 0x65,
-	0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x48, 0x00,
-	0x52, 0x11, 0x69, 0x6e, 0x69, 0x74, 0x69, 0x61, 0x6c, 0x69, 0x7a, 0x65, 0x53, 0x65, 0x73, 0x73,
-	0x69, 0x6f, 0x6e, 0x12, 0x53, 0x0a, 0x10, 0x67, 0x65, 0x74, 0x5f, 0x63, 0x6f, 0x6e, 0x74, 0x65,
-	0x6e, 0x74, 0x5f, 0x69, 0x6e, 0x66, 0x6f, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x27, 0x2e,
-	0x6b, 0x6f, 0x70, 0x69, 0x61, 0x5f, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79,
-	0x2e, 0x47, 0x65, 0x74, 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x52,
-	0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x48, 0x00, 0x52, 0x0e, 0x67, 0x65, 0x74, 0x43, 0x6f, 0x6e,
-	0x74, 0x65, 0x6e, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x36, 0x0a, 0x05, 0x66, 0x6c, 0x75, 0x73,
-	0x68, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1e, 0x2e, 0x6b, 0x6f, 0x70, 0x69, 0x61, 0x5f,
-	0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x46, 0x6c, 0x75, 0x73, 0x68,
-	0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x48, 0x00, 0x52, 0x05, 0x66, 0x6c, 0x75, 0x73, 0x68,
-	0x12, 0x4c, 0x0a, 0x0d, 0x77, 0x72, 0x69, 0x74, 0x65, 0x5f, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e,
-	0x74, 0x18, 0x0d, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x25, 0x2e, 0x6b, 0x6f, 0x70, 0x69, 0x61, 0x5f,
-	0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x57, 0x72, 0x69, 0x74, 0x65,
-	0x43, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x48, 0x00,
-	0x52, 0x0c, 0x77, 0x72, 0x69, 0x74, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x12, 0x46,
-	0x0a, 0x0b, 0x67, 0x65, 0x74, 0x5f, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x18, 0x0e, 0x20,
-	0x01, 0x28, 0x0b, 0x32, 0x23, 0x2e, 0x6b, 0x6f, 0x70, 0x69, 0x61, 0x5f, 0x72, 0x65, 0x70, 0x6f,
-	0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x47, 0x65, 0x74, 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x6e,
-	0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x48, 0x00, 0x52, 0x0a, 0x67, 0x65, 0x74, 0x43,
-	0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x12, 0x49, 0x0a, 0x0c, 0x67, 0x65, 0x74, 0x5f, 0x6d, 0x61,
-	0x6e, 0x69, 0x66, 0x65, 0x73, 0x74, 0x18, 0x0f, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x24, 0x2e, 0x6b,
-	0x6f, 0x70, 0x69, 0x61, 0x5f, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e,
-	0x47, 0x65, 0x74, 0x4d, 0x61, 0x6e, 0x69, 0x66, 0x65, 0x73, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65,
-	0x73, 0x74, 0x48, 0x00, 0x52, 0x0b, 0x67, 0x65, 0x74, 0x4d, 0x61, 0x6e, 0x69, 0x66, 0x65, 0x73,
-	0x74, 0x12, 0x49, 0x0a, 0x0c, 0x70, 0x75, 0x74, 0x5f, 0x6d, 0x61, 0x6e, 0x69, 0x66, 0x65, 0x73,
-	0x74, 0x18, 0x10, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x24, 0x2e, 0x6b, 0x6f, 0x70, 0x69, 0x61, 0x5f,
-	0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x50, 0x75, 0x74, 0x4d, 0x61,
-	0x6e, 0x69, 0x66, 0x65, 0x73, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x48, 0x00, 0x52,
-	0x0b, 0x70, 0x75, 0x74, 0x4d, 0x61, 0x6e, 0x69, 0x66, 0x65, 0x73, 0x74, 0x12, 0x4f, 0x0a, 0x0e,
-	0x66, 0x69, 0x6e, 0x64, 0x5f, 0x6d, 0x61, 0x6e, 0x69, 0x66, 0x65, 0x73, 0x74, 0x73, 0x18, 0x11,
-	0x20, 0x01, 0x28, 0x0b, 0x32, 0x26, 0x2e, 0x6b, 0x6f, 0x70, 0x69, 0x61, 0x5f, 0x72, 0x65, 0x70,
-	0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x46, 0x69, 0x6e, 0x64, 0x4d, 0x61, 0x6e, 0x69,
-	0x66, 0x65, 0x73, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x48, 0x00, 0x52, 0x0d,
-	0x66, 0x69, 0x6e, 0x64, 0x4d, 0x61, 0x6e, 0x69, 0x66, 0x65, 0x73, 0x74, 0x73, 0x12, 0x52, 0x0a,
-	0x0f, 0x64, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x5f, 0x6d, 0x61, 0x6e, 0x69, 0x66, 0x65, 0x73, 0x74,
-	0x18, 0x12, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x27, 0x2e, 0x6b, 0x6f, 0x70, 0x69, 0x61, 0x5f, 0x72,
-	0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65,
-	0x4d, 0x61, 0x6e, 0x69, 0x66, 0x65, 0x73, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x48,
-	0x00, 0x52, 0x0e, 0x64, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x4d, 0x61, 0x6e, 0x69, 0x66, 0x65, 0x73,
-	0x74, 0x12, 0x58, 0x0a, 0x11, 0x70, 0x72, 0x65, 0x66, 0x65, 0x74, 0x63, 0x68, 0x5f, 0x63, 0x6f,
-	0x6e, 0x74, 0x65, 0x6e, 0x74, 0x73, 0x18, 0x13, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x29, 0x2e, 0x6b,
+	0x0a, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x49, 0x64, 0x73, 0x22, 0x63, 0x0a, 0x1b, 0x41,
+	0x70, 0x70, 0x6c, 0x79, 0x52, 0x65, 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x50, 0x6f, 0x6c,
+	0x69, 0x63, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1f, 0x0a, 0x0b, 0x73, 0x6f,
+	0x75, 0x72, 0x63, 0x65, 0x5f, 0x70, 0x61, 0x74, 0x68, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52,
+	0x0a, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x50, 0x61, 0x74, 0x68, 0x12, 0x23, 0x0a, 0x0d, 0x72,
+	0x65, 0x61, 0x6c, 0x6c, 0x79, 0x5f, 0x64, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x18, 0x02, 0x20, 0x01,
+	0x28, 0x08, 0x52, 0x0c, 0x72, 0x65, 0x61, 0x6c, 0x6c, 0x79, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65,
+	0x22, 0x41, 0x0a, 0x1c, 0x41, 0x70, 0x70, 0x6c, 0x79, 0x52, 0x65, 0x74, 0x65, 0x6e, 0x74, 0x69,
+	0x6f, 0x6e, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65,
+	0x12, 0x21, 0x0a, 0x0c, 0x6d, 0x61, 0x6e, 0x69, 0x66, 0x65, 0x73, 0x74, 0x5f, 0x69, 0x64, 0x73,
+	0x18, 0x01, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0b, 0x6d, 0x61, 0x6e, 0x69, 0x66, 0x65, 0x73, 0x74,
+	0x49, 0x64, 0x73, 0x22, 0xd0, 0x08, 0x0a, 0x0e, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x52,
+	0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1d, 0x0a, 0x0a, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73,
+	0x74, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x09, 0x72, 0x65, 0x71, 0x75,
+	0x65, 0x73, 0x74, 0x49, 0x64, 0x12, 0x57, 0x0a, 0x0d, 0x74, 0x72, 0x61, 0x63, 0x65, 0x5f, 0x63,
+	0x6f, 0x6e, 0x74, 0x65, 0x78, 0x74, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x32, 0x2e, 0x6b,
 	0x6f, 0x70, 0x69, 0x61, 0x5f, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e,
-	0x50, 0x72, 0x65, 0x66, 0x65, 0x74, 0x63, 0x68, 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x73,
-	0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x48, 0x00, 0x52, 0x10, 0x70, 0x72, 0x65, 0x66, 0x65,
-	0x74, 0x63, 0x68, 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x73, 0x1a, 0x3f, 0x0a, 0x11, 0x54,
+	0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x2e, 0x54,
 	0x72, 0x61, 0x63, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x78, 0x74, 0x45, 0x6e, 0x74, 0x72, 0x79,
-	0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b,
-	0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28,
-	0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x42, 0x09, 0x0a, 0x07,
-	0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0xaf, 0x07, 0x0a, 0x0f, 0x53, 0x65, 0x73, 0x73,
-	0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x1d, 0x0a, 0x0a, 0x72,
-	0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52,
-	0x09, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x49, 0x64, 0x12, 0x19, 0x0a, 0x08, 0x68, 0x61,
-	0x73, 0x5f, 0x6d, 0x6f, 0x72, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x68, 0x61,
-	0x73, 0x4d, 0x6f, 0x72, 0x65, 0x12, 0x37, 0x0a, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x02,
-	0x20, 0x01, 0x28, 0x0b, 0x32, 0x1f, 0x2e, 0x6b, 0x6f, 0x70, 0x69, 0x61, 0x5f, 0x72, 0x65, 0x70,
-	0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x52, 0x65, 0x73,
-	0x70, 0x6f, 0x6e, 0x73, 0x65, 0x48, 0x00, 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x12, 0x5c,
+	0x52, 0x0c, 0x74, 0x72, 0x61, 0x63, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x78, 0x74, 0x12, 0x5b,
 	0x0a, 0x12, 0x69, 0x6e, 0x69, 0x74, 0x69, 0x61, 0x6c, 0x69, 0x7a, 0x65, 0x5f, 0x73, 0x65, 0x73,
-	0x73, 0x69, 0x6f, 0x6e, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2b, 0x2e, 0x6b, 0x6f, 0x70,
+	0x73, 0x69, 0x6f, 0x6e, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2a, 0x2e, 0x6b, 0x6f, 0x70,
 	0x69, 0x61, 0x5f, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x49, 0x6e,
 	0x69, 0x74, 0x69, 0x61, 0x6c, 0x69, 0x7a, 0x65, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x52,
-	0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x48, 0x00, 0x52, 0x11, 0x69, 0x6e, 0x69, 0x74, 0x69,
-	0x61, 0x6c, 0x69, 0x7a, 0x65, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x54, 0x0a, 0x10,
-	0x67, 0x65, 0x74, 0x5f, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x5f, 0x69, 0x6e, 0x66, 0x6f,
-	0x18, 0x0b, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x28, 0x2e, 0x6b, 0x6f, 0x70, 0x69, 0x61, 0x5f, 0x72,
-	0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x47, 0x65, 0x74, 0x43, 0x6f, 0x6e,
-	0x74, 0x65, 0x6e, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65,
-	0x48, 0x00, 0x52, 0x0e, 0x67, 0x65, 0x74, 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x49, 0x6e,
-	0x66, 0x6f, 0x12, 0x37, 0x0a, 0x05, 0x66, 0x6c, 0x75, 0x73, 0x68, 0x18, 0x0c, 0x20, 0x01, 0x28,
-	0x0b, 0x32, 0x1f, 0x2e, 0x6b, 0x6f, 0x70, 0x69, 0x61, 0x5f, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69,
-	0x74, 0x6f, 0x72, 0x79, 0x2e, 0x46, 0x6c, 0x75, 0x73, 0x68, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e,
-	0x73, 0x65, 0x48, 0x00, 0x52, 0x05, 0x66, 0x6c, 0x75, 0x73, 0x68, 0x12, 0x4d, 0x0a, 0x0d, 0x77,
-	0x72, 0x69, 0x74, 0x65, 0x5f, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x18, 0x0d, 0x20, 0x01,
-	0x28, 0x0b, 0x32, 0x26, 0x2e, 0x6b, 0x6f, 0x70, 0x69, 0x61, 0x5f, 0x72, 0x65, 0x70, 0x6f, 0x73,
-	0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x57, 0x72, 0x69, 0x74, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x65,
-	0x6e, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x48, 0x00, 0x52, 0x0c, 0x77, 0x72,
-	0x69, 0x74, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x12, 0x47, 0x0a, 0x0b, 0x67, 0x65,
-	0x74, 0x5f, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x18, 0x0e, 0x20, 0x01, 0x28, 0x0b, 0x32,
+	0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x48, 0x00, 0x52, 0x11, 0x69, 0x6e, 0x69, 0x74, 0x69, 0x61,
+	0x6c, 0x69, 0x7a, 0x65, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x53, 0x0a, 0x10, 0x67,
+	0x65, 0x74, 0x5f, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x5f, 0x69, 0x6e, 0x66, 0x6f, 0x18,
+	0x0b, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x27, 0x2e, 0x6b, 0x6f, 0x70, 0x69, 0x61, 0x5f, 0x72, 0x65,
+	0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x47, 0x65, 0x74, 0x43, 0x6f, 0x6e, 0x74,
+	0x65, 0x6e, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x48, 0x00,
+	0x52, 0x0e, 0x67, 0x65, 0x74, 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x49, 0x6e, 0x66, 0x6f,
+	0x12, 0x36, 0x0a, 0x05, 0x66, 0x6c, 0x75, 0x73, 0x68, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x0b, 0x32,
+	0x1e, 0x2e, 0x6b, 0x6f, 0x70, 0x69, 0x61, 0x5f, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f,
+	0x72, 0x79, 0x2e, 0x46, 0x6c, 0x75, 0x73, 0x68, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x48,
+	0x00, 0x52, 0x05, 0x66, 0x6c, 0x75, 0x73, 0x68, 0x12, 0x4c, 0x0a, 0x0d, 0x77, 0x72, 0x69, 0x74,
+	0x65, 0x5f, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x18, 0x0d, 0x20, 0x01, 0x28, 0x0b, 0x32,
+	0x25, 0x2e, 0x6b, 0x6f, 0x70, 0x69, 0x61, 0x5f, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f,
+	0x72, 0x79, 0x2e, 0x57, 0x72, 0x69, 0x74, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x52,
+	0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x48, 0x00, 0x52, 0x0c, 0x77, 0x72, 0x69, 0x74, 0x65, 0x43,
+	0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x12, 0x46, 0x0a, 0x0b, 0x67, 0x65, 0x74, 0x5f, 0x63, 0x6f,
+	0x6e, 0x74, 0x65, 0x6e, 0x74, 0x18, 0x0e, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x23, 0x2e, 0x6b, 0x6f,
+	0x70, 0x69, 0x61, 0x5f, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x47,
+	0x65, 0x74, 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74,
+	0x48, 0x00, 0x52, 0x0a, 0x67, 0x65, 0x74, 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x12, 0x49,
+	0x0a, 0x0c, 0x67, 0x65, 0x74, 0x5f, 0x6d, 0x61, 0x6e, 0x69, 0x66, 0x65, 0x73, 0x74, 0x18, 0x0f,
+	0x20, 0x01, 0x28, 0x0b, 0x32, 0x24, 0x2e, 0x6b, 0x6f, 0x70, 0x69, 0x61, 0x5f, 0x72, 0x65, 0x70,
+	0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x47, 0x65, 0x74, 0x4d, 0x61, 0x6e, 0x69, 0x66,
+	0x65, 0x73, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x48, 0x00, 0x52, 0x0b, 0x67, 0x65,
+	0x74, 0x4d, 0x61, 0x6e, 0x69, 0x66, 0x65, 0x73, 0x74, 0x12, 0x49, 0x0a, 0x0c, 0x70, 0x75, 0x74,
+	0x5f, 0x6d, 0x61, 0x6e, 0x69, 0x66, 0x65, 0x73, 0x74, 0x18, 0x10, 0x20, 0x01, 0x28, 0x0b, 0x32,
 	0x24, 0x2e, 0x6b, 0x6f, 0x70, 0x69, 0x61, 0x5f, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f,
-	0x72, 0x79, 0x2e, 0x47, 0x65, 0x74, 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x73,
-	0x70, 0x6f, 0x6e, 0x73, 0x65, 0x48, 0x00, 0x52, 0x0a, 0x67, 0x65, 0x74, 0x43, 0x6f, 0x6e, 0x74,
-	0x65, 0x6e, 0x74, 0x12, 0x4a, 0x0a, 0x0c, 0x67, 0x65, 0x74, 0x5f, 0x6d, 0x61, 0x6e, 0x69, 0x66,
-	0x65, 0x73, 0x74, 0x18, 0x0f, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x25, 0x2e, 0x6b, 0x6f, 0x70, 0x69,
-	0x61, 0x5f, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x47, 0x65, 0x74,
-	0x4d, 0x61, 0x6e, 0x69, 0x66, 0x65, 0x73, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65,
-	0x48, 0x00, 0x52, 0x0b, 0x67, 0x65, 0x74, 0x4d, 0x61, 0x6e, 0x69, 0x66, 0x65, 0x73, 0x74, 0x12,
-	0x4a, 0x0a, 0x0c, 0x70, 0x75, 0x74, 0x5f, 0x6d, 0x61, 0x6e, 0x69, 0x66, 0x65, 0x73, 0x74, 0x18,
-	0x10, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x25, 0x2e, 0x6b, 0x6f, 0x70, 0x69, 0x61, 0x5f, 0x72, 0x65,
-	0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x50, 0x75, 0x74, 0x4d, 0x61, 0x6e, 0x69,
-	0x66, 0x65, 0x73, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x48, 0x00, 0x52, 0x0b,
-	0x70, 0x75, 0x74, 0x4d, 0x61, 0x6e, 0x69, 0x66, 0x65, 0x73, 0x74, 0x12, 0x50, 0x0a, 0x0e, 0x66,
-	0x69, 0x6e, 0x64, 0x5f, 0x6d, 0x61, 0x6e, 0x69, 0x66, 0x65, 0x73, 0x74, 0x73, 0x18, 0x11, 0x20,
-	0x01, 0x28, 0x0b, 0x32, 0x27, 0x2e, 0x6b, 0x6f, 0x70, 0x69, 0x61, 0x5f, 0x72, 0x65, 0x70, 0x6f,
-	0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x46, 0x69, 0x6e, 0x64, 0x4d, 0x61, 0x6e, 0x69, 0x66,
-	0x65, 0x73, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x48, 0x00, 0x52, 0x0d,
-	0x66, 0x69, 0x6e, 0x64, 0x4d, 0x61, 0x6e, 0x69, 0x66, 0x65, 0x73, 0x74, 0x73, 0x12, 0x53, 0x0a,
-	0x0f, 0x64, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x5f, 0x6d, 0x61, 0x6e, 0x69, 0x66, 0x65, 0x73, 0x74,
-	0x18, 0x12, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x28, 0x2e, 0x6b, 0x6f, 0x70, 0x69, 0x61, 0x5f, 0x72,
-	0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65,
-	0x4d, 0x61, 0x6e, 0x69, 0x66, 0x65, 0x73, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65,
-	0x48, 0x00, 0x52, 0x0e, 0x64, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x4d, 0x61, 0x6e, 0x69, 0x66, 0x65,
-	0x73, 0x74, 0x12, 0x59, 0x0a, 0x11, 0x70, 0x72, 0x65, 0x66, 0x65, 0x74, 0x63, 0x68, 0x5f, 0x63,
-	0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x73, 0x18, 0x13, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2a, 0x2e,
-	0x6b, 0x6f, 0x70, 0x69, 0x61, 0x5f, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79,
-	0x2e, 0x50, 0x72, 0x65, 0x66, 0x65, 0x74, 0x63, 0x68, 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74,
-	0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x48, 0x00, 0x52, 0x10, 0x70, 0x72, 0x65,
-	0x66, 0x65, 0x74, 0x63, 0x68, 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x73, 0x42, 0x0a, 0x0a,
-	0x08, 0x72, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x32, 0x65, 0x0a, 0x0f, 0x4b, 0x6f, 0x70,
-	0x69, 0x61, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x12, 0x52, 0x0a, 0x07,
-	0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x20, 0x2e, 0x6b, 0x6f, 0x70, 0x69, 0x61, 0x5f,
-	0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x53, 0x65, 0x73, 0x73, 0x69,
-	0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x21, 0x2e, 0x6b, 0x6f, 0x70, 0x69,
-	0x61, 0x5f, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x53, 0x65, 0x73,
-	0x73, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x28, 0x01, 0x30, 0x01,
-	0x42, 0x29, 0x5a, 0x27, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x6b,
-	0x6f, 0x70, 0x69, 0x61, 0x2f, 0x6b, 0x6f, 0x70, 0x69, 0x61, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72,
-	0x6e, 0x61, 0x6c, 0x2f, 0x67, 0x72, 0x70, 0x63, 0x61, 0x70, 0x69, 0x62, 0x06, 0x70, 0x72, 0x6f,
-	0x74, 0x6f, 0x33,
+	0x72, 0x79, 0x2e, 0x50, 0x75, 0x74, 0x4d, 0x61, 0x6e, 0x69, 0x66, 0x65, 0x73, 0x74, 0x52, 0x65,
+	0x71, 0x75, 0x65, 0x73, 0x74, 0x48, 0x00, 0x52, 0x0b, 0x70, 0x75, 0x74, 0x4d, 0x61, 0x6e, 0x69,
+	0x66, 0x65, 0x73, 0x74, 0x12, 0x4f, 0x0a, 0x0e, 0x66, 0x69, 0x6e, 0x64, 0x5f, 0x6d, 0x61, 0x6e,
+	0x69, 0x66, 0x65, 0x73, 0x74, 0x73, 0x18, 0x11, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x26, 0x2e, 0x6b,
+	0x6f, 0x70, 0x69, 0x61, 0x5f, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e,
+	0x46, 0x69, 0x6e, 0x64, 0x4d, 0x61, 0x6e, 0x69, 0x66, 0x65, 0x73, 0x74, 0x73, 0x52, 0x65, 0x71,
+	0x75, 0x65, 0x73, 0x74, 0x48, 0x00, 0x52, 0x0d, 0x66, 0x69, 0x6e, 0x64, 0x4d, 0x61, 0x6e, 0x69,
+	0x66, 0x65, 0x73, 0x74, 0x73, 0x12, 0x52, 0x0a, 0x0f, 0x64, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x5f,
+	0x6d, 0x61, 0x6e, 0x69, 0x66, 0x65, 0x73, 0x74, 0x18, 0x12, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x27,
+	0x2e, 0x6b, 0x6f, 0x70, 0x69, 0x61, 0x5f, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72,
+	0x79, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x4d, 0x61, 0x6e, 0x69, 0x66, 0x65, 0x73, 0x74,
+	0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x48, 0x00, 0x52, 0x0e, 0x64, 0x65, 0x6c, 0x65, 0x74,
+	0x65, 0x4d, 0x61, 0x6e, 0x69, 0x66, 0x65, 0x73, 0x74, 0x12, 0x58, 0x0a, 0x11, 0x70, 0x72, 0x65,
+	0x66, 0x65, 0x74, 0x63, 0x68, 0x5f, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x73, 0x18, 0x13,
+	0x20, 0x01, 0x28, 0x0b, 0x32, 0x29, 0x2e, 0x6b, 0x6f, 0x70, 0x69, 0x61, 0x5f, 0x72, 0x65, 0x70,
+	0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x50, 0x72, 0x65, 0x66, 0x65, 0x74, 0x63, 0x68,
+	0x43, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x48,
+	0x00, 0x52, 0x10, 0x70, 0x72, 0x65, 0x66, 0x65, 0x74, 0x63, 0x68, 0x43, 0x6f, 0x6e, 0x74, 0x65,
+	0x6e, 0x74, 0x73, 0x12, 0x65, 0x0a, 0x16, 0x61, 0x70, 0x70, 0x6c, 0x79, 0x5f, 0x72, 0x65, 0x74,
+	0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x18, 0x14, 0x20,
+	0x01, 0x28, 0x0b, 0x32, 0x2d, 0x2e, 0x6b, 0x6f, 0x70, 0x69, 0x61, 0x5f, 0x72, 0x65, 0x70, 0x6f,
+	0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x41, 0x70, 0x70, 0x6c, 0x79, 0x52, 0x65, 0x74, 0x65,
+	0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65,
+	0x73, 0x74, 0x48, 0x00, 0x52, 0x14, 0x61, 0x70, 0x70, 0x6c, 0x79, 0x52, 0x65, 0x74, 0x65, 0x6e,
+	0x74, 0x69, 0x6f, 0x6e, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x1a, 0x3f, 0x0a, 0x11, 0x54, 0x72,
+	0x61, 0x63, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x78, 0x74, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12,
+	0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65,
+	0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09,
+	0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x42, 0x09, 0x0a, 0x07, 0x72,
+	0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x97, 0x08, 0x0a, 0x0f, 0x53, 0x65, 0x73, 0x73, 0x69,
+	0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x1d, 0x0a, 0x0a, 0x72, 0x65,
+	0x71, 0x75, 0x65, 0x73, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x09,
+	0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x49, 0x64, 0x12, 0x19, 0x0a, 0x08, 0x68, 0x61, 0x73,
+	0x5f, 0x6d, 0x6f, 0x72, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x68, 0x61, 0x73,
+	0x4d, 0x6f, 0x72, 0x65, 0x12, 0x37, 0x0a, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x02, 0x20,
+	0x01, 0x28, 0x0b, 0x32, 0x1f, 0x2e, 0x6b, 0x6f, 0x70, 0x69, 0x61, 0x5f, 0x72, 0x65, 0x70, 0x6f,
+	0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x52, 0x65, 0x73, 0x70,
+	0x6f, 0x6e, 0x73, 0x65, 0x48, 0x00, 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x12, 0x5c, 0x0a,
+	0x12, 0x69, 0x6e, 0x69, 0x74, 0x69, 0x61, 0x6c, 0x69, 0x7a, 0x65, 0x5f, 0x73, 0x65, 0x73, 0x73,
+	0x69, 0x6f, 0x6e, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2b, 0x2e, 0x6b, 0x6f, 0x70, 0x69,
+	0x61, 0x5f, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x49, 0x6e, 0x69,
+	0x74, 0x69, 0x61, 0x6c, 0x69, 0x7a, 0x65, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x65,
+	0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x48, 0x00, 0x52, 0x11, 0x69, 0x6e, 0x69, 0x74, 0x69, 0x61,
+	0x6c, 0x69, 0x7a, 0x65, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x54, 0x0a, 0x10, 0x67,
+	0x65, 0x74, 0x5f, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x5f, 0x69, 0x6e, 0x66, 0x6f, 0x18,
+	0x0b, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x28, 0x2e, 0x6b, 0x6f, 0x70, 0x69, 0x61, 0x5f, 0x72, 0x65,
+	0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x47, 0x65, 0x74, 0x43, 0x6f, 0x6e, 0x74,
+	0x65, 0x6e, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x48,
+	0x00, 0x52, 0x0e, 0x67, 0x65, 0x74, 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x49, 0x6e, 0x66,
+	0x6f, 0x12, 0x37, 0x0a, 0x05, 0x66, 0x6c, 0x75, 0x73, 0x68, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x0b,
+	0x32, 0x1f, 0x2e, 0x6b, 0x6f, 0x70, 0x69, 0x61, 0x5f, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74,
+	0x6f, 0x72, 0x79, 0x2e, 0x46, 0x6c, 0x75, 0x73, 0x68, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73,
+	0x65, 0x48, 0x00, 0x52, 0x05, 0x66, 0x6c, 0x75, 0x73, 0x68, 0x12, 0x4d, 0x0a, 0x0d, 0x77, 0x72,
+	0x69, 0x74, 0x65, 0x5f, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x18, 0x0d, 0x20, 0x01, 0x28,
+	0x0b, 0x32, 0x26, 0x2e, 0x6b, 0x6f, 0x70, 0x69, 0x61, 0x5f, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69,
+	0x74, 0x6f, 0x72, 0x79, 0x2e, 0x57, 0x72, 0x69, 0x74, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x6e,
+	0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x48, 0x00, 0x52, 0x0c, 0x77, 0x72, 0x69,
+	0x74, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x12, 0x47, 0x0a, 0x0b, 0x67, 0x65, 0x74,
+	0x5f, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x18, 0x0e, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x24,
+	0x2e, 0x6b, 0x6f, 0x70, 0x69, 0x61, 0x5f, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72,
+	0x79, 0x2e, 0x47, 0x65, 0x74, 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x73, 0x70,
+	0x6f, 0x6e, 0x73, 0x65, 0x48, 0x00, 0x52, 0x0a, 0x67, 0x65, 0x74, 0x43, 0x6f, 0x6e, 0x74, 0x65,
+	0x6e, 0x74, 0x12, 0x4a, 0x0a, 0x0c, 0x67, 0x65, 0x74, 0x5f, 0x6d, 0x61, 0x6e, 0x69, 0x66, 0x65,
+	0x73, 0x74, 0x18, 0x0f, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x25, 0x2e, 0x6b, 0x6f, 0x70, 0x69, 0x61,
+	0x5f, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x47, 0x65, 0x74, 0x4d,
+	0x61, 0x6e, 0x69, 0x66, 0x65, 0x73, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x48,
+	0x00, 0x52, 0x0b, 0x67, 0x65, 0x74, 0x4d, 0x61, 0x6e, 0x69, 0x66, 0x65, 0x73, 0x74, 0x12, 0x4a,
+	0x0a, 0x0c, 0x70, 0x75, 0x74, 0x5f, 0x6d, 0x61, 0x6e, 0x69, 0x66, 0x65, 0x73, 0x74, 0x18, 0x10,
+	0x20, 0x01, 0x28, 0x0b, 0x32, 0x25, 0x2e, 0x6b, 0x6f, 0x70, 0x69, 0x61, 0x5f, 0x72, 0x65, 0x70,
+	0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x50, 0x75, 0x74, 0x4d, 0x61, 0x6e, 0x69, 0x66,
+	0x65, 0x73, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x48, 0x00, 0x52, 0x0b, 0x70,
+	0x75, 0x74, 0x4d, 0x61, 0x6e, 0x69, 0x66, 0x65, 0x73, 0x74, 0x12, 0x50, 0x0a, 0x0e, 0x66, 0x69,
+	0x6e, 0x64, 0x5f, 0x6d, 0x61, 0x6e, 0x69, 0x66, 0x65, 0x73, 0x74, 0x73, 0x18, 0x11, 0x20, 0x01,
+	0x28, 0x0b, 0x32, 0x27, 0x2e, 0x6b, 0x6f, 0x70, 0x69, 0x61, 0x5f, 0x72, 0x65, 0x70, 0x6f, 0x73,
+	0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x46, 0x69, 0x6e, 0x64, 0x4d, 0x61, 0x6e, 0x69, 0x66, 0x65,
+	0x73, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x48, 0x00, 0x52, 0x0d, 0x66,
+	0x69, 0x6e, 0x64, 0x4d, 0x61, 0x6e, 0x69, 0x66, 0x65, 0x73, 0x74, 0x73, 0x12, 0x53, 0x0a, 0x0f,
+	0x64, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x5f, 0x6d, 0x61, 0x6e, 0x69, 0x66, 0x65, 0x73, 0x74, 0x18,
+	0x12, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x28, 0x2e, 0x6b, 0x6f, 0x70, 0x69, 0x61, 0x5f, 0x72, 0x65,
+	0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x4d,
+	0x61, 0x6e, 0x69, 0x66, 0x65, 0x73, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x48,
+	0x00, 0x52, 0x0e, 0x64, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x4d, 0x61, 0x6e, 0x69, 0x66, 0x65, 0x73,
+	0x74, 0x12, 0x59, 0x0a, 0x11, 0x70, 0x72, 0x65, 0x66, 0x65, 0x74, 0x63, 0x68, 0x5f, 0x63, 0x6f,
+	0x6e, 0x74, 0x65, 0x6e, 0x74, 0x73, 0x18, 0x13, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2a, 0x2e, 0x6b,
+	0x6f, 0x70, 0x69, 0x61, 0x5f, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e,
+	0x50, 0x72, 0x65, 0x66, 0x65, 0x74, 0x63, 0x68, 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x73,
+	0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x48, 0x00, 0x52, 0x10, 0x70, 0x72, 0x65, 0x66,
+	0x65, 0x74, 0x63, 0x68, 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x73, 0x12, 0x66, 0x0a, 0x16,
+	0x61, 0x70, 0x70, 0x6c, 0x79, 0x5f, 0x72, 0x65, 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x5f,
+	0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x18, 0x14, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2e, 0x2e, 0x6b,
+	0x6f, 0x70, 0x69, 0x61, 0x5f, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e,
+	0x41, 0x70, 0x70, 0x6c, 0x79, 0x52, 0x65, 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x50, 0x6f,
+	0x6c, 0x69, 0x63, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x48, 0x00, 0x52, 0x14,
+	0x61, 0x70, 0x70, 0x6c, 0x79, 0x52, 0x65, 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x50, 0x6f,
+	0x6c, 0x69, 0x63, 0x79, 0x42, 0x0a, 0x0a, 0x08, 0x72, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65,
+	0x32, 0x65, 0x0a, 0x0f, 0x4b, 0x6f, 0x70, 0x69, 0x61, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74,
+	0x6f, 0x72, 0x79, 0x12, 0x52, 0x0a, 0x07, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x20,
+	0x2e, 0x6b, 0x6f, 0x70, 0x69, 0x61, 0x5f, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72,
+	0x79, 0x2e, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74,
+	0x1a, 0x21, 0x2e, 0x6b, 0x6f, 0x70, 0x69, 0x61, 0x5f, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74,
+	0x6f, 0x72, 0x79, 0x2e, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f,
+	0x6e, 0x73, 0x65, 0x28, 0x01, 0x30, 0x01, 0x42, 0x29, 0x5a, 0x27, 0x67, 0x69, 0x74, 0x68, 0x75,
+	0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x6b, 0x6f, 0x70, 0x69, 0x61, 0x2f, 0x6b, 0x6f, 0x70, 0x69,
+	0x61, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x67, 0x72, 0x70, 0x63, 0x61,
+	0x70, 0x69, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
 }
 
 var (
@@ -2100,50 +2253,52 @@ func file_repository_server_proto_rawDescGZIP() []byte {
 }
 
 var file_repository_server_proto_enumTypes = make([]protoimpl.EnumInfo, 1)
-var file_repository_server_proto_msgTypes = make([]protoimpl.MessageInfo, 30)
+var file_repository_server_proto_msgTypes = make([]protoimpl.MessageInfo, 32)
 var file_repository_server_proto_goTypes = []interface{}{
-	(ErrorResponse_Code)(0),           // 0: kopia_repository.ErrorResponse.Code
-	(*ContentInfo)(nil),               // 1: kopia_repository.ContentInfo
-	(*ManifestEntryMetadata)(nil),     // 2: kopia_repository.ManifestEntryMetadata
-	(*ErrorResponse)(nil),             // 3: kopia_repository.ErrorResponse
-	(*RepositoryParameters)(nil),      // 4: kopia_repository.RepositoryParameters
-	(*InitializeSessionRequest)(nil),  // 5: kopia_repository.InitializeSessionRequest
-	(*InitializeSessionResponse)(nil), // 6: kopia_repository.InitializeSessionResponse
-	(*GetContentInfoRequest)(nil),     // 7: kopia_repository.GetContentInfoRequest
-	(*GetContentInfoResponse)(nil),    // 8: kopia_repository.GetContentInfoResponse
-	(*GetContentRequest)(nil),         // 9: kopia_repository.GetContentRequest
-	(*GetContentResponse)(nil),        // 10: kopia_repository.GetContentResponse
-	(*FlushRequest)(nil),              // 11: kopia_repository.FlushRequest
-	(*FlushResponse)(nil),             // 12: kopia_repository.FlushResponse
-	(*WriteContentRequest)(nil),       // 13: kopia_repository.WriteContentRequest
-	(*WriteContentResponse)(nil),      // 14: kopia_repository.WriteContentResponse
-	(*GetManifestRequest)(nil),        // 15: kopia_repository.GetManifestRequest
-	(*GetManifestResponse)(nil),       // 16: kopia_repository.GetManifestResponse
-	(*PutManifestRequest)(nil),        // 17: kopia_repository.PutManifestRequest
-	(*PutManifestResponse)(nil),       // 18: kopia_repository.PutManifestResponse
-	(*DeleteManifestRequest)(nil),     // 19: kopia_repository.DeleteManifestRequest
-	(*DeleteManifestResponse)(nil),    // 20: kopia_repository.DeleteManifestResponse
-	(*FindManifestsRequest)(nil),      // 21: kopia_repository.FindManifestsRequest
-	(*FindManifestsResponse)(nil),     // 22: kopia_repository.FindManifestsResponse
-	(*PrefetchContentsRequest)(nil),   // 23: kopia_repository.PrefetchContentsRequest
-	(*PrefetchContentsResponse)(nil),  // 24: kopia_repository.PrefetchContentsResponse
-	(*SessionRequest)(nil),            // 25: kopia_repository.SessionRequest
-	(*SessionResponse)(nil),           // 26: kopia_repository.SessionResponse
-	nil,                               // 27: kopia_repository.ManifestEntryMetadata.LabelsEntry
-	nil,                               // 28: kopia_repository.PutManifestRequest.LabelsEntry
-	nil,                               // 29: kopia_repository.FindManifestsRequest.LabelsEntry
-	nil,                               // 30: kopia_repository.SessionRequest.TraceContextEntry
+	(ErrorResponse_Code)(0),              // 0: kopia_repository.ErrorResponse.Code
+	(*ContentInfo)(nil),                  // 1: kopia_repository.ContentInfo
+	(*ManifestEntryMetadata)(nil),        // 2: kopia_repository.ManifestEntryMetadata
+	(*ErrorResponse)(nil),                // 3: kopia_repository.ErrorResponse
+	(*RepositoryParameters)(nil),         // 4: kopia_repository.RepositoryParameters
+	(*InitializeSessionRequest)(nil),     // 5: kopia_repository.InitializeSessionRequest
+	(*InitializeSessionResponse)(nil),    // 6: kopia_repository.InitializeSessionResponse
+	(*GetContentInfoRequest)(nil),        // 7: kopia_repository.GetContentInfoRequest
+	(*GetContentInfoResponse)(nil),       // 8: kopia_repository.GetContentInfoResponse
+	(*GetContentRequest)(nil),            // 9: kopia_repository.GetContentRequest
+	(*GetContentResponse)(nil),           // 10: kopia_repository.GetContentResponse
+	(*FlushRequest)(nil),                 // 11: kopia_repository.FlushRequest
+	(*FlushResponse)(nil),                // 12: kopia_repository.FlushResponse
+	(*WriteContentRequest)(nil),          // 13: kopia_repository.WriteContentRequest
+	(*WriteContentResponse)(nil),         // 14: kopia_repository.WriteContentResponse
+	(*GetManifestRequest)(nil),           // 15: kopia_repository.GetManifestRequest
+	(*GetManifestResponse)(nil),          // 16: kopia_repository.GetManifestResponse
+	(*PutManifestRequest)(nil),           // 17: kopia_repository.PutManifestRequest
+	(*PutManifestResponse)(nil),          // 18: kopia_repository.PutManifestResponse
+	(*DeleteManifestRequest)(nil),        // 19: kopia_repository.DeleteManifestRequest
+	(*DeleteManifestResponse)(nil),       // 20: kopia_repository.DeleteManifestResponse
+	(*FindManifestsRequest)(nil),         // 21: kopia_repository.FindManifestsRequest
+	(*FindManifestsResponse)(nil),        // 22: kopia_repository.FindManifestsResponse
+	(*PrefetchContentsRequest)(nil),      // 23: kopia_repository.PrefetchContentsRequest
+	(*PrefetchContentsResponse)(nil),     // 24: kopia_repository.PrefetchContentsResponse
+	(*ApplyRetentionPolicyRequest)(nil),  // 25: kopia_repository.ApplyRetentionPolicyRequest
+	(*ApplyRetentionPolicyResponse)(nil), // 26: kopia_repository.ApplyRetentionPolicyResponse
+	(*SessionRequest)(nil),               // 27: kopia_repository.SessionRequest
+	(*SessionResponse)(nil),              // 28: kopia_repository.SessionResponse
+	nil,                                  // 29: kopia_repository.ManifestEntryMetadata.LabelsEntry
+	nil,                                  // 30: kopia_repository.PutManifestRequest.LabelsEntry
+	nil,                                  // 31: kopia_repository.FindManifestsRequest.LabelsEntry
+	nil,                                  // 32: kopia_repository.SessionRequest.TraceContextEntry
 }
 var file_repository_server_proto_depIdxs = []int32{
-	27, // 0: kopia_repository.ManifestEntryMetadata.labels:type_name -> kopia_repository.ManifestEntryMetadata.LabelsEntry
+	29, // 0: kopia_repository.ManifestEntryMetadata.labels:type_name -> kopia_repository.ManifestEntryMetadata.LabelsEntry
 	0,  // 1: kopia_repository.ErrorResponse.code:type_name -> kopia_repository.ErrorResponse.Code
 	4,  // 2: kopia_repository.InitializeSessionResponse.parameters:type_name -> kopia_repository.RepositoryParameters
 	1,  // 3: kopia_repository.GetContentInfoResponse.info:type_name -> kopia_repository.ContentInfo
 	2,  // 4: kopia_repository.GetManifestResponse.metadata:type_name -> kopia_repository.ManifestEntryMetadata
-	28, // 5: kopia_repository.PutManifestRequest.labels:type_name -> kopia_repository.PutManifestRequest.LabelsEntry
-	29, // 6: kopia_repository.FindManifestsRequest.labels:type_name -> kopia_repository.FindManifestsRequest.LabelsEntry
+	30, // 5: kopia_repository.PutManifestRequest.labels:type_name -> kopia_repository.PutManifestRequest.LabelsEntry
+	31, // 6: kopia_repository.FindManifestsRequest.labels:type_name -> kopia_repository.FindManifestsRequest.LabelsEntry
 	2,  // 7: kopia_repository.FindManifestsResponse.metadata:type_name -> kopia_repository.ManifestEntryMetadata
-	30, // 8: kopia_repository.SessionRequest.trace_context:type_name -> kopia_repository.SessionRequest.TraceContextEntry
+	32, // 8: kopia_repository.SessionRequest.trace_context:type_name -> kopia_repository.SessionRequest.TraceContextEntry
 	5,  // 9: kopia_repository.SessionRequest.initialize_session:type_name -> kopia_repository.InitializeSessionRequest
 	7,  // 10: kopia_repository.SessionRequest.get_content_info:type_name -> kopia_repository.GetContentInfoRequest
 	11, // 11: kopia_repository.SessionRequest.flush:type_name -> kopia_repository.FlushRequest
@@ -2154,24 +2309,26 @@ func file_repository_server_proto_rawDescGZIP() []byte {
 	21, // 16: kopia_repository.SessionRequest.find_manifests:type_name -> kopia_repository.FindManifestsRequest
 	19, // 17: kopia_repository.SessionRequest.delete_manifest:type_name -> kopia_repository.DeleteManifestRequest
 	23, // 18: kopia_repository.SessionRequest.prefetch_contents:type_name -> kopia_repository.PrefetchContentsRequest
-	3,  // 19: kopia_repository.SessionResponse.error:type_name -> kopia_repository.ErrorResponse
-	6,  // 20: kopia_repository.SessionResponse.initialize_session:type_name -> kopia_repository.InitializeSessionResponse
-	8,  // 21: kopia_repository.SessionResponse.get_content_info:type_name -> kopia_repository.GetContentInfoResponse
-	12, // 22: kopia_repository.SessionResponse.flush:type_name -> kopia_repository.FlushResponse
-	14, // 23: kopia_repository.SessionResponse.write_content:type_name -> kopia_repository.WriteContentResponse
-	10, // 24: kopia_repository.SessionResponse.get_content:type_name -> kopia_repository.GetContentResponse
-	16, // 25: kopia_repository.SessionResponse.get_manifest:type_name -> kopia_repository.GetManifestResponse
-	18, // 26: kopia_repository.SessionResponse.put_manifest:type_name -> kopia_repository.PutManifestResponse
-	22, // 27: kopia_repository.SessionResponse.find_manifests:type_name -> kopia_repository.FindManifestsResponse
-	20, // 28: kopia_repository.SessionResponse.delete_manifest:type_name -> kopia_repository.DeleteManifestResponse
-	24, // 29: kopia_repository.SessionResponse.prefetch_contents:type_name -> kopia_repository.PrefetchContentsResponse
-	25, // 30: kopia_repository.KopiaRepository.Session:input_type -> kopia_repository.SessionRequest
-	26, // 31: kopia_repository.KopiaRepository.Session:output_type -> kopia_repository.SessionResponse
-	31, // [31:32] is the sub-list for method output_type
-	30, // [30:31] is the sub-list for method input_type
-	30, // [30:30] is the sub-list for extension type_name
-	30, // [30:30] is the sub-list for extension extendee
-	0,  // [0:30] is the sub-list for field type_name
+	25, // 19: kopia_repository.SessionRequest.apply_retention_policy:type_name -> kopia_repository.ApplyRetentionPolicyRequest
+	3,  // 20: kopia_repository.SessionResponse.error:type_name -> kopia_repository.ErrorResponse
+	6,  // 21: kopia_repository.SessionResponse.initialize_session:type_name -> kopia_repository.InitializeSessionResponse
+	8,  // 22: kopia_repository.SessionResponse.get_content_info:type_name -> kopia_repository.GetContentInfoResponse
+	12, // 23: kopia_repository.SessionResponse.flush:type_name -> kopia_repository.FlushResponse
+	14, // 24: kopia_repository.SessionResponse.write_content:type_name -> kopia_repository.WriteContentResponse
+	10, // 25: kopia_repository.SessionResponse.get_content:type_name -> kopia_repository.GetContentResponse
+	16, // 26: kopia_repository.SessionResponse.get_manifest:type_name -> kopia_repository.GetManifestResponse
+	18, // 27: kopia_repository.SessionResponse.put_manifest:type_name -> kopia_repository.PutManifestResponse
+	22, // 28: kopia_repository.SessionResponse.find_manifests:type_name -> kopia_repository.FindManifestsResponse
+	20, // 29: kopia_repository.SessionResponse.delete_manifest:type_name -> kopia_repository.DeleteManifestResponse
+	24, // 30: kopia_repository.SessionResponse.prefetch_contents:type_name -> kopia_repository.PrefetchContentsResponse
+	26, // 31: kopia_repository.SessionResponse.apply_retention_policy:type_name -> kopia_repository.ApplyRetentionPolicyResponse
+	27, // 32: kopia_repository.KopiaRepository.Session:input_type -> kopia_repository.SessionRequest
+	28, // 33: kopia_repository.KopiaRepository.Session:output_type -> kopia_repository.SessionResponse
+	33, // [33:34] is the sub-list for method output_type
+	32, // [32:33] is the sub-list for method input_type
+	32, // [32:32] is the sub-list for extension type_name
+	32, // [32:32] is the sub-list for extension extendee
+	0,  // [0:32] is the sub-list for field type_name
 }
 
 func init() { file_repository_server_proto_init() }
@@ -2469,7 +2626,7 @@ func file_repository_server_proto_init() {
 			}
 		}
 		file_repository_server_proto_msgTypes[24].Exporter = func(v interface{}, i int) interface{} {
-			switch v := v.(*SessionRequest); i {
+			switch v := v.(*ApplyRetentionPolicyRequest); i {
 			case 0:
 				return &v.state
 			case 1:
@@ -2481,6 +2638,30 @@ func file_repository_server_proto_init() {
 			}
 		}
 		file_repository_server_proto_msgTypes[25].Exporter = func(v interface{}, i int) interface{} {
+			switch v := v.(*ApplyRetentionPolicyResponse); i {
+			case 0:
+				return &v.state
+			case 1:
+				return &v.sizeCache
+			case 2:
+				return &v.unknownFields
+			default:
+				return nil
+			}
+		}
+		file_repository_server_proto_msgTypes[26].Exporter = func(v interface{}, i int) interface{} {
+			switch v := v.(*SessionRequest); i {
+			case 0:
+				return &v.state
+			case 1:
+				return &v.sizeCache
+			case 2:
+				return &v.unknownFields
+			default:
+				return nil
+			}
+		}
+		file_repository_server_proto_msgTypes[27].Exporter = func(v interface{}, i int) interface{} {
 			switch v := v.(*SessionResponse); i {
 			case 0:
 				return &v.state
@@ -2493,7 +2674,7 @@ func file_repository_server_proto_init() {
 			}
 		}
 	}
-	file_repository_server_proto_msgTypes[24].OneofWrappers = []interface{}{
+	file_repository_server_proto_msgTypes[26].OneofWrappers = []interface{}{
 		(*SessionRequest_InitializeSession)(nil),
 		(*SessionRequest_GetContentInfo)(nil),
 		(*SessionRequest_Flush)(nil),
@@ -2504,8 +2685,9 @@ func file_repository_server_proto_init() {
 		(*SessionRequest_FindManifests)(nil),
 		(*SessionRequest_DeleteManifest)(nil),
 		(*SessionRequest_PrefetchContents)(nil),
+		(*SessionRequest_ApplyRetentionPolicy)(nil),
 	}
-	file_repository_server_proto_msgTypes[25].OneofWrappers = []interface{}{
+	file_repository_server_proto_msgTypes[27].OneofWrappers = []interface{}{
 		(*SessionResponse_Error)(nil),
 		(*SessionResponse_InitializeSession)(nil),
 		(*SessionResponse_GetContentInfo)(nil),
@@ -2517,6 +2699,7 @@ func file_repository_server_proto_init() {
 		(*SessionResponse_FindManifests)(nil),
 		(*SessionResponse_DeleteManifest)(nil),
 		(*SessionResponse_PrefetchContents)(nil),
+		(*SessionResponse_ApplyRetentionPolicy)(nil),
 	}
 	type x struct{}
 	out := protoimpl.TypeBuilder{
@@ -2524,7 +2707,7 @@ type x struct{
 			GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
 			RawDescriptor: file_repository_server_proto_rawDesc,
 			NumEnums:      1,
-			NumMessages:   30,
+			NumMessages:   32,
 			NumExtensions: 0,
 			NumServices:   1,
 		},
diff --git a/internal/grpcapi/repository_server.proto b/internal/grpcapi/repository_server.proto
index 7969a6ce..c6edd354 100644
--- a/internal/grpcapi/repository_server.proto
+++ b/internal/grpcapi/repository_server.proto
@@ -136,6 +136,15 @@ message PrefetchContentsResponse {
   repeated string content_ids = 1;
 }
 
+message ApplyRetentionPolicyRequest {
+  string source_path = 1;
+  bool really_delete = 2;
+}
+
+message ApplyRetentionPolicyResponse {
+  repeated string manifest_ids = 1;
+}
+
 message SessionRequest {
   int64 request_id = 1;
   map<string,string> trace_context = 2;
@@ -154,6 +163,7 @@ message SessionRequest {
     FindManifestsRequest find_manifests = 17;
     DeleteManifestRequest delete_manifest = 18;
     PrefetchContentsRequest prefetch_contents = 19;
+    ApplyRetentionPolicyRequest apply_retention_policy = 20;
   }
 }
 
@@ -174,6 +184,7 @@ message SessionResponse {
     FindManifestsResponse find_manifests = 17;
     DeleteManifestResponse delete_manifest = 18;
     PrefetchContentsResponse prefetch_contents = 19;
+    ApplyRetentionPolicyResponse apply_retention_policy = 20;
   }
 }
 
diff --git a/internal/remoterepoapi/remoterepoapi.go b/internal/remoterepoapi/remoterepoapi.go
index 4deec668..90534ac8 100644
--- a/internal/remoterepoapi/remoterepoapi.go
+++ b/internal/remoterepoapi/remoterepoapi.go
@@ -37,7 +37,18 @@ type PrefetchContentsRequest struct {
 	Hint       string       `json:"hint"`
 }
 
-// PrefetchContentsResponse represents a request from request to prefetch contents.
+// PrefetchContentsResponse represents a response from request to prefetch contents.
 type PrefetchContentsResponse struct {
 	ContentIDs []content.ID `json:"contents"`
 }
+
+// ApplyRetentionPolicyRequest represents a request to apply retention policy to a given source path.
+type ApplyRetentionPolicyRequest struct {
+	SourcePath   string `json:"sourcePath"`
+	ReallyDelete bool   `json:"reallyDelete"`
+}
+
+// ApplyRetentionPolicyResponse represents a response to a request to apply retention policy.
+type ApplyRetentionPolicyResponse struct {
+	ManifestIDs []manifest.ID `json:"manifests"`
+}
diff --git a/internal/server/api_manifest.go b/internal/server/api_manifest.go
index abf47e2a..48cf405e 100644
--- a/internal/server/api_manifest.go
+++ b/internal/server/api_manifest.go
@@ -3,6 +3,7 @@
 import (
 	"context"
 	"encoding/json"
+	"strings"
 
 	"github.com/pkg/errors"
 
@@ -11,6 +12,8 @@
 	"github.com/kopia/kopia/internal/serverapi"
 	"github.com/kopia/kopia/repo"
 	"github.com/kopia/kopia/repo/manifest"
+	"github.com/kopia/kopia/snapshot"
+	"github.com/kopia/kopia/snapshot/policy"
 )
 
 func handleManifestGet(ctx context.Context, rc requestContext) (interface{}, *apiError) {
@@ -123,3 +126,47 @@ func handleManifestCreate(ctx context.Context, rc requestContext) (interface{},
 
 	return &manifest.EntryMetadata{ID: id}, nil
 }
+
+func handleApplyRetentionPolicy(ctx context.Context, rc requestContext) (interface{}, *apiError) {
+	rw, ok := rc.rep.(repo.RepositoryWriter)
+	if !ok {
+		return nil, repositoryNotWritableError()
+	}
+
+	var req remoterepoapi.ApplyRetentionPolicyRequest
+
+	if err := json.Unmarshal(rc.body, &req); err != nil {
+		return nil, requestError(serverapi.ErrorMalformedRequest, "malformed request")
+	}
+
+	usernameAtHostname, _, _ := rc.req.BasicAuth()
+
+	parts := strings.Split(usernameAtHostname, "@")
+	if len(parts) != 2 { //nolint:gomnd
+		return nil, requestError(serverapi.ErrorMalformedRequest, "malformed username")
+	}
+
+	// only allow users to apply retention policy if they have permission to add snapshots
+	// for a particular path.
+	if !hasManifestAccess(ctx, rc, map[string]string{
+		manifest.TypeLabelKey:  snapshot.ManifestType,
+		snapshot.UsernameLabel: parts[0],
+		snapshot.HostnameLabel: parts[1],
+		snapshot.PathLabel:     req.SourcePath,
+	}, auth.AccessLevelAppend) {
+		return nil, accessDeniedError()
+	}
+
+	ids, err := policy.ApplyRetentionPolicy(ctx, rw, snapshot.SourceInfo{
+		UserName: parts[0],
+		Host:     parts[1],
+		Path:     req.SourcePath,
+	}, req.ReallyDelete)
+	if err != nil {
+		return nil, internalServerError(err)
+	}
+
+	return &remoterepoapi.ApplyRetentionPolicyResponse{
+		ManifestIDs: ids,
+	}, nil
+}
diff --git a/internal/server/grpc_session.go b/internal/server/grpc_session.go
index b49ca085..3dbe2cc4 100644
--- a/internal/server/grpc_session.go
+++ b/internal/server/grpc_session.go
@@ -26,6 +26,8 @@
 	"github.com/kopia/kopia/repo/content"
 	"github.com/kopia/kopia/repo/manifest"
 	"github.com/kopia/kopia/repo/object"
+	"github.com/kopia/kopia/snapshot"
+	"github.com/kopia/kopia/snapshot/policy"
 )
 
 type grpcServerState struct {
@@ -82,12 +84,12 @@ func (s *Server) Session(srv grpcapi.KopiaRepository_SessionServer) error {
 		return status.Errorf(codes.Unavailable, "not connected to a direct repository")
 	}
 
-	username, err := s.authenticateGRPCSession(ctx, dr)
+	usernameAtHostname, err := s.authenticateGRPCSession(ctx, dr)
 	if err != nil {
 		return err
 	}
 
-	authz := s.authorizer.Authorize(ctx, dr, username)
+	authz := s.authorizer.Authorize(ctx, dr, usernameAtHostname)
 	if authz == nil {
 		authz = auth.NoAccess()
 	}
@@ -97,8 +99,8 @@ func (s *Server) Session(srv grpcapi.KopiaRepository_SessionServer) error {
 		return status.Errorf(codes.PermissionDenied, "peer not found in context")
 	}
 
-	log(ctx).Infof("starting session for user %q from %v", username, p.Addr)
-	defer log(ctx).Infof("session ended for user %q from %v", username, p.Addr)
+	log(ctx).Infof("starting session for user %q from %v", usernameAtHostname, p.Addr)
+	defer log(ctx).Infof("session ended for user %q from %v", usernameAtHostname, p.Addr)
 
 	opt, err := s.handleInitialSessionHandshake(srv, dr)
 	if err != nil {
@@ -131,7 +133,7 @@ func (s *Server) Session(srv grpcapi.KopiaRepository_SessionServer) error {
 			go func() {
 				defer s.grpcServerState.sem.Release(1)
 
-				handleSessionRequest(ctx, dw, authz, req, func(resp *grpcapi.SessionResponse) {
+				handleSessionRequest(ctx, dw, authz, usernameAtHostname, req, func(resp *grpcapi.SessionResponse) {
 					if err := s.send(srv, req.RequestId, resp); err != nil {
 						select {
 						case lastErr <- err:
@@ -148,7 +150,7 @@ func (s *Server) Session(srv grpcapi.KopiaRepository_SessionServer) error {
 
 var tracer = otel.Tracer("kopia/grpc")
 
-func handleSessionRequest(ctx context.Context, dw repo.DirectRepositoryWriter, authz auth.AuthorizationInfo, req *grpcapi.SessionRequest, respond func(*grpcapi.SessionResponse)) {
+func handleSessionRequest(ctx context.Context, dw repo.DirectRepositoryWriter, authz auth.AuthorizationInfo, usernameAtHostname string, req *grpcapi.SessionRequest, respond func(*grpcapi.SessionResponse)) {
 	if req.TraceContext != nil {
 		var tc propagation.TraceContext
 		ctx = tc.Extract(ctx, propagation.MapCarrier(req.TraceContext))
@@ -182,6 +184,9 @@ func handleSessionRequest(ctx context.Context, dw repo.DirectRepositoryWriter, a
 	case *grpcapi.SessionRequest_PrefetchContents:
 		respond(handlePrefetchContentsRequest(ctx, dw, authz, inner.PrefetchContents))
 
+	case *grpcapi.SessionRequest_ApplyRetentionPolicy:
+		respond(handleApplyRetentionPolicyRequest(ctx, dw, authz, usernameAtHostname, inner.ApplyRetentionPolicy))
+
 	case *grpcapi.SessionRequest_InitializeSession:
 		respond(errorResponse(errors.Errorf("InitializeSession must be the first request in a session")))
 
@@ -440,6 +445,47 @@ func handlePrefetchContentsRequest(ctx context.Context, rep repo.Repository, aut
 	}
 }
 
+func handleApplyRetentionPolicyRequest(ctx context.Context, rep repo.RepositoryWriter, authz auth.AuthorizationInfo, usernameAtHostname string, req *grpcapi.ApplyRetentionPolicyRequest) *grpcapi.SessionResponse {
+	ctx, span := tracer.Start(ctx, "GRPCSession.ApplyRetentionPolicy")
+	defer span.End()
+
+	parts := strings.Split(usernameAtHostname, "@")
+	if len(parts) != 2 { //nolint:gomnd
+		return errorResponse(errors.Errorf("invalid username@hostname: %q", usernameAtHostname))
+	}
+
+	username := parts[0]
+	hostname := parts[1]
+
+	// only allow users to apply retention policy if they have permission to add snapshots
+	// for a particular path.
+	if authz.ManifestAccessLevel(map[string]string{
+		manifest.TypeLabelKey:  snapshot.ManifestType,
+		snapshot.UsernameLabel: username,
+		snapshot.HostnameLabel: hostname,
+		snapshot.PathLabel:     req.SourcePath,
+	}) < auth.AccessLevelAppend {
+		return accessDeniedResponse()
+	}
+
+	manifestIDs, err := policy.ApplyRetentionPolicy(ctx, rep, snapshot.SourceInfo{
+		Host:     hostname,
+		UserName: username,
+		Path:     req.SourcePath,
+	}, req.ReallyDelete)
+	if err != nil {
+		return errorResponse(err)
+	}
+
+	return &grpcapi.SessionResponse{
+		Response: &grpcapi.SessionResponse_ApplyRetentionPolicy{
+			ApplyRetentionPolicy: &grpcapi.ApplyRetentionPolicyResponse{
+				ManifestIds: manifest.IDsToStrings(manifestIDs),
+			},
+		},
+	}
+}
+
 func accessDeniedResponse() *grpcapi.SessionResponse {
 	return &grpcapi.SessionResponse{
 		Response: &grpcapi.SessionResponse_Error{
diff --git a/internal/server/server.go b/internal/server/server.go
index d0729133..a9917ea6 100644
--- a/internal/server/server.go
+++ b/internal/server/server.go
@@ -163,6 +163,7 @@ func (s *Server) SetupRepositoryAPIHandlers(m *mux.Router) {
 	m.HandleFunc("/api/v1/manifests/{manifestID}", s.handleRepositoryAPI(handlerWillCheckAuthorization, handleManifestDelete)).Methods(http.MethodDelete)
 	m.HandleFunc("/api/v1/manifests", s.handleRepositoryAPI(handlerWillCheckAuthorization, handleManifestCreate)).Methods(http.MethodPost)
 	m.HandleFunc("/api/v1/manifests", s.handleRepositoryAPI(handlerWillCheckAuthorization, handleManifestList)).Methods(http.MethodGet)
+	m.HandleFunc("/api/v1/policies/apply-retention", s.handleRepositoryAPI(handlerWillCheckAuthorization, handleApplyRetentionPolicy)).Methods(http.MethodPost)
 }
 
 // SetupControlAPIHandlers registers control API handlers.
@@ -865,6 +866,7 @@ type Options struct {
 	UIPreferencesFile      string // name of the JSON file storing UI preferences
 	ServerControlUser      string // name of the user allowed to access the server control API
 	DisableCSRFTokenChecks bool
+	PersistentLogs         bool
 	UITitlePrefix          string
 }
 
@@ -1047,7 +1049,7 @@ func New(ctx context.Context, options *Options) (*Server, error) {
 		grpcServerState:      makeGRPCServerState(options.MaxConcurrency),
 		authenticator:        options.Authenticator,
 		authorizer:           options.Authorizer,
-		taskmgr:              uitask.NewManager(),
+		taskmgr:              uitask.NewManager(options.PersistentLogs),
 		mounts:               map[object.ID]mount.Controller{},
 		authCookieSigningKey: []byte(options.AuthCookieSigningKey),
 	}
diff --git a/internal/uitask/uitask_manager.go b/internal/uitask/uitask_manager.go
index 17bafac4..3a05ee4e 100644
--- a/internal/uitask/uitask_manager.go
+++ b/internal/uitask/uitask_manager.go
@@ -32,6 +32,8 @@ type Manager struct {
 
 	MaxFinishedTasks      int // +checklocksignore
 	MaxLogMessagesPerTask int // +checklocksignore
+
+	persistentLogs bool // +checklocksignore
 }
 
 // Controller allows the task to communicate with task manager and receive signals.
@@ -56,7 +58,13 @@ func (m *Manager) Run(ctx context.Context, kind, description string, task TaskFu
 		maxLogMessages: m.MaxLogMessagesPerTask,
 	}
 
-	ctx = logging.WithLogger(ctx, r.loggerForModule)
+	if m.persistentLogs {
+		// log to regular file logger in addition to in-memory buffers.
+		ctx = logging.WithAdditionalLogger(ctx, r.loggerForModule)
+	} else {
+		// only log to in-memory buffers.
+		ctx = logging.WithLogger(ctx, r.loggerForModule)
+	}
 
 	m.startTask(r)
 
@@ -251,12 +259,13 @@ func (m *Manager) completeTask(r *runningTaskInfo, err error) {
 }
 
 // NewManager creates new UI Task Manager.
-func NewManager() *Manager {
+func NewManager(alsoLogToFile bool) *Manager {
 	return &Manager{
 		running:  map[string]*runningTaskInfo{},
 		finished: map[string]*Info{},
 
 		MaxLogMessagesPerTask: maxLogMessagesPerTask,
 		MaxFinishedTasks:      maxFinishedTasks,
+		persistentLogs:        alsoLogToFile,
 	}
 }
diff --git a/repo/api_server_repository.go b/repo/api_server_repository.go
index 154ddd9c..f4e44a99 100644
--- a/repo/api_server_repository.go
+++ b/repo/api_server_repository.go
@@ -282,6 +282,19 @@ func (r *apiServerRepository) PrefetchContents(ctx context.Context, contentIDs [
 	return resp.ContentIDs
 }
 
+func (r *apiServerRepository) ApplyRetentionPolicy(ctx context.Context, sourcePath string, reallyDelete bool) ([]manifest.ID, error) {
+	var result remoterepoapi.ApplyRetentionPolicyResponse
+
+	if err := r.cli.Post(ctx, "policies/apply-retention", remoterepoapi.ApplyRetentionPolicyRequest{
+		SourcePath:   sourcePath,
+		ReallyDelete: reallyDelete,
+	}, &result); err != nil {
+		return nil, errors.Wrap(err, "unable to apply retention policy")
+	}
+
+	return result.ManifestIDs, nil
+}
+
 // OnSuccessfulFlush registers the provided callback to be invoked after flush succeeds.
 func (r *apiServerRepository) OnSuccessfulFlush(callback RepositoryWriterCallback) {
 	r.afterFlush = append(r.afterFlush, callback)
diff --git a/repo/content/committed_read_manager.go b/repo/content/committed_read_manager.go
index 97d6aaa7..04baec19 100644
--- a/repo/content/committed_read_manager.go
+++ b/repo/content/committed_read_manager.go
@@ -593,7 +593,9 @@ func (sm *SharedManager) CloseShared(ctx context.Context) error {
 func (sm *SharedManager) AlsoLogToContentLog(ctx context.Context) context.Context {
 	sm.repoLogManager.Enable()
 
-	return logging.AlsoLogTo(ctx, sm.log)
+	return logging.WithAdditionalLogger(ctx, func(module string) logging.Logger {
+		return sm.log
+	})
 }
 
 func (sm *SharedManager) shouldRefreshIndexes() bool {
diff --git a/repo/grpc_repository_client.go b/repo/grpc_repository_client.go
index 809128da..81275eab 100644
--- a/repo/grpc_repository_client.go
+++ b/repo/grpc_repository_client.go
@@ -441,6 +441,33 @@ func (r *grpcInnerSession) PrefetchContents(ctx context.Context, contentIDs []co
 	return nil
 }
 
+func (r *grpcRepositoryClient) ApplyRetentionPolicy(ctx context.Context, sourcePath string, reallyDelete bool) ([]manifest.ID, error) {
+	return inSessionWithoutRetry(ctx, r, func(ctx context.Context, sess *grpcInnerSession) ([]manifest.ID, error) {
+		return sess.ApplyRetentionPolicy(ctx, sourcePath, reallyDelete)
+	})
+}
+
+func (r *grpcInnerSession) ApplyRetentionPolicy(ctx context.Context, sourcePath string, reallyDelete bool) ([]manifest.ID, error) {
+	for resp := range r.sendRequest(ctx, &apipb.SessionRequest{
+		Request: &apipb.SessionRequest_ApplyRetentionPolicy{
+			ApplyRetentionPolicy: &apipb.ApplyRetentionPolicyRequest{
+				SourcePath:   sourcePath,
+				ReallyDelete: reallyDelete,
+			},
+		},
+	}) {
+		switch rr := resp.Response.(type) {
+		case *apipb.SessionResponse_ApplyRetentionPolicy:
+			return manifest.IDsFromStrings(rr.ApplyRetentionPolicy.ManifestIds), nil
+
+		default:
+			return nil, unhandledSessionResponse(resp)
+		}
+	}
+
+	return nil, errNoSessionResponse()
+}
+
 func (r *grpcRepositoryClient) Time() time.Time {
 	return clock.Now()
 }
diff --git a/repo/logging/broadcast.go b/repo/logging/broadcast.go
index b6b79c21..af2504da 100644
--- a/repo/logging/broadcast.go
+++ b/repo/logging/broadcast.go
@@ -9,9 +9,21 @@
 func Broadcast(logger ...Logger) Logger {
 	var cores []zapcore.Core
 
+	var singleName string
+
 	for _, l := range logger {
-		cores = append(cores, l.Desugar().Core())
+		dl := l.Desugar()
+
+		if singleName == "" {
+			singleName = dl.Name()
+		}
+
+		if dl.Name() != singleName {
+			singleName = "-"
+		}
+
+		cores = append(cores, dl.Core())
 	}
 
-	return zap.New(zapcore.NewTee(cores...)).Sugar()
+	return zap.New(zapcore.NewTee(cores...)).Sugar().Named(singleName)
 }
diff --git a/repo/logging/ctx.go b/repo/logging/ctx.go
index da341acb..2c21b67f 100644
--- a/repo/logging/ctx.go
+++ b/repo/logging/ctx.go
@@ -34,6 +34,15 @@ func WithLogger(ctx context.Context, l LoggerFactory) context.Context {
 	})
 }
 
+// WithAdditionalLogger returns a context where all logging is emitted the original output plus the provided logger factory.
+func WithAdditionalLogger(ctx context.Context, fact LoggerFactory) context.Context {
+	originalLogFactory := loggerFactoryFromContext(ctx)
+
+	return WithLogger(ctx, func(module string) Logger {
+		return Broadcast(originalLogFactory(module), fact(module))
+	})
+}
+
 // loggerFactoryFromContext returns a LoggerFactory associated with current context.
 func loggerFactoryFromContext(ctx context.Context) LoggerFactory {
 	v := ctx.Value(loggerCacheKey)
@@ -43,12 +52,3 @@ func loggerFactoryFromContext(ctx context.Context) LoggerFactory {
 
 	return v.(*loggerCache).getLogger //nolint:forcetypeassert
 }
-
-// AlsoLogTo returns a context where all logging is emitted the original output plus the provided loggers.
-func AlsoLogTo(ctx context.Context, loggers ...Logger) context.Context {
-	originalLogFactory := loggerFactoryFromContext(ctx)
-
-	return WithLogger(ctx, func(module string) Logger {
-		return Broadcast(append([]Logger{originalLogFactory(module)}, loggers...)...)
-	})
-}
diff --git a/repo/manifest/manifest_manager.go b/repo/manifest/manifest_manager.go
index 9b02459d..8bb2f39f 100644
--- a/repo/manifest/manifest_manager.go
+++ b/repo/manifest/manifest_manager.go
@@ -253,6 +253,28 @@ func (m *Manager) Compact(ctx context.Context) error {
 	return m.committed.compact(ctx)
 }
 
+// IDsToStrings converts the IDs to strings.
+func IDsToStrings(input []ID) []string {
+	var result []string
+
+	for _, v := range input {
+		result = append(result, string(v))
+	}
+
+	return result
+}
+
+// IDsFromStrings converts the IDs to strings.
+func IDsFromStrings(input []string) []ID {
+	var result []ID
+
+	for _, v := range input {
+		result = append(result, ID(v))
+	}
+
+	return result
+}
+
 func copyLabels(m map[string]string) map[string]string {
 	r := map[string]string{}
 	for k, v := range m {
diff --git a/repo/repository.go b/repo/repository.go
index 7883f35f..1110625b 100644
--- a/repo/repository.go
+++ b/repo/repository.go
@@ -54,6 +54,12 @@ type RepositoryWriter interface {
 	Flush(ctx context.Context) error
 }
 
+// RemoteRetentionPolicy is an interface implemented by repository clients that support remote retention policy.
+// when implemented, the repository server will invoke ApplyRetentionPolicy() server-side.
+type RemoteRetentionPolicy interface {
+	ApplyRetentionPolicy(ctx context.Context, sourcePath string, reallyDelete bool) ([]manifest.ID, error)
+}
+
 // DirectRepository provides additional low-level repository functionality.
 //
 //nolint:interfacebloat
diff --git a/snapshot/policy/expire.go b/snapshot/policy/expire.go
index 7bf08645..fbf05b8a 100644
--- a/snapshot/policy/expire.go
+++ b/snapshot/policy/expire.go
@@ -7,11 +7,24 @@
 	"github.com/pkg/errors"
 
 	"github.com/kopia/kopia/repo"
+	"github.com/kopia/kopia/repo/manifest"
 	"github.com/kopia/kopia/snapshot"
 )
 
 // ApplyRetentionPolicy applies retention policy to a given source by deleting expired snapshots.
-func ApplyRetentionPolicy(ctx context.Context, rep repo.RepositoryWriter, sourceInfo snapshot.SourceInfo, reallyDelete bool) ([]*snapshot.Manifest, error) {
+func ApplyRetentionPolicy(ctx context.Context, rep repo.RepositoryWriter, sourceInfo snapshot.SourceInfo, reallyDelete bool) ([]manifest.ID, error) {
+	// it is desired to not allow snapshots to be deleted by repository clients,
+	// while still maintain the ability to apply snapshot retention policies server-side.
+	if remote, ok := rep.(repo.RemoteRetentionPolicy); ok {
+		if sourceInfo.UserName == rep.ClientOptions().Username && sourceInfo.Host == rep.ClientOptions().Hostname {
+			// for repository clients, apply retention policy on the server.
+			log(ctx).Debug("applying retention policy on the server")
+
+			//nolint:wrapcheck
+			return remote.ApplyRetentionPolicy(ctx, sourceInfo.Path, reallyDelete)
+		}
+	}
+
 	snapshots, err := snapshot.ListSnapshots(ctx, rep, sourceInfo)
 	if err != nil {
 		return nil, errors.Wrap(err, "error listing snapshots")
@@ -23,9 +36,9 @@ func ApplyRetentionPolicy(ctx context.Context, rep repo.RepositoryWriter, source
 	}
 
 	if reallyDelete {
-		for _, it := range toDelete {
-			if err := rep.DeleteManifest(ctx, it.ID); err != nil {
-				return toDelete, errors.Wrapf(err, "error deleting manifest %v", it.ID)
+		for _, manifestID := range toDelete {
+			if err := rep.DeleteManifest(ctx, manifestID); err != nil {
+				return toDelete, errors.Wrapf(err, "error deleting manifest %v", manifestID)
 			}
 		}
 	}
@@ -33,8 +46,8 @@ func ApplyRetentionPolicy(ctx context.Context, rep repo.RepositoryWriter, source
 	return toDelete, nil
 }
 
-func getExpiredSnapshots(ctx context.Context, rep repo.Repository, snapshots []*snapshot.Manifest) ([]*snapshot.Manifest, error) {
-	var toDelete []*snapshot.Manifest
+func getExpiredSnapshots(ctx context.Context, rep repo.Repository, snapshots []*snapshot.Manifest) ([]manifest.ID, error) {
+	var toDelete []manifest.ID
 
 	for _, snapshotGroup := range snapshot.GroupBySource(snapshots) {
 		td, err := getExpiredSnapshotsForSource(ctx, rep, snapshotGroup)
@@ -48,7 +61,7 @@ func getExpiredSnapshots(ctx context.Context, rep repo.Repository, snapshots []*
 	return toDelete, nil
 }
 
-func getExpiredSnapshotsForSource(ctx context.Context, rep repo.Repository, snapshots []*snapshot.Manifest) ([]*snapshot.Manifest, error) {
+func getExpiredSnapshotsForSource(ctx context.Context, rep repo.Repository, snapshots []*snapshot.Manifest) ([]manifest.ID, error) {
 	src := snapshots[0].Source
 
 	pol, _, _, err := GetEffectivePolicy(ctx, rep, src)
@@ -58,12 +71,12 @@ func getExpiredSnapshotsForSource(ctx context.Context, rep repo.Repository, snap
 
 	pol.RetentionPolicy.ComputeRetentionReasons(snapshots)
 
-	var toDelete []*snapshot.Manifest
+	var toDelete []manifest.ID
 
 	for _, s := range snapshots {
 		if len(s.RetentionReasons) == 0 && len(s.Pins) == 0 {
 			log(ctx).Debugf("  deleting %v", s.StartTime)
-			toDelete = append(toDelete, s)
+			toDelete = append(toDelete, s.ID)
 		} else {
 			log(ctx).Debugf("  keeping %v retention: [%v] pins: [%v]", s.StartTime, strings.Join(s.RetentionReasons, ","), strings.Join(s.Pins, ","))
 		}
diff --git a/tools/gettool/autodownload/autodownload.go b/tools/gettool/autodownload/autodownload.go
index 398636cc..e6fc5fff 100644
--- a/tools/gettool/autodownload/autodownload.go
+++ b/tools/gettool/autodownload/autodownload.go
@@ -158,8 +158,66 @@ func unzip(dir string, r io.Reader, stripPathComponents int) error {
 	return nil
 }
 
-// Download downloads the provided.
-func Download(url, dir string, checksum map[string]string, stripPathComponents int) (err error) {
+// Download downloads the provided URL and extracts it to the provided directory, retrying
+// exponentionally until succeeded.
+func Download(url, dir string, checksum map[string]string, stripPathComponents int) error {
+	const (
+		// sleep durations 5, 10, 20, 40, 80, 160, 320
+		// total: 635 seconds, ~10 minutes
+		maxRetries       = 8
+		initialSleepTime = 5 * time.Second
+	)
+
+	nextSleepTime := initialSleepTime
+
+	for i := 0; i < maxRetries; i++ {
+		err := downloadInternal(url, dir, checksum, stripPathComponents)
+		if err == nil {
+			// success
+			return nil
+		}
+
+		// 404 is non-retryable
+		if errors.Is(err, errNotFound) {
+			return errors.Wrap(err, "non-retryable")
+		}
+
+		// invalid checksum is non-retryable
+		var ec InvalidChecksumError
+		if errors.As(err, &ec) {
+			// invalid checksum, do not retry.
+			return errors.Wrap(err, "non-retryable")
+		}
+
+		// all other errors are retryable
+		if i != maxRetries-1 {
+			log.Printf("Attempt #%v failed, sleeping for %v: %v", i, nextSleepTime, err)
+			time.Sleep(nextSleepTime)
+
+			nextSleepTime *= 2
+
+			if err := os.RemoveAll(dir); err != nil {
+				log.Printf("unable to remove %v: %v", dir, err)
+			}
+		}
+	}
+
+	return errors.Errorf("unable to download %v", url)
+}
+
+// InvalidChecksumError is returned by Download when the checksum of the downloaded file does not match the expected checksum.
+type InvalidChecksumError struct {
+	actual   string
+	expected string
+}
+
+func (e InvalidChecksumError) Error() string {
+	return fmt.Sprintf("invalid checksum: %v, wanted %v", e.actual, e.expected)
+}
+
+var errNotFound = errors.New("not found")
+
+func downloadInternal(url, dir string, checksum map[string]string, stripPathComponents int) (err error) {
 	resp, err := http.Get(url) //nolint:gosec,noctx
 	if err != nil {
 		return errors.Wrapf(err, "unable to get %q", url)
@@ -168,6 +226,10 @@ func Download(url, dir string, checksum map[string]string, stripPathComponents i
 	defer resp.Body.Close() //nolint:errcheck
 
 	if resp.StatusCode != http.StatusOK {
+		if resp.StatusCode == http.StatusNotFound {
+			return errNotFound
+		}
+
 		return errors.Errorf("invalid server response for %q: %v", url, resp.Status)
 	}
 
@@ -187,7 +249,7 @@ func Download(url, dir string, checksum map[string]string, stripPathComponents i
 		return errors.Errorf("missing checksum - calculated as %v", actualChecksum)
 
 	case checksum[url] != actualChecksum:
-		return errors.Errorf("invalid checksum: %v, wanted %v", actualChecksum, checksum[url])
+		return InvalidChecksumError{actualChecksum, checksum[url]}
 
 	default:
 		log.Printf("%v checksum ok", url)
@@ -210,7 +272,7 @@ func Download(url, dir string, checksum map[string]string, stripPathComponents i
 	case strings.HasSuffix(url, ".tar.gz"):
 		return errors.Wrap(untar(dir, r, stripPathComponents), "untar error")
 	case strings.HasSuffix(url, ".zip"):
-		return errors.Wrap(unzip(dir, r, stripPathComponents), "untar error")
+		return errors.Wrap(unzip(dir, r, stripPathComponents), "unzip error")
 	default:
 		return errors.Errorf("unsupported archive format")
 	}


Test patch:
diff --git a/.github/workflows/compat-test.yml b/.github/workflows/compat-test.yml
index b70c1150..34b090cd 100644
--- a/.github/workflows/compat-test.yml
+++ b/.github/workflows/compat-test.yml
@@ -14,7 +14,7 @@ jobs:
     runs-on: ubuntu-latest
     steps:
     - name: Check out repository
-      uses: actions/checkout@v3
+      uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4.0.0
       with:
         fetch-depth: 0
     - name: Set up Go
diff --git a/.github/workflows/endurance-test.yml b/.github/workflows/endurance-test.yml
index 9d000290..6529137c 100644
--- a/.github/workflows/endurance-test.yml
+++ b/.github/workflows/endurance-test.yml
@@ -19,7 +19,7 @@ jobs:
     runs-on: ubuntu-latest
     steps:
     - name: Check out repository
-      uses: actions/checkout@v3
+      uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4.0.0
       with:
         fetch-depth: 0
     - name: Set up Go
diff --git a/.github/workflows/htmlui-tests.yml b/.github/workflows/htmlui-tests.yml
index ce098162..0afc14af 100644
--- a/.github/workflows/htmlui-tests.yml
+++ b/.github/workflows/htmlui-tests.yml
@@ -27,7 +27,7 @@ jobs:
     runs-on: ubuntu-latest
     steps:
     - name: Check out repository
-      uses: actions/checkout@v3
+      uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4.0.0
       with:
         fetch-depth: 0
     - name: Set up Go
diff --git a/.github/workflows/stress-test.yml b/.github/workflows/stress-test.yml
index cdb1186b..4e69d81f 100644
--- a/.github/workflows/stress-test.yml
+++ b/.github/workflows/stress-test.yml
@@ -18,7 +18,7 @@ jobs:
     runs-on: ubuntu-latest
     steps:
     - name: Check out repository
-      uses: actions/checkout@v3
+      uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4.0.0
       with:
         fetch-depth: 0
     - name: Set up Go
diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
index cd9cdae0..b885a8d8 100644
--- a/.github/workflows/tests.yml
+++ b/.github/workflows/tests.yml
@@ -38,7 +38,7 @@ jobs:
     continue-on-error: ${{ contains(matrix.os, 'self-hosted') }}
     steps:
     - name: Check out repository
-      uses: actions/checkout@v3
+      uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4.0.0
       with:
         fetch-depth: 0
     - name: Set up Go
diff --git a/cli/observability_flags_test.go b/cli/observability_flags_test.go
index a218c57c..4e11977b 100644
--- a/cli/observability_flags_test.go
+++ b/cli/observability_flags_test.go
@@ -47,7 +47,6 @@ func TestMetricsPushFlags(t *testing.T) {
 		"--metrics-push-addr="+server.URL,
 		"--metrics-push-interval=30s",
 		"--metrics-push-format=text",
-		"--enable-jaeger-collector", // this has no observable effects whether Jaeger is running or not
 	)
 
 	env.RunAndExpectSuccess(t, "repo", "status",
@@ -79,6 +78,16 @@ func TestMetricsPushFlags(t *testing.T) {
 	)
 }
 
+func TestOTLPFlags(t *testing.T) {
+	env := testenv.NewCLITest(t, testenv.RepoFormatNotImportant, testenv.NewInProcRunner(t))
+
+	// deprecated flag
+	env.RunAndExpectFailure(t, "benchmark", "crypto", "--repeat=1", "--block-size=1KB", "--print-options", "--enable-jaeger-collector")
+
+	// this has no effect whether OTLP collector is running or not.
+	env.RunAndExpectSuccess(t, "benchmark", "crypto", "--repeat=1", "--block-size=1KB", "--print-options", "--otlp-trace")
+}
+
 func TestMetricsSaveToOutputDirFlags(t *testing.T) {
 	env := testenv.NewCLITest(t, testenv.RepoFormatNotImportant, testenv.NewInProcRunner(t))
 
diff --git a/internal/acl/acl_manager_test.go b/internal/acl/acl_manager_test.go
index 02b3146f..c58385ed 100644
--- a/internal/acl/acl_manager_test.go
+++ b/internal/acl/acl_manager_test.go
@@ -175,7 +175,7 @@ func TestLoadEntries(t *testing.T) {
 		Access: acl.AccessLevelFull,
 	}
 
-	require.NoError(t, acl.AddACL(ctx, env.RepositoryWriter, e1))
+	require.NoError(t, acl.AddACL(ctx, env.RepositoryWriter, e1, false))
 
 	entries, err = acl.LoadEntries(ctx, env.RepositoryWriter, entries)
 	require.NoError(t, err)
@@ -192,7 +192,7 @@ func TestLoadEntries(t *testing.T) {
 		Access: acl.AccessLevelFull,
 	}
 
-	require.NoError(t, acl.AddACL(ctx, env.RepositoryWriter, e2))
+	require.NoError(t, acl.AddACL(ctx, env.RepositoryWriter, e2, false))
 
 	entries, err = acl.LoadEntries(ctx, env.RepositoryWriter, entries)
 	require.NoError(t, err)
diff --git a/internal/auth/authz_test.go b/internal/auth/authz_test.go
index 1360a1c6..931c7ab7 100644
--- a/internal/auth/authz_test.go
+++ b/internal/auth/authz_test.go
@@ -109,7 +109,7 @@ func TestDefaultAuthorizer_DefaultACLs(t *testing.T) {
 	ctx, env := repotesting.NewEnvironment(t, repotesting.FormatNotImportant)
 
 	for _, e := range auth.DefaultACLs {
-		require.NoError(t, acl.AddACL(ctx, env.RepositoryWriter, e))
+		require.NoError(t, acl.AddACL(ctx, env.RepositoryWriter, e, false))
 	}
 
 	verifyLegacyAuthorizer(ctx, t, env.Repository, auth.DefaultAuthorizer())
diff --git a/internal/parallelwork/parallel_work_queue_test.go b/internal/parallelwork/parallel_work_queue_test.go
index 9b98f307..408e6fe7 100644
--- a/internal/parallelwork/parallel_work_queue_test.go
+++ b/internal/parallelwork/parallel_work_queue_test.go
@@ -224,18 +224,23 @@ func TestOnNthCompletion(t *testing.T) {
 		// callback must be called exactly 1 time
 		require.Equal(t, callbackInvoked.Load(), int32(1))
 
-		var cnt int
+		var (
+			errCalledCount int
+			noErrorCount   int
+		)
+
 		for result := range results {
-			cnt++
-			switch cnt {
-			// n-th invocation must run and return an expected error
-			case n:
-				require.Error(t, result)
-				require.ErrorIs(t, result, errCalled)
-			// other invocations must not run and return any error
-			default:
-				require.NoError(t, result)
+			if result == nil {
+				noErrorCount++
+				continue
 			}
+
+			errCalledCount++
+
+			require.ErrorIs(t, result, errCalled)
 		}
+
+		require.Equal(t, errCalledCount, 1)
+		require.Equal(t, noErrorCount, n)
 	})
 }
diff --git a/internal/uitask/uitask_test.go b/internal/uitask/uitask_test.go
index 07833f09..8eed8515 100644
--- a/internal/uitask/uitask_test.go
+++ b/internal/uitask/uitask_test.go
@@ -1,6 +1,7 @@
 package uitask_test
 
 import (
+	"bytes"
 	"context"
 	"encoding/json"
 	"strings"
@@ -10,6 +11,7 @@
 	"github.com/google/go-cmp/cmp"
 	"github.com/google/uuid"
 	"github.com/pkg/errors"
+	"github.com/stretchr/testify/require"
 
 	"github.com/kopia/kopia/internal/uitask"
 	"github.com/kopia/kopia/repo/content"
@@ -21,11 +23,29 @@
 	ignoredLog = logging.Module(content.FormatLogModule)
 )
 
-func TestUITask(t *testing.T) {
-	t.Parallel()
+func TestUITask_withoutPersistentLogging(t *testing.T) {
+	var logBuf bytes.Buffer
 
-	ctx := context.Background()
-	m := uitask.NewManager()
+	ctx := logging.WithLogger(context.Background(), logging.ToWriter(&logBuf))
+
+	m := uitask.NewManager(false)
+	testUITaskInternal(t, ctx, m)
+	require.Equal(t, "", logBuf.String())
+}
+
+func TestUITask_withPersistentLogging(t *testing.T) {
+	var logBuf bytes.Buffer
+
+	ctx := logging.WithLogger(context.Background(), logging.ToWriter(&logBuf))
+
+	m := uitask.NewManager(true)
+	testUITaskInternal(t, ctx, m)
+	require.Equal(t, "first\nthis is ignored\niii\nwww\neee\n", logBuf.String())
+}
+
+//nolint:thelper
+func testUITaskInternal(t *testing.T, ctx context.Context, m *uitask.Manager) {
+	t.Parallel()
 
 	m.MaxLogMessagesPerTask = 3
 	m.MaxFinishedTasks = 3
@@ -225,7 +245,7 @@ func verifyTaskList(t *testing.T, m *uitask.Manager, wantStatuses map[string]uit
 func TestUITaskCancel_NonExistent(t *testing.T) {
 	t.Parallel()
 
-	m := uitask.NewManager()
+	m := uitask.NewManager(false)
 	m.CancelTask("no-such-task")
 }
 
@@ -233,7 +253,7 @@ func TestUITaskCancel_AfterOnCancel(t *testing.T) {
 	t.Parallel()
 
 	ctx := context.Background()
-	m := uitask.NewManager()
+	m := uitask.NewManager(false)
 
 	ch := make(chan string)
 
@@ -277,7 +297,7 @@ func TestUITaskCancel_BeforeOnCancel(t *testing.T) {
 	t.Parallel()
 
 	ctx := context.Background()
-	m := uitask.NewManager()
+	m := uitask.NewManager(false)
 
 	ch := make(chan string)
 
@@ -331,7 +351,7 @@ func verifyTaskLog(t *testing.T, m *uitask.Manager, taskID string, want []string
 	t.Helper()
 
 	if got, want := logText(m.TaskLog(taskID)), strings.Join(want, "\n"); got != want {
-		t.Fatalf("invalid task log %v, want %v", got, want)
+		t.Fatalf("invalid task log: got `%v`, want `%v`", got, want)
 	}
 }
 
diff --git a/repo/logging/logging_test.go b/repo/logging/logging_test.go
index 31c1be6e..0d436e7f 100644
--- a/repo/logging/logging_test.go
+++ b/repo/logging/logging_test.go
@@ -82,6 +82,23 @@ func TestNonNullWriterModule(t *testing.T) {
 	require.Equal(t, "A\nS\t{\"b\":123}\nB\nC\nW\n", buf.String())
 }
 
+func TestWithAdditionalLogger(t *testing.T) {
+	var buf, buf2 bytes.Buffer
+
+	ctx := logging.WithLogger(context.Background(), logging.ToWriter(&buf))
+	ctx = logging.WithAdditionalLogger(ctx, logging.ToWriter(&buf2))
+	l := logging.Module("mod1")(ctx)
+
+	l.Debugf("A")
+	l.Debugw("S", "b", 123)
+	l.Infof("B")
+	l.Errorf("C")
+	l.Warnf("W")
+
+	require.Equal(t, "A\nS\t{\"b\":123}\nB\nC\nW\n", buf.String())
+	require.Equal(t, "A\nS\t{\"b\":123}\nB\nC\nW\n", buf2.String())
+}
+
 func BenchmarkLogger(b *testing.B) {
 	mod1 := logging.Module("mod1")
 	ctx := logging.WithLogger(context.Background(), testlogging.PrintfFactory(b.Logf))
diff --git a/tests/end_to_end_test/acl_test.go b/tests/end_to_end_test/acl_test.go
index 56f5de55..6faa2b4f 100644
--- a/tests/end_to_end_test/acl_test.go
+++ b/tests/end_to_end_test/acl_test.go
@@ -1,17 +1,34 @@
 package endtoend_test
 
 import (
+	"fmt"
 	"testing"
 
+	"github.com/stretchr/testify/require"
+
 	"github.com/kopia/kopia/internal/auth"
 	"github.com/kopia/kopia/internal/testutil"
 	"github.com/kopia/kopia/tests/clitestutil"
 	"github.com/kopia/kopia/tests/testenv"
 )
 
-func TestACL(t *testing.T) {
+func TestACL_GRPC(t *testing.T) {
+	verifyACL(t, false)
+}
+
+func TestACL_HTTP(t *testing.T) {
+	verifyACL(t, true)
+}
+
+//nolint:thelper
+func verifyACL(t *testing.T, disableGRPC bool) {
 	t.Parallel()
 
+	grpcArgument := "--grpc"
+	if disableGRPC {
+		grpcArgument = "--no-grpc"
+	}
+
 	serverRunner := testenv.NewInProcRunner(t)
 	serverEnvironment := testenv.NewCLITest(t, testenv.RepoFormatNotImportant, serverRunner)
 
@@ -19,27 +36,38 @@ func TestACL(t *testing.T) {
 
 	serverEnvironment.RunAndExpectSuccess(t, "repo", "create", "filesystem", "--path", serverEnvironment.RepoDir, "--override-hostname=foo", "--override-username=foo", "--enable-actions")
 
-	if got, want := len(serverEnvironment.RunAndExpectSuccess(t, "server", "acl", "list")), 0; got != want {
-		t.Fatalf("unexpected ACLs found")
-	}
+	require.Len(t, serverEnvironment.RunAndExpectSuccess(t, "server", "acl", "list"), 0)
 
 	// enable ACLs - that should insert all the rules.
 	serverEnvironment.RunAndExpectSuccess(t, "server", "acl", "enable")
 
-	if got, want := len(serverEnvironment.RunAndExpectSuccess(t, "server", "acl", "list")), len(auth.DefaultACLs); got != want {
-		t.Fatalf("unexpected ACLs found")
-	}
+	require.Len(t, serverEnvironment.RunAndExpectSuccess(t, "server", "acl", "list"), len(auth.DefaultACLs))
+
+	// reduce default access to snapshots to APPEND - this will fail because exactly identical rule already exists and grants FULL access.
+	serverEnvironment.RunAndExpectFailure(t, "server", "acl", "add", "--user", "*@*", "--target", "type=snapshot,username=OWN_USER,hostname=OWN_HOST", "--access=APPEND")
+
+	// reduce default access to snapshots to APPEND with --overwrite, this wil succeed.
+	serverEnvironment.RunAndExpectSuccess(t, "server", "acl", "add", "--user", "*@*", "--target", "type=snapshot,username=OWN_USER,hostname=OWN_HOST", "--access=APPEND", "--overwrite")
 
 	// add read access to all snapshots and policies for user foo@bar
 	serverEnvironment.RunAndExpectSuccess(t, "server", "acl", "add", "--user", "foo@bar", "--target", "type=snapshot", "--access=READ")
 	serverEnvironment.RunAndExpectSuccess(t, "server", "acl", "add", "--user", "foo@bar", "--target", "type=policy", "--access=READ")
 
+	// add append access to all snapshots and read-only access to policies for user another@bar
+	serverEnvironment.RunAndExpectSuccess(t, "server", "acl", "add", "--user", "another@bar", "--target", "type=snapshot", "--access=APPEND")
+	serverEnvironment.RunAndExpectSuccess(t, "server", "acl", "add", "--user", "another@bar", "--target", "type=policy", "--access=READ")
+
 	// add full access to global policy for all users
 	serverEnvironment.RunAndExpectSuccess(t, "server", "acl", "add", "--user", "*@*", "--target", "type=policy,policyType=global", "--access=FULL")
 
 	serverEnvironment.RunAndExpectSuccess(t, "server", "users", "add", "foo@bar", "--user-password", "baz")
+	serverEnvironment.RunAndExpectSuccess(t, "server", "users", "add", "another@bar", "--user-password", "baz")
 	serverEnvironment.RunAndExpectSuccess(t, "server", "users", "add", "alice@wonderland", "--user-password", "baz")
 
+	const keepLatestSnapshots = 3
+
+	serverEnvironment.RunAndExpectSuccess(t, "policy", "set", "another@bar", fmt.Sprintf("--keep-latest=%v", keepLatestSnapshots))
+
 	var sp testutil.ServerParameters
 
 	wait, kill := serverEnvironment.RunAndProcessStderr(t, sp.ProcessOutput,
@@ -70,6 +98,24 @@ func TestACL(t *testing.T) {
 		"--override-username", "foo",
 		"--override-hostname", "bar",
 		"--password", "baz",
+		grpcArgument,
+	)
+
+	anotherBarRunner := testenv.NewInProcRunner(t)
+	anotherBarClientEnvironment := testenv.NewCLITest(t, testenv.RepoFormatNotImportant, anotherBarRunner)
+
+	defer anotherBarClientEnvironment.RunAndExpectSuccess(t, "repo", "disconnect")
+
+	delete(anotherBarClientEnvironment.Environment, "KOPIA_PASSWORD")
+
+	// connect as foo@bar with password baz
+	anotherBarClientEnvironment.RunAndExpectSuccess(t, "repo", "connect", "server",
+		"--url", sp.BaseURL+"/",
+		"--server-cert-fingerprint", sp.SHA256Fingerprint,
+		"--override-username", "another",
+		"--override-hostname", "bar",
+		"--password", "baz",
+		grpcArgument,
 	)
 
 	aliceInWonderlandRunner := testenv.NewInProcRunner(t)
@@ -86,6 +132,7 @@ func TestACL(t *testing.T) {
 		"--override-username", "alice",
 		"--override-hostname", "wonderland",
 		"--password", "baz",
+		grpcArgument,
 	)
 
 	// both alice and foo@bar can see global policy
@@ -117,6 +164,23 @@ func TestACL(t *testing.T) {
 		t.Fatalf("foo@bar expected to see 1 source (own), got %v", snaps)
 	}
 
+	// another@bar can create snapshots but not delete them
+	anotherBarClientEnvironment.RunAndExpectSuccess(t, "snapshot", "create", sharedTestDataDir1)
+	anotherBarClientEnvironment.RunAndExpectSuccess(t, "snapshot", "create", sharedTestDataDir1)
+	anotherBarClientEnvironment.RunAndExpectSuccess(t, "snapshot", "create", sharedTestDataDir1)
+	anotherBarClientEnvironment.RunAndExpectSuccess(t, "snapshot", "create", sharedTestDataDir1)
+	anotherBarClientEnvironment.RunAndExpectSuccess(t, "snapshot", "create", sharedTestDataDir1)
+	anotherBarClientEnvironment.RunAndExpectSuccess(t, "snapshot", "create", sharedTestDataDir1)
+
+	// make sure only `keepLatestSnapshots` snapshots are kept, so retention policy
+	// is working.
+	snapshots := clitestutil.ListSnapshotsAndExpectSuccess(t, anotherBarClientEnvironment, sharedTestDataDir1)[0].Snapshots
+	require.Len(t, snapshots, keepLatestSnapshots)
+
+	// APPEND policy despite being able to maintain retention rules, prevents snapshots from being deleted
+	// by the client.
+	anotherBarClientEnvironment.RunAndExpectFailure(t, "snapshot", "delete", snapshots[0].SnapshotID, "--delete")
+
 	// alice changes her own password and reconnects
 	aliceInWonderlandClientEnvironment.RunAndExpectSuccess(t, "server", "users", "set", "alice@wonderland", "--user-password", "new-password")