From 952577a43125da0cfb486fc71ea39c5989e79b5a Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Daniel=20H=C3=A5=C3=A5vi?= <dhaavi@users.noreply.github.com>
Date: Wed, 27 Nov 2024 16:14:34 +0100
Subject: [PATCH] Revert "New updater/installer"

---
 .github/workflows/release.yml                 |   70 -
 .github/workflows/tauri.yml                   |   19 -
 Earthfile                                     |  297 +-
 base/api/endpoints_debug.go                   |    5 +-
 base/config/init_test.go                      |   46 +-
 base/config/main.go                           |   39 +-
 base/config/module.go                         |    1 -
 base/database/database_test.go                |    2 +-
 base/database/dbmodule/db.go                  |   27 +-
 base/database/main.go                         |   42 +-
 base/dataroot/root.go                         |   25 +
 base/info/version.go                          |   24 -
 base/log/flags.go                             |   13 +
 base/log/input.go                             |   39 +-
 base/log/logging.go                           |   96 +-
 base/log/logging_test.go                      |    5 +-
 base/log/output.go                            |  162 +-
 base/log/slog.go                              |   41 +-
 base/log/trace.go                             |   33 +-
 base/log/writer.go                            |  119 -
 base/metrics/metrics_host.go                  |   12 +-
 base/metrics/module.go                        |    4 +-
 base/notifications/module.go                  |   41 -
 base/updater/doc.go                           |    2 +
 base/updater/export.go                        |   15 +
 base/updater/fetch.go                         |  347 ++
 base/updater/file.go                          |  156 +
 base/updater/filename.go                      |   57 +
 base/updater/filename_test.go                 |   80 +
 base/updater/get.go                           |   91 +
 base/updater/indexes.go                       |  109 +
 base/updater/indexes_test.go                  |   57 +
 base/updater/notifier.go                      |   33 +
 base/updater/registry.go                      |  270 ++
 base/updater/registry_test.go                 |   35 +
 base/updater/resource.go                      |  582 ++++
 base/updater/resource_test.go                 |  119 +
 base/updater/signing.go                       |   49 +
 base/updater/state.go                         |  180 ++
 base/updater/storage.go                       |  272 ++
 base/updater/storage_test.go                  |   68 +
 base/updater/unpacking.go                     |  195 ++
 base/updater/updating.go                      |  359 +++
 cmds/hub/main.go                              |   15 +-
 cmds/notifier/.gitignore                      |   34 +
 cmds/notifier/README.md                       |    5 +
 cmds/notifier/http_api.go                     |   63 +
 cmds/notifier/icons.go                        |   25 +
 cmds/notifier/main.go                         |  287 ++
 cmds/notifier/notification.go                 |   35 +
 cmds/notifier/notify.go                       |  102 +
 cmds/notifier/notify_linux.go                 |  160 +
 cmds/notifier/notify_windows.go               |  184 ++
 cmds/notifier/shutdown.go                     |   50 +
 cmds/notifier/snoretoast-guid.patch           |   15 +
 cmds/notifier/spn.go                          |  104 +
 cmds/notifier/subsystems.go                   |  121 +
 cmds/notifier/tray.go                         |  217 ++
 .../notifier/wintoast/notification_builder.go |   90 +
 cmds/notifier/wintoast/wintoast.go            |  217 ++
 cmds/observation-hub/main.go                  |   11 +-
 cmds/portmaster-core/main.go                  |  204 +-
 cmds/portmaster-core/recover_linux.go         |   90 -
 cmds/portmaster-core/run.go                   |  140 -
 cmds/portmaster-core/run_linux.go             |  145 -
 cmds/portmaster-core/run_windows.go           |  241 --
 cmds/portmaster-core/update.go                |   77 -
 cmds/portmaster-start/.gitignore              |    6 +
 cmds/portmaster-start/build                   |   77 +
 cmds/portmaster-start/console_default.go      |   11 +
 cmds/portmaster-start/console_windows.go      |  150 +
 cmds/portmaster-start/dirs.go                 |   42 +
 cmds/portmaster-start/install_windows.go      |  180 ++
 cmds/portmaster-start/lock.go                 |  109 +
 cmds/portmaster-start/logs.go                 |  127 +
 cmds/portmaster-start/main.go                 |  257 ++
 cmds/portmaster-start/pack                    |  123 +
 cmds/portmaster-start/recover_linux.go        |   82 +
 cmds/portmaster-start/run.go                  |  486 +++
 cmds/portmaster-start/service_windows.go      |  134 +
 cmds/portmaster-start/show.go                 |   45 +
 cmds/portmaster-start/shutdown.go             |   49 +
 cmds/portmaster-start/update.go               |  158 +
 cmds/portmaster-start/verify.go               |  179 ++
 cmds/portmaster-start/version.go              |   81 +
 cmds/updatemgr/confirm.go                     |   20 +
 cmds/updatemgr/main.go                        |   42 +-
 cmds/updatemgr/purge.go                       |   33 +
 cmds/updatemgr/release.go                     |  195 ++
 cmds/updatemgr/scan.go                        |   88 +-
 cmds/updatemgr/sign.go                        |  191 +-
 desktop/tauri/src-tauri/Cargo.lock            | 2849 ++++-------------
 desktop/tauri/src-tauri/Cargo.toml            |   33 +-
 desktop/tauri/src-tauri/README.md             |   41 -
 .../src-tauri/gen/schemas/acl-manifests.json  |    2 +-
 .../src-tauri/gen/schemas/desktop-schema.json | 2701 ++++++++++------
 .../src-tauri/gen/schemas/linux-schema.json   | 2701 ++++++++++------
 desktop/tauri/src-tauri/src/cli.rs            |  107 -
 desktop/tauri/src-tauri/src/config.rs         |    2 +-
 desktop/tauri/src-tauri/src/main.rs           |  161 +-
 .../tauri/src-tauri/src/portapi/message.rs    |  143 +-
 .../src-tauri/src/portapi/models/config.rs    |    6 +-
 .../src-tauri/src/portmaster/commands.rs      |    6 +-
 desktop/tauri/src-tauri/src/portmaster/mod.rs |   17 +-
 .../src-tauri/src/portmaster/notifications.rs |    8 +-
 .../tauri/src-tauri/src/service/systemd.rs    |    6 +-
 desktop/tauri/src-tauri/src/traymenu.rs       |  151 +-
 desktop/tauri/src-tauri/src/window.rs         |   11 +-
 desktop/tauri/src-tauri/src/xdg/mod.rs        |   19 +-
 desktop/tauri/src-tauri/tauri.conf.json5      |   42 +-
 desktop/tauri/src-tauri/templates/files.wxs   |   39 -
 desktop/tauri/src-tauri/templates/main.wxs    |   12 +-
 .../src-tauri/templates/main_original.wxs     |    2 +-
 .../templates/nsis_install_hooks.nsh          |   33 +-
 desktop/tauri/src-tauri/templates/service.wxs |    2 +-
 go.mod                                        |   14 +-
 go.sum                                        |   22 +-
 packaging/linux/portmaster.service            |    9 +-
 packaging/linux/postinst                      |   14 -
 packaging/linux/postrm                        |    9 -
 .../windows/generate_windows_installers.ps1   |   74 -
 service/broadcasts/data.go                    |   24 +-
 service/broadcasts/install_info.go            |    4 -
 service/broadcasts/module.go                  |    5 +-
 service/broadcasts/notify.go                  |    3 +-
 service/config.go                             |  110 +-
 service/core/api.go                           |   51 +-
 service/core/base/global.go                   |   26 +
 service/core/base/logs.go                     |    3 +-
 service/core/base/module.go                   |    1 -
 service/core/core.go                          |    4 -
 service/firewall/api.go                       |   10 +-
 .../interception/interception_windows.go      |    7 +-
 service/firewall/interception/module.go       |    5 +-
 .../interception/windowskext2/kext.go         |    3 -
 service/firewall/module.go                    |    3 -
 service/instance.go                           |  175 +-
 service/intel/filterlists/database.go         |   20 +-
 service/intel/filterlists/index.go            |   24 +-
 service/intel/filterlists/module.go           |    5 +-
 service/intel/filterlists/updater.go          |   86 +-
 service/intel/geoip/database.go               |   88 +-
 service/intel/geoip/init_test.go              |   18 +-
 service/intel/geoip/module.go                 |    5 +-
 service/mgr/worker.go                         |    5 +-
 service/netenv/init_test.go                   |   18 +-
 service/netenv/main.go                        |    5 +-
 service/netenv/online-status.go               |    3 +-
 service/netquery/database.go                  |   14 +-
 service/netquery/module_api.go                |    1 -
 service/network/api.go                        |    3 +-
 service/process/module.go                     |   18 +-
 service/process/process.go                    |    2 +-
 service/process/profile.go                    |   14 +-
 service/profile/endpoints/endpoints_test.go   |   19 +-
 service/profile/module.go                     |   20 +-
 service/profile/profile.go                    |    2 +-
 service/resolver/main_test.go                 |   21 +-
 service/ui/module.go                          |   16 +-
 service/ui/serve.go                           |    5 +-
 service/updates.go                            |  120 -
 service/updates/api.go                        |  161 +
 service/updates/assets/portmaster.service     |   44 +
 service/updates/config.go                     |  178 +
 service/updates/downloader.go                 |  307 --
 service/updates/export.go                     |  238 ++
 service/updates/get.go                        |   72 +
 service/updates/helper/electron.go            |   57 +
 service/updates/helper/indexes.go             |  136 +
 service/updates/helper/signing.go             |   42 +
 service/updates/helper/updates.go             |   95 +
 service/updates/index.go                      |  381 ---
 service/updates/index_scan.go                 |  342 --
 service/updates/main.go                       |  318 ++
 service/updates/module.go                     |  509 +--
 service/updates/notify.go                     |  180 ++
 service/updates/os_integration_default.go     |    8 +
 service/updates/os_integration_linux.go       |  204 ++
 service/updates/restart.go                    |  135 +
 service/updates/state.go                      |   49 +
 service/updates/updates_test.go               |  155 -
 service/updates/upgrade.go                    |  209 --
 service/updates/upgrader.go                   |  406 +++
 spn/access/module.go                          |    4 +-
 spn/captain/hooks.go                          |    5 +-
 spn/captain/intel.go                          |   36 +-
 spn/captain/module.go                         |    2 +-
 spn/hub/hub_test.go                           |   19 +-
 spn/instance.go                               |  134 +-
 spn/navigator/module_test.go                  |   19 +-
 190 files changed, 15286 insertions(+), 9096 deletions(-)
 delete mode 100644 .github/workflows/release.yml
 create mode 100644 base/dataroot/root.go
 create mode 100644 base/log/flags.go
 delete mode 100644 base/log/writer.go
 create mode 100644 base/updater/doc.go
 create mode 100644 base/updater/export.go
 create mode 100644 base/updater/fetch.go
 create mode 100644 base/updater/file.go
 create mode 100644 base/updater/filename.go
 create mode 100644 base/updater/filename_test.go
 create mode 100644 base/updater/get.go
 create mode 100644 base/updater/indexes.go
 create mode 100644 base/updater/indexes_test.go
 create mode 100644 base/updater/notifier.go
 create mode 100644 base/updater/registry.go
 create mode 100644 base/updater/registry_test.go
 create mode 100644 base/updater/resource.go
 create mode 100644 base/updater/resource_test.go
 create mode 100644 base/updater/signing.go
 create mode 100644 base/updater/state.go
 create mode 100644 base/updater/storage.go
 create mode 100644 base/updater/storage_test.go
 create mode 100644 base/updater/unpacking.go
 create mode 100644 base/updater/updating.go
 create mode 100644 cmds/notifier/.gitignore
 create mode 100644 cmds/notifier/README.md
 create mode 100644 cmds/notifier/http_api.go
 create mode 100644 cmds/notifier/icons.go
 create mode 100644 cmds/notifier/main.go
 create mode 100644 cmds/notifier/notification.go
 create mode 100644 cmds/notifier/notify.go
 create mode 100644 cmds/notifier/notify_linux.go
 create mode 100644 cmds/notifier/notify_windows.go
 create mode 100644 cmds/notifier/shutdown.go
 create mode 100644 cmds/notifier/snoretoast-guid.patch
 create mode 100644 cmds/notifier/spn.go
 create mode 100644 cmds/notifier/subsystems.go
 create mode 100644 cmds/notifier/tray.go
 create mode 100644 cmds/notifier/wintoast/notification_builder.go
 create mode 100644 cmds/notifier/wintoast/wintoast.go
 delete mode 100644 cmds/portmaster-core/recover_linux.go
 delete mode 100644 cmds/portmaster-core/run.go
 delete mode 100644 cmds/portmaster-core/run_linux.go
 delete mode 100644 cmds/portmaster-core/run_windows.go
 delete mode 100644 cmds/portmaster-core/update.go
 create mode 100644 cmds/portmaster-start/.gitignore
 create mode 100755 cmds/portmaster-start/build
 create mode 100644 cmds/portmaster-start/console_default.go
 create mode 100644 cmds/portmaster-start/console_windows.go
 create mode 100644 cmds/portmaster-start/dirs.go
 create mode 100644 cmds/portmaster-start/install_windows.go
 create mode 100644 cmds/portmaster-start/lock.go
 create mode 100644 cmds/portmaster-start/logs.go
 create mode 100644 cmds/portmaster-start/main.go
 create mode 100755 cmds/portmaster-start/pack
 create mode 100644 cmds/portmaster-start/recover_linux.go
 create mode 100644 cmds/portmaster-start/run.go
 create mode 100644 cmds/portmaster-start/service_windows.go
 create mode 100644 cmds/portmaster-start/show.go
 create mode 100644 cmds/portmaster-start/shutdown.go
 create mode 100644 cmds/portmaster-start/update.go
 create mode 100644 cmds/portmaster-start/verify.go
 create mode 100644 cmds/portmaster-start/version.go
 create mode 100644 cmds/updatemgr/confirm.go
 create mode 100644 cmds/updatemgr/purge.go
 create mode 100644 cmds/updatemgr/release.go
 delete mode 100644 desktop/tauri/src-tauri/README.md
 delete mode 100644 desktop/tauri/src-tauri/src/cli.rs
 delete mode 100644 desktop/tauri/src-tauri/templates/files.wxs
 delete mode 100644 packaging/windows/generate_windows_installers.ps1
 delete mode 100644 service/updates.go
 create mode 100644 service/updates/api.go
 create mode 100644 service/updates/assets/portmaster.service
 create mode 100644 service/updates/config.go
 delete mode 100644 service/updates/downloader.go
 create mode 100644 service/updates/export.go
 create mode 100644 service/updates/get.go
 create mode 100644 service/updates/helper/electron.go
 create mode 100644 service/updates/helper/indexes.go
 create mode 100644 service/updates/helper/signing.go
 create mode 100644 service/updates/helper/updates.go
 delete mode 100644 service/updates/index.go
 delete mode 100644 service/updates/index_scan.go
 create mode 100644 service/updates/main.go
 create mode 100644 service/updates/notify.go
 create mode 100644 service/updates/os_integration_default.go
 create mode 100644 service/updates/os_integration_linux.go
 create mode 100644 service/updates/restart.go
 create mode 100644 service/updates/state.go
 delete mode 100644 service/updates/updates_test.go
 delete mode 100644 service/updates/upgrade.go
 create mode 100644 service/updates/upgrader.go

diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
deleted file mode 100644
index 2ec7fa30..00000000
--- a/.github/workflows/release.yml
+++ /dev/null
@@ -1,70 +0,0 @@
-name: Release
-
-on:
-  workflow_dispatch:
-
-jobs:
-  release-prep:
-    name: Prep
-    runs-on: ubuntu-latest
-    steps:
-    - uses: earthly/actions-setup@v1
-      with:
-        version: v0.8.0
-    - uses: actions/checkout@v4
-
-    - name: Log in to the Container registry
-      uses: docker/login-action@65b78e6e13532edd9afa3aa52ac7964289d1a9c1
-      with:
-        registry: ghcr.io
-        username: ${{ github.actor }}
-        password: ${{ secrets.GITHUB_TOKEN }}
-
-    - name: Build all artifacts
-      run: earthly --remote-cache=ghcr.io/safing/build-cache --push +release-prep
-
-    - name: Upload Dist
-      uses: actions/upload-artifact@v4
-      with:
-        name: dist
-        path: ./dist/
-        if-no-files-found: error
-
-  installer-linux:
-    name: Installer linux
-    runs-on: ubuntu-latest
-    needs: release-prep
-    steps:
-    - uses: earthly/actions-setup@v1
-      with:
-        version: v0.8.0
-    - uses: actions/checkout@v4
-
-    - name: Log in to the Container registry
-      uses: docker/login-action@65b78e6e13532edd9afa3aa52ac7964289d1a9c1
-      with:
-        registry: ghcr.io
-        username: ${{ github.actor }}
-        password: ${{ secrets.GITHUB_TOKEN }}
-
-    - name: Build linux installers
-      run: earthly --ci --remote-cache=ghcr.io/safing/build-cache --push +installer-linux
-      # --ci include --no-output flag
-
-  installer-windows:
-    name: Installer windows
-    runs-on: windows-latest
-    needs: release-prep
-    steps:
-    - name: Checkout Repository
-      uses: actions/checkout@v4
-
-    - name: Download Dist
-      uses: actions/download-artifact@v4
-      with:
-        name: dist
-        path: dist/
-
-    - name: Build windows artifacts
-      run: powershell -NoProfile -File ./packaging/windows/generate_windows_installers.ps1
-
diff --git a/.github/workflows/tauri.yml b/.github/workflows/tauri.yml
index a84f74e3..9ae0da99 100644
--- a/.github/workflows/tauri.yml
+++ b/.github/workflows/tauri.yml
@@ -34,22 +34,3 @@ jobs:
 
     - name: Build tauri project
       run: earthly --ci --remote-cache=ghcr.io/safing/build-cache --push +tauri-ci
-
-  lint:
-    name: Linter
-    runs-on: ubuntu-latest
-    steps:
-    - uses: earthly/actions-setup@v1
-      with:
-        version: v0.8.0
-    - uses: actions/checkout@v4
-
-    - name: Log in to the Container registry
-      uses: docker/login-action@65b78e6e13532edd9afa3aa52ac7964289d1a9c1
-      with:
-        registry: ghcr.io
-        username: ${{ github.actor }}
-        password: ${{ secrets.GITHUB_TOKEN }}
-
-    - name: Build tauri project
-      run: earthly --ci --remote-cache=ghcr.io/safing/build-cache --push +tauri-lint
diff --git a/Earthfile b/Earthfile
index 8937bd05..9d016b08 100644
--- a/Earthfile
+++ b/Earthfile
@@ -3,7 +3,6 @@ VERSION --arg-scope-and-set --global-cache 0.8
 ARG --global go_version = 1.22
 ARG --global node_version = 18
 ARG --global rust_version = 1.79
-ARG --global tauri_version = "2.0.1"
 ARG --global golangci_lint_version = 1.57.1
 
 ARG --global go_builder_image = "golang:${go_version}-alpine"
@@ -57,14 +56,15 @@ build:
 
     # Build Tauri app binaries:
     # ./dist/linux_amd64/portmaster-app
-    # ./dist/windows_amd64/portmaster-app
-    BUILD +tauri-build --target="x86_64-unknown-linux-gnu"
-    BUILD +tauri-build --target="x86_64-pc-windows-gnu"
-
-    # TODO(vladimir): Build bundles
     # ./dist/linux_amd64/Portmaster-0.1.0-1.x86_64.rpm
     # ./dist/linux_amd64/Portmaster_0.1.0_amd64.deb
+    BUILD +tauri-build --target="x86_64-unknown-linux-gnu"
+    # TODO:
+    # BUILD +tauri-build --target="x86_64-pc-windows-gnu"
+
     # Bild Tauri bundle for Windows:
+    # ./dist/windows_amd64/portmaster-app_vX-X-X.zip
+    BUILD +tauri-build-windows-bundle
 
     # Build UI assets:
     # ./dist/all/assets.zip
@@ -82,7 +82,7 @@ angular-ci:
 
 tauri-ci:
     BUILD +tauri-build --target="x86_64-unknown-linux-gnu"
-    BUILD +tauri-build --target="x86_64-pc-windows-gnu"
+    BUILD +tauri-build-windows-bundle
 
 kext-ci:
     BUILD +kext-build
@@ -177,7 +177,7 @@ go-build:
     ARG GOOS=linux
     ARG GOARCH=amd64
     ARG GOARM
-    ARG CMDS=portmaster-core
+    ARG CMDS=portmaster-start portmaster-core
 
     CACHE --sharing shared "$GOCACHE"
     CACHE --sharing shared "$GOMODCACHE"
@@ -349,7 +349,6 @@ angular-project:
     # Save portmaster UI as local artifact.
     IF [ "${project}" = "portmaster" ]
         SAVE ARTIFACT --keep-ts "./${project}.zip" AS LOCAL ${outputDir}/all/${project}-ui.zip
-        SAVE ARTIFACT --keep-ts "./${project}.zip" output/${project}.zip
     END
 
 # Build the angular projects (portmaster-UI and tauri-builtin) in dev mode
@@ -421,7 +420,7 @@ rust-base:
     DO rust+INIT --keep_fingerprints=true
 
     # For now we need tauri-cli 2.0.0 for bulding
-    DO rust+CARGO --args="install tauri-cli --version ${tauri_version} --locked"
+    DO rust+CARGO --args="install tauri-cli --version ^2.0.0-beta"
 
     # Explicitly cache here.
     SAVE IMAGE --cache-hint
@@ -435,7 +434,7 @@ tauri-src:
     # are preserved such that Rust's incremental compilation works correctly.
     COPY --keep-ts ./desktop/tauri/ .
     COPY assets/data ./../../assets/data
-    COPY packaging ./../../packaging
+    COPY packaging/linux ./../../packaging/linux
     COPY (+angular-project/output/tauri-builtin --project=tauri-builtin --dist=./dist/tauri-builtin --configuration=production --baseHref="/") ./../angular/dist/tauri-builtin
 
     WORKDIR /app/tauri/src-tauri
@@ -447,14 +446,48 @@ tauri-build:
     FROM +tauri-src
 
     ARG --required target
+    ARG output=".*/release/(([^\./]+|([^\./]+\.(dll|exe)))|bundle/(deb|rpm)/.*\.(deb|rpm))"
+    ARG bundle="none"
+
+    # if we want tauri to create the installer bundles we also need to provide all external binaries
+    # we need to do some magic here because tauri expects the binaries to include the rust target tripple.
+    # We already know that triple because it's a required argument. From that triple, we use +RUST_TO_GO_ARCH_STRING
+    # function from below to parse the triple and guess wich GOOS and GOARCH we need.
+    RUN mkdir /tmp/gobuild
+    RUN mkdir ./binaries
 
-    ARG output=".*/release/([^\./]+|([^\./]+\.(dll|exe)))"
     DO +RUST_TO_GO_ARCH_STRING --rustTarget="${target}"
     RUN echo "GOOS=${GOOS} GOARCH=${GOARCH} GOARM=${GOARM} GO_ARCH_STRING=${GO_ARCH_STRING}"
 
+    # Our tauri app has externalBins configured so tauri will try to embed them when it finished compiling
+    # the app. Make sure we copy portmaster-start and portmaster-core in all architectures supported.
+    # See documentation for externalBins for more information on how tauri searches for the binaries.
+    COPY (+go-build/output --CMDS="portmaster-start portmaster-core" --GOOS="${GOOS}" --GOARCH="${GOARCH}" --GOARM="${GOARM}") /tmp/gobuild
+
+    # Place them in the correct folder with the rust target tripple attached.
+    FOR bin IN $(ls /tmp/gobuild)
+        # ${bin$.*} does not work in SET commands unfortunately so we use a shell
+        # snippet here:
+        RUN set -e ; \
+            dest="./binaries/${bin}-${target}" ; \
+            if [ -z "${bin##*.exe}" ]; then \
+                dest="./binaries/${bin%.*}-${target}.exe" ; \
+            fi ; \
+            cp "/tmp/gobuild/${bin}" "${dest}" ;
+    END
+
+    # Just for debugging ...
+    # RUN ls -R ./binaries
+
+    # The following is exected to work but doesn't. for whatever reason cargo-sweep errors out on the windows-toolchain.
+    #
+    #   DO rust+CARGO --args="tauri build --bundles none --ci --target=${target}" --output="release/[^/\.]+"
+    #
+    # For, now, we just directly mount the rust target cache and call cargo ourself.
+
     DO rust+SET_CACHE_MOUNTS_ENV
     RUN rustup target add "${target}"
-    RUN --mount=$EARTHLY_RUST_TARGET_CACHE cargo tauri build  --ci --target="${target}" --no-bundle
+    RUN --mount=$EARTHLY_RUST_TARGET_CACHE cargo tauri build  --ci --target="${target}"
     DO rust+COPY_OUTPUT --output="${output}"
 
     # BUG(cross-compilation):
@@ -473,13 +506,127 @@ tauri-build:
     RUN echo output: $(ls -R "target/${target}/release")
 
     # Binaries
-    SAVE ARTIFACT --if-exists --keep-ts "target/${target}/release/portmaster" AS LOCAL "${outputDir}/${GO_ARCH_STRING}/portmaster"
-    SAVE ARTIFACT --if-exists --keep-ts "target/${target}/release/portmaster.exe" AS LOCAL "${outputDir}/${GO_ARCH_STRING}/portmaster.exe"
-    # SAVE ARTIFACT --if-exists --keep-ts "target/${target}/release/WebView2Loader.dll" AS LOCAL "${outputDir}/${GO_ARCH_STRING}/WebView2Loader.dll"
+    SAVE ARTIFACT --if-exists --keep-ts "target/${target}/release/app" AS LOCAL "${outputDir}/${GO_ARCH_STRING}/portmaster-app"
+    SAVE ARTIFACT --if-exists --keep-ts "target/${target}/release/app.exe" AS LOCAL "${outputDir}/${GO_ARCH_STRING}/portmaster-app.exe"
+    SAVE ARTIFACT --if-exists --keep-ts "target/${target}/release/WebView2Loader.dll" AS LOCAL "${outputDir}/${GO_ARCH_STRING}/WebView2Loader.dll"
 
-    SAVE ARTIFACT --if-exists --keep-ts "target/${target}/release/portmaster" ./output/portmaster
-    SAVE ARTIFACT --if-exists --keep-ts "target/${target}/release/portmaster.exe" ./output/portmaster.exe
+    # Installers
+    SAVE ARTIFACT --if-exists --keep-ts "target/${target}/release/bundle/deb/*.deb" AS LOCAL "${outputDir}/${GO_ARCH_STRING}/"
+    SAVE ARTIFACT --if-exists --keep-ts "target/${target}/release/bundle/rpm/*.rpm" AS LOCAL "${outputDir}/${GO_ARCH_STRING}/"
 
+tauri-build-windows-bundle:
+    FROM +tauri-src
+
+    ARG target="x86_64-pc-windows-gnu"
+    ARG output=".*/release/(([^\./]+|([^\./]+\.(dll|exe))))"
+    ARG bundle="none"
+
+    ARG GOOS=windows
+    ARG GOARCH=amd64
+    ARG GOARM
+
+    # The binaries will not be used but we still need to create them. Tauri will check for them.
+    RUN mkdir /tmp/gobuild
+    RUN mkdir ./binaries
+
+    DO +RUST_TO_GO_ARCH_STRING --rustTarget="${target}"
+    RUN echo "GOOS=${GOOS} GOARCH=${GOARCH} GOARM=${GOARM} GO_ARCH_STRING=${GO_ARCH_STRING}"
+
+    # Our tauri app has externalBins configured so tauri will look for them when it finished compiling
+    # the app. Make sure we copy portmaster-start and portmaster-core in all architectures supported.
+    # See documentation for externalBins for more information on how tauri searches for the binaries.
+    COPY (+go-build/output --GOOS="${GOOS}" --CMDS="portmaster-start portmaster-core" --GOARCH="${GOARCH}" --GOARM="${GOARM}") /tmp/gobuild
+
+    # Place them in the correct folder with the rust target tripple attached.
+    FOR bin IN $(ls /tmp/gobuild)
+        # ${bin$.*} does not work in SET commands unfortunately so we use a shell
+        # snippet here:
+        RUN set -e ; \
+            dest="./binaries/${bin}-${target}" ; \
+            if [ -z "${bin##*.exe}" ]; then \
+                dest="./binaries/${bin%.*}-${target}.exe" ; \
+            fi ; \
+            cp "/tmp/gobuild/${bin}" "${dest}" ;
+    END
+
+    # Just for debugging ...
+    # RUN ls -R ./binaries
+
+    DO rust+SET_CACHE_MOUNTS_ENV
+    RUN rustup target add "${target}"
+    RUN --mount=$EARTHLY_RUST_TARGET_CACHE cargo tauri build --no-bundle --ci --target="${target}"
+    DO rust+COPY_OUTPUT --output="${output}"
+
+    # Get version from git.
+    COPY .git .
+    LET version = "$(git tag --points-at || true)"
+    IF [ -z "${version}" ]
+        LET dev_version = "$(git describe --tags --first-parent --abbrev=0 || true)"
+        IF [ -n "${dev_version}" ]
+            SET version = "${dev_version}"
+        END
+    END
+    IF [ -z "${version}" ]
+        SET version = "v0.0.0"
+    END
+    ENV VERSION="${version}"
+    RUN echo "Version: $VERSION"
+    ENV VERSION_SUFFIX="$(echo $VERSION | tr '.' '-')"
+    RUN echo "Version Suffix: $VERSION_SUFFIX"
+
+    RUN echo output: $(ls -R "target/${target}/release")
+    RUN mv "target/${target}/release/app.exe" "target/${target}/release/portmaster-app_${VERSION_SUFFIX}.exe"
+    RUN zip "target/${target}/release/portmaster-app_${VERSION_SUFFIX}.zip" "target/${target}/release/portmaster-app_${VERSION_SUFFIX}.exe" -j portmaster-app${VERSION_SUFFIX}.exe "target/${target}/release/WebView2Loader.dll" -j WebView2Loader.dll
+    SAVE ARTIFACT --if-exists "target/${target}/release/portmaster-app_${VERSION_SUFFIX}.zip" AS LOCAL "${outputDir}/${GO_ARCH_STRING}/"
+
+tauri-prep-windows:
+    FROM +angular-base --configuration=production
+    ARG target="x86_64-pc-windows-msvc"
+
+    # if we want tauri to create the installer bundles we also need to provide all external binaries
+    # we need to do some magic here because tauri expects the binaries to include the rust target tripple.
+    # We already know that triple because it's a required argument. From that triple, we use +RUST_TO_GO_ARCH_STRING
+    # function from below to parse the triple and guess wich GOOS and GOARCH we need.
+    RUN mkdir /tmp/gobuild
+    RUN mkdir ./binaries
+
+    DO +RUST_TO_GO_ARCH_STRING --rustTarget="${target}"
+    RUN echo "GOOS=${GOOS} GOARCH=${GOARCH} GOARM=${GOARM} GO_ARCH_STRING=${GO_ARCH_STRING}"
+
+    # Our tauri app has externalBins configured so tauri will try to embed them when it finished compiling
+    # the app. Make sure we copy portmaster-start and portmaster-core in all architectures supported.
+    # See documentation for externalBins for more information on how tauri searches for the binaries.
+
+    COPY (+go-build/output --GOOS="${GOOS}" --CMDS="portmaster-start portmaster-core" --GOARCH="${GOARCH}" --GOARM="${GOARM}") /tmp/gobuild
+
+    # Place them in the correct folder with the rust target tripple attached.
+    FOR bin IN $(ls /tmp/gobuild)
+        # ${bin$.*} does not work in SET commands unfortunately so we use a shell
+        # snippet here:
+        RUN set -e ; \
+            dest="./binaries/${bin}-${target}" ; \
+            if [ -z "${bin##*.exe}" ]; then \
+                dest="./binaries/${bin%.*}-${target}.exe" ; \
+            fi ; \
+            cp "/tmp/gobuild/${bin}" "${dest}" ;
+    END
+
+    # Copy source
+    COPY --keep-ts ./desktop/tauri/src-tauri src-tauri
+    COPY --keep-ts ./assets assets
+
+    # Build UI
+    ENV NODE_ENV="production"
+    RUN --no-cache ./node_modules/.bin/ng build --configuration production --base-href / "tauri-builtin"
+
+    # Just for debugging ...
+    # RUN ls -R ./binaries
+    # RUN ls -R ./dist
+
+    SAVE ARTIFACT "./dist/tauri-builtin" AS LOCAL "${outputDir}/${GO_ARCH_STRING}/desktop/angular/dist/"
+    SAVE ARTIFACT "./src-tauri" AS LOCAL "${outputDir}/${GO_ARCH_STRING}/desktop/tauri/src-tauri"
+    SAVE ARTIFACT "./binaries" AS LOCAL "${outputDir}/${GO_ARCH_STRING}/desktop/tauri/src-tauri/"
+    SAVE ARTIFACT "./assets" AS LOCAL "${outputDir}/${GO_ARCH_STRING}/assets"
 
 tauri-release:
     FROM ${work_image}
@@ -489,120 +636,6 @@ tauri-release:
         BUILD +tauri-build --target="${arch}"
     END
 
-tauri-lint:
-    FROM +rust-base
-    ARG target="x86_64-unknown-linux-gnu"
-
-    WORKDIR /app
-    # Copy static files that are embedded inside the executable.
-    COPY --keep-ts ./assets ./assets
-
-    # Copy all the rust code
-    COPY --keep-ts ./desktop/tauri ./desktop/tauri
-
-    # Create a empty ui dir so it will satisfy the build.
-    RUN mkdir -p ./desktop/angular/dist/tauri-builtin
-
-    SAVE IMAGE --cache-hint
-
-    # Run the linter.
-    WORKDIR /app/desktop/tauri/src-tauri
-    RUN --mount=$EARTHLY_RUST_TARGET_CACHE cargo clippy --all-targets --all-features -- -D warnings
-
-release-prep:
-    FROM +rust-base
-
-    # Linux specific
-    COPY (+tauri-build/output/portmaster --target="x86_64-unknown-linux-gnu") ./output/binary/linux_amd64/portmaster
-    COPY (+go-build/output/portmaster-core --GOARCH=amd64 --GOOS=linux --CMDS=portmaster-core) ./output/binary/linux_amd64/portmaster-core
-
-    # Windows specific
-    COPY (+tauri-build/output/portmaster.exe --target="x86_64-pc-windows-gnu") ./output/binary/windows_amd64/portmaster.exe
-    COPY (+go-build/output/portmaster-core.exe --GOARCH=amd64 --GOOS=windows --CMDS=portmaster-core) ./output/binary/windows_amd64/portmaster-core.exe
-    # TODO(vladimir): figure out a way to get the lastest release of the kext.
-    RUN touch ./output/binary/windows_amd64/portmaster-kext.sys
-
-    # All platforms
-    COPY (+assets/assets.zip) ./output/binary/all/assets.zip
-    COPY (+angular-project/output/portmaster.zip --project=portmaster --dist=./dist --configuration=production --baseHref=/ui/modules/portmaster/) ./output/binary/all/portmaster.zip
-
-    # Intel
-    # TODO(vladimir): figure out a way to download all latest intel data.
-    RUN mkdir -p ./output/intel
-    RUN wget -O ./output/intel/geoipv4.mmdb.gz "https://updates.safing.io/all/intel/geoip/geoipv4_v20240820-0-1.mmdb.gz" && \
-        wget -O ./output/intel/geoipv6.mmdb.gz "https://updates.safing.io/all/intel/geoip/geoipv6_v20240820-0-1.mmdb.gz" && \
-        wget -O ./output/intel/index.dsd "https://updates.safing.io/all/intel/lists/index_v2023-6-13.dsd" && \
-        wget -O ./output/intel/base.dsdl "https://updates.safing.io/all/intel/lists/base_v20241001-0-9.dsdl" && \
-        wget -O ./output/intel/intermediate.dsdl "https://updates.safing.io/all/intel/lists/intermediate_v20240929-0-0.dsdl" && \
-        wget -O ./output/intel/urgent.dsdl "https://updates.safing.io/all/intel/lists/urgent_v20241002-2-14.dsdl"
-
-    COPY (+go-build/output/updatemgr --GOARCH=amd64 --GOOS=linux --CMDS=updatemgr) ./updatemgr
-    RUN ./updatemgr scan --dir "./output/binary" > ./output/binary/index.json
-    RUN ./updatemgr scan --dir "./output/intel" > ./output/intel/index.json
-
-    # Intel Extracted (needed for the installers)
-    RUN mkdir -p ./output/intel_decompressed
-    RUN cp ./output/intel/index.json ./output/intel_decompressed/index.json
-    RUN gzip -dc ./output/intel/geoipv4.mmdb.gz > ./output/intel_decompressed/geoipv4.mmdb
-    RUN gzip -dc ./output/intel/geoipv6.mmdb.gz > ./output/intel_decompressed/geoipv6.mmdb
-    RUN cp ./output/intel/index.dsd ./output/intel_decompressed/index.dsd
-    RUN cp ./output/intel/base.dsdl ./output/intel_decompressed/base.dsdl
-    RUN cp ./output/intel/intermediate.dsdl ./output/intel_decompressed/intermediate.dsdl
-    RUN cp ./output/intel/urgent.dsdl ./output/intel_decompressed/urgent.dsdl
-
-    # Save all artifacts to output folder
-    SAVE ARTIFACT --if-exists --keep-ts "output/binary/index.json" AS LOCAL "${outputDir}/binary/index.json"
-    SAVE ARTIFACT --if-exists --keep-ts "output/binary/all/*" AS LOCAL "${outputDir}/binary/all/"
-    SAVE ARTIFACT --if-exists --keep-ts "output/binary/linux_amd64/*" AS LOCAL "${outputDir}/binary/linux_amd64/"
-    SAVE ARTIFACT --if-exists --keep-ts "output/binary/windows_amd64/*" AS LOCAL "${outputDir}/binary/windows_amd64/"
-    SAVE ARTIFACT --if-exists --keep-ts "output/intel/*" AS LOCAL "${outputDir}/intel/"
-    SAVE ARTIFACT --if-exists --keep-ts "output/intel_decompressed/*" AS LOCAL "${outputDir}/intel_decompressed/"
-
-    # Save all artifacts to the container output folder so other containers can access it.
-    SAVE ARTIFACT --if-exists --keep-ts "output/binary/index.json" "output/binary/index.json"
-    SAVE ARTIFACT --if-exists --keep-ts "output/binary/all/*" "output/binary/all/"
-    SAVE ARTIFACT --if-exists --keep-ts "output/binary/linux_amd64/*" "output/binary/linux_amd64/"
-    SAVE ARTIFACT --if-exists --keep-ts "output/binary/windows_amd64/*" "output/binary/windows_amd64/"
-    SAVE ARTIFACT --if-exists --keep-ts "output/intel/*" "output/intel/"
-    SAVE ARTIFACT --if-exists --keep-ts "output/intel_decompressed/*" "output/intel_decompressed/"
-
-installer-linux:
-    FROM +rust-base
-    # ARG --required target
-    ARG target="x86_64-unknown-linux-gnu"
-
-    WORKDIR /app/tauri
-    COPY --keep-ts ./desktop/tauri/ .
-    COPY assets/data ./../../assets/data
-    COPY packaging ./../../packaging
-
-    WORKDIR /app/tauri/src-tauri
-
-    SAVE IMAGE --cache-hint
-
-    DO +RUST_TO_GO_ARCH_STRING --rustTarget="${target}"
-
-    # Build and copy the binaries
-    RUN mkdir -p target/${target}/release
-    COPY (+release-prep/output/binary/linux_amd64/portmaster) ./target/${target}/release/portmaster
-
-    RUN mkdir -p binary
-    COPY (+release-prep/output/binary/index.json) ./binary/index.json
-    COPY (+release-prep/output/binary/linux_amd64/portmaster-core) ./binary/portmaster-core
-    COPY (+release-prep/output/binary/all/portmaster.zip) ./binary/portmaster.zip
-    COPY (+release-prep/output/binary/all/assets.zip) ./binary/assets.zip
-
-    # Download the intel data
-    RUN mkdir -p intel
-    COPY (+release-prep/output/intel_decompressed/*) ./intel/
-
-    # build the installers
-    RUN cargo tauri bundle --ci --target="${target}"
-
-    # Installers
-    SAVE ARTIFACT --if-exists --keep-ts "target/${target}/release/bundle/deb/*.deb" AS LOCAL "${outputDir}/${GO_ARCH_STRING}/"
-    SAVE ARTIFACT --if-exists --keep-ts "target/${target}/release/bundle/rpm/*.rpm" AS LOCAL "${outputDir}/${GO_ARCH_STRING}/"
-
 kext-build:
     FROM ${rust_builder_image}
 
diff --git a/base/api/endpoints_debug.go b/base/api/endpoints_debug.go
index 06de4cd5..55865d9c 100644
--- a/base/api/endpoints_debug.go
+++ b/base/api/endpoints_debug.go
@@ -12,7 +12,6 @@ import (
 	"time"
 
 	"github.com/safing/portmaster/base/info"
-	"github.com/safing/portmaster/base/log"
 	"github.com/safing/portmaster/base/utils/debug"
 )
 
@@ -153,12 +152,12 @@ func getStack(_ *Request) (data []byte, err error) {
 
 // printStack prints the current goroutine stack to stderr.
 func printStack(_ *Request) (msg string, err error) {
-	_, err = fmt.Fprint(log.GlobalWriter, "===== PRINTING STACK =====\n")
+	_, err = fmt.Fprint(os.Stderr, "===== PRINTING STACK =====\n")
 	if err == nil {
 		err = pprof.Lookup("goroutine").WriteTo(os.Stderr, 1)
 	}
 	if err == nil {
-		_, err = fmt.Fprint(log.GlobalWriter, "===== END OF STACK =====\n")
+		_, err = fmt.Fprint(os.Stderr, "===== END OF STACK =====\n")
 	}
 	if err != nil {
 		return "", err
diff --git a/base/config/init_test.go b/base/config/init_test.go
index d5d90aea..53967044 100644
--- a/base/config/init_test.go
+++ b/base/config/init_test.go
@@ -6,52 +6,30 @@ import (
 	"testing"
 )
 
-type testInstance struct {
-	dataDir string
-}
+type testInstance struct{}
 
 var _ instance = testInstance{}
 
-func (stub testInstance) DataDir() string {
-	return stub.dataDir
-}
-
 func (stub testInstance) SetCmdLineOperation(f func() error) {}
 
-func newTestInstance(testName string) (*testInstance, error) {
-	testDir, err := os.MkdirTemp("", fmt.Sprintf("portmaster-%s", testName))
+func runTest(m *testing.M) error {
+	ds, err := InitializeUnitTestDataroot("test-config")
 	if err != nil {
-		return nil, fmt.Errorf("failed to make tmp dir: %w", err)
+		return fmt.Errorf("failed to initialize dataroot: %w", err)
 	}
-
-	return &testInstance{
-		dataDir: testDir,
-	}, nil
-}
-
-func TestMain(m *testing.M) {
-	instance, err := newTestInstance("test-config")
+	defer func() { _ = os.RemoveAll(ds) }()
+	module, err = New(&testInstance{})
 	if err != nil {
-		panic(fmt.Errorf("failed to create test instance: %w", err))
-	}
-	defer func() { _ = os.RemoveAll(instance.DataDir()) }()
-
-	module, err = New(instance)
-	if err != nil {
-		panic(fmt.Errorf("failed to initialize module: %w", err))
+		return fmt.Errorf("failed to initialize module: %w", err)
 	}
 
 	m.Run()
+	return nil
 }
 
-func TestConfigPersistence(t *testing.T) { //nolint:paralleltest
-	err := SaveConfig()
-	if err != nil {
-		t.Fatal(err)
-	}
-
-	err = loadConfig(true)
-	if err != nil {
-		t.Fatal(err)
+func TestMain(m *testing.M) {
+	if err := runTest(m); err != nil {
+		fmt.Printf("%s\n", err)
+		os.Exit(1)
 	}
 }
diff --git a/base/config/main.go b/base/config/main.go
index dc10544c..0ed0b7e6 100644
--- a/base/config/main.go
+++ b/base/config/main.go
@@ -10,6 +10,8 @@ import (
 	"path/filepath"
 	"sort"
 
+	"github.com/safing/portmaster/base/dataroot"
+	"github.com/safing/portmaster/base/utils"
 	"github.com/safing/portmaster/base/utils/debug"
 	"github.com/safing/portmaster/service/mgr"
 )
@@ -17,13 +19,29 @@ import (
 // ChangeEvent is the name of the config change event.
 const ChangeEvent = "config change"
 
-var exportConfig bool
+var (
+	dataRoot *utils.DirStructure
+
+	exportConfig bool
+)
+
+// SetDataRoot sets the data root from which the updates module derives its paths.
+func SetDataRoot(root *utils.DirStructure) {
+	if dataRoot == nil {
+		dataRoot = root
+	}
+}
 
 func init() {
 	flag.BoolVar(&exportConfig, "export-config-options", false, "export configuration registry and exit")
 }
 
 func prep() error {
+	SetDataRoot(dataroot.Root())
+	if dataRoot == nil {
+		return errors.New("data root is not set")
+	}
+
 	if exportConfig {
 		module.instance.SetCmdLineOperation(exportConfigCmd)
 		return mgr.ErrExecuteCmdLineOp
@@ -33,7 +51,7 @@ func prep() error {
 }
 
 func start() error {
-	configFilePath = filepath.Join(module.instance.DataDir(), "config.json")
+	configFilePath = filepath.Join(dataRoot.Path, "config.json")
 
 	// Load log level from log package after it started.
 	err := loadLogLevel()
@@ -118,3 +136,20 @@ func GetActiveConfigValues() map[string]interface{} {
 
 	return values
 }
+
+// InitializeUnitTestDataroot initializes a new random tmp directory for running tests.
+func InitializeUnitTestDataroot(testName string) (string, error) {
+	basePath, err := os.MkdirTemp("", fmt.Sprintf("portmaster-%s", testName))
+	if err != nil {
+		return "", fmt.Errorf("failed to make tmp dir: %w", err)
+	}
+
+	ds := utils.NewDirStructure(basePath, 0o0755)
+	SetDataRoot(ds)
+	err = dataroot.Initialize(basePath, 0o0755)
+	if err != nil {
+		return "", fmt.Errorf("failed to initialize dataroot: %w", err)
+	}
+
+	return basePath, nil
+}
diff --git a/base/config/module.go b/base/config/module.go
index d44abe1b..465e5454 100644
--- a/base/config/module.go
+++ b/base/config/module.go
@@ -56,6 +56,5 @@ func New(instance instance) (*Config, error) {
 }
 
 type instance interface {
-	DataDir() string
 	SetCmdLineOperation(f func() error)
 }
diff --git a/base/database/database_test.go b/base/database/database_test.go
index be4971dd..03dcc66a 100644
--- a/base/database/database_test.go
+++ b/base/database/database_test.go
@@ -26,7 +26,7 @@ func TestMain(m *testing.M) {
 		panic(err)
 	}
 
-	err = Initialize(testDir)
+	err = InitializeWithPath(testDir)
 	if err != nil {
 		panic(err)
 	}
diff --git a/base/database/dbmodule/db.go b/base/database/dbmodule/db.go
index 51d121fd..99991c88 100644
--- a/base/database/dbmodule/db.go
+++ b/base/database/dbmodule/db.go
@@ -2,10 +2,11 @@ package dbmodule
 
 import (
 	"errors"
-	"path/filepath"
 	"sync/atomic"
 
 	"github.com/safing/portmaster/base/database"
+	"github.com/safing/portmaster/base/dataroot"
+	"github.com/safing/portmaster/base/utils"
 	"github.com/safing/portmaster/service/mgr"
 )
 
@@ -26,18 +27,18 @@ func (dbm *DBModule) Stop() error {
 	return stop()
 }
 
-var databasesRootDir string
+var databaseStructureRoot *utils.DirStructure
 
 // SetDatabaseLocation sets the location of the database for initialization. Supply either a path or dir structure.
-func SetDatabaseLocation(dir string) {
-	if databasesRootDir == "" {
-		databasesRootDir = dir
+func SetDatabaseLocation(dirStructureRoot *utils.DirStructure) {
+	if databaseStructureRoot == nil {
+		databaseStructureRoot = dirStructureRoot
 	}
 }
 
 func prep() error {
-	SetDatabaseLocation(filepath.Join(module.instance.DataDir(), "databases"))
-	if databasesRootDir == "" {
+	SetDatabaseLocation(dataroot.Root())
+	if databaseStructureRoot == nil {
 		return errors.New("database location not specified")
 	}
 
@@ -63,16 +64,16 @@ func New(instance instance) (*DBModule, error) {
 		return nil, errors.New("only one instance allowed")
 	}
 
+	if err := prep(); err != nil {
+		return nil, err
+	}
 	m := mgr.New("DBModule")
 	module = &DBModule{
 		mgr:      m,
 		instance: instance,
 	}
-	if err := prep(); err != nil {
-		return nil, err
-	}
 
-	err := database.Initialize(databasesRootDir)
+	err := database.Initialize(databaseStructureRoot)
 	if err != nil {
 		return nil, err
 	}
@@ -80,6 +81,4 @@ func New(instance instance) (*DBModule, error) {
 	return module, nil
 }
 
-type instance interface {
-	DataDir() string
-}
+type instance interface{}
diff --git a/base/database/main.go b/base/database/main.go
index ed0bb934..f84a0108 100644
--- a/base/database/main.go
+++ b/base/database/main.go
@@ -3,10 +3,14 @@ package database
 import (
 	"errors"
 	"fmt"
-	"os"
-	"path/filepath"
 
 	"github.com/tevino/abool"
+
+	"github.com/safing/portmaster/base/utils"
+)
+
+const (
+	databasesSubDir = "databases"
 )
 
 var (
@@ -15,18 +19,25 @@ var (
 	shuttingDown   = abool.NewBool(false)
 	shutdownSignal = make(chan struct{})
 
-	rootDir string
+	rootStructure      *utils.DirStructure
+	databasesStructure *utils.DirStructure
 )
 
-// Initialize initializes the database at the specified location.
-func Initialize(databasesRootDir string) error {
-	if initialized.SetToIf(false, true) {
-		rootDir = databasesRootDir
+// InitializeWithPath initializes the database at the specified location using a path.
+func InitializeWithPath(dirPath string) error {
+	return Initialize(utils.NewDirStructure(dirPath, 0o0755))
+}
 
-		// Ensure database root dir exists.
-		err := os.MkdirAll(rootDir, 0o0700)
+// Initialize initializes the database at the specified location using a dir structure.
+func Initialize(dirStructureRoot *utils.DirStructure) error {
+	if initialized.SetToIf(false, true) {
+		rootStructure = dirStructureRoot
+
+		// ensure root and databases dirs
+		databasesStructure = rootStructure.ChildDir(databasesSubDir, 0o0700)
+		err := databasesStructure.Ensure()
 		if err != nil {
-			return fmt.Errorf("could not create/open database directory (%s): %w", rootDir, err)
+			return fmt.Errorf("could not create/open database directory (%s): %w", rootStructure.Path, err)
 		}
 
 		return nil
@@ -56,12 +67,11 @@ func Shutdown() (err error) {
 
 // getLocation returns the storage location for the given name and type.
 func getLocation(name, storageType string) (string, error) {
-	location := filepath.Join(rootDir, name, storageType)
-
-	// Make sure location exists.
-	err := os.MkdirAll(location, 0o0700)
+	location := databasesStructure.ChildDir(name, 0o0700).ChildDir(storageType, 0o0700)
+	// check location
+	err := location.Ensure()
 	if err != nil {
-		return "", fmt.Errorf("failed to create/check database dir %q: %w", location, err)
+		return "", fmt.Errorf(`failed to create/check database dir "%s": %w`, location.Path, err)
 	}
-	return location, nil
+	return location.Path, nil
 }
diff --git a/base/dataroot/root.go b/base/dataroot/root.go
new file mode 100644
index 00000000..296b342f
--- /dev/null
+++ b/base/dataroot/root.go
@@ -0,0 +1,25 @@
+package dataroot
+
+import (
+	"errors"
+	"os"
+
+	"github.com/safing/portmaster/base/utils"
+)
+
+var root *utils.DirStructure
+
+// Initialize initializes the data root directory.
+func Initialize(rootDir string, perm os.FileMode) error {
+	if root != nil {
+		return errors.New("already initialized")
+	}
+
+	root = utils.NewDirStructure(rootDir, perm)
+	return root.Ensure()
+}
+
+// Root returns the data root directory.
+func Root() *utils.DirStructure {
+	return root
+}
diff --git a/base/info/version.go b/base/info/version.go
index 2c6c3058..91bad092 100644
--- a/base/info/version.go
+++ b/base/info/version.go
@@ -10,8 +10,6 @@ import (
 	"sync"
 )
 
-// FIXME: version does not show in portmaster
-
 var (
 	name    string
 	license string
@@ -153,28 +151,6 @@ func FullVersion() string {
 	return builder.String()
 }
 
-// CondensedVersion returns the rather complete, but condensed version string.
-func CondensedVersion() string {
-	info := GetInfo()
-
-	cgoInfo := "-cgo"
-	if info.CGO {
-		cgoInfo = "+cgo"
-	}
-	dirtyInfo := "clean"
-	if info.Dirty {
-		dirtyInfo = "dirty"
-	}
-
-	return fmt.Sprintf(
-		"%s %s (%s; built with %s [%s %s] from %s [%s] at %s)",
-		info.Name, version,
-		runtime.GOOS,
-		runtime.Version(), runtime.Compiler, cgoInfo,
-		info.Commit, dirtyInfo, info.CommitTime,
-	)
-}
-
 // CheckVersion checks if the metadata is ok.
 func CheckVersion() error {
 	switch {
diff --git a/base/log/flags.go b/base/log/flags.go
new file mode 100644
index 00000000..eb019297
--- /dev/null
+++ b/base/log/flags.go
@@ -0,0 +1,13 @@
+package log
+
+import "flag"
+
+var (
+	logLevelFlag     string
+	pkgLogLevelsFlag string
+)
+
+func init() {
+	flag.StringVar(&logLevelFlag, "log", "", "set log level to [trace|debug|info|warning|error|critical]")
+	flag.StringVar(&pkgLogLevelsFlag, "plog", "", "set log level of packages: database=trace,notifications=debug")
+}
diff --git a/base/log/input.go b/base/log/input.go
index 6dd4aa96..ef8564a9 100644
--- a/base/log/input.go
+++ b/base/log/input.go
@@ -3,6 +3,7 @@ package log
 import (
 	"fmt"
 	"runtime"
+	"strings"
 	"sync/atomic"
 	"time"
 )
@@ -24,11 +25,6 @@ func log(level Severity, msg string, tracer *ContextTracer) {
 		return
 	}
 
-	// Check log level.
-	if uint32(level) < atomic.LoadUint32(logLevel) {
-		return
-	}
-
 	// get time
 	now := time.Now()
 
@@ -45,6 +41,31 @@ func log(level Severity, msg string, tracer *ContextTracer) {
 		}
 	}
 
+	// check if level is enabled for file or generally
+	if pkgLevelsActive.IsSet() {
+		pathSegments := strings.Split(file, "/")
+		if len(pathSegments) < 2 {
+			// file too short for package levels
+			return
+		}
+		pkgLevelsLock.Lock()
+		severity, ok := pkgLevels[pathSegments[len(pathSegments)-2]]
+		pkgLevelsLock.Unlock()
+		if ok {
+			if level < severity {
+				return
+			}
+		} else {
+			// no package level set, check against global level
+			if uint32(level) < atomic.LoadUint32(logLevel) {
+				return
+			}
+		}
+	} else if uint32(level) < atomic.LoadUint32(logLevel) {
+		// no package levels set, check against global level
+		return
+	}
+
 	// create log object
 	log := &logLine{
 		msg:       msg,
@@ -80,7 +101,13 @@ func log(level Severity, msg string, tracer *ContextTracer) {
 }
 
 func fastcheck(level Severity) bool {
-	return uint32(level) >= atomic.LoadUint32(logLevel)
+	if pkgLevelsActive.IsSet() {
+		return true
+	}
+	if uint32(level) >= atomic.LoadUint32(logLevel) {
+		return true
+	}
+	return false
 }
 
 // Trace is used to log tiny steps. Log traces to context if you can!
diff --git a/base/log/logging.go b/base/log/logging.go
index d9ed43d9..b859bf11 100644
--- a/base/log/logging.go
+++ b/base/log/logging.go
@@ -2,7 +2,6 @@ package log
 
 import (
 	"fmt"
-	"log/slog"
 	"os"
 	"strings"
 	"sync"
@@ -34,26 +33,6 @@ import (
 // Severity describes a log level.
 type Severity uint32
 
-func (s Severity) toSLogLevel() slog.Level {
-	// Convert to slog level.
-	switch s {
-	case TraceLevel:
-		return slog.LevelDebug
-	case DebugLevel:
-		return slog.LevelDebug
-	case InfoLevel:
-		return slog.LevelInfo
-	case WarningLevel:
-		return slog.LevelWarn
-	case ErrorLevel:
-		return slog.LevelError
-	case CriticalLevel:
-		return slog.LevelError
-	}
-	// Failed to convert, return default log level
-	return slog.LevelWarn
-}
-
 // Message describes a log level message and is implemented
 // by logLine.
 type Message interface {
@@ -126,6 +105,10 @@ var (
 	logLevelInt = uint32(InfoLevel)
 	logLevel    = &logLevelInt
 
+	pkgLevelsActive = abool.NewBool(false)
+	pkgLevels       = make(map[string]Severity)
+	pkgLevelsLock   sync.Mutex
+
 	logsWaiting     = make(chan struct{}, 1)
 	logsWaitingFlag = abool.NewBool(false)
 
@@ -138,6 +121,19 @@ var (
 	startedSignal = make(chan struct{})
 )
 
+// SetPkgLevels sets individual log levels for packages. Only effective after Start().
+func SetPkgLevels(levels map[string]Severity) {
+	pkgLevelsLock.Lock()
+	pkgLevels = levels
+	pkgLevelsLock.Unlock()
+	pkgLevelsActive.Set()
+}
+
+// UnSetPkgLevels removes all individual log levels for packages.
+func UnSetPkgLevels() {
+	pkgLevelsActive.UnSet()
+}
+
 // GetLogLevel returns the current log level.
 func GetLogLevel() Severity {
 	return Severity(atomic.LoadUint32(logLevel))
@@ -191,36 +187,47 @@ func ParseLevel(level string) Severity {
 }
 
 // Start starts the logging system. Must be called in order to see logs.
-func Start(level string, logToStdout bool, logDir string) (err error) {
+func Start() (err error) {
 	if !initializing.SetToIf(false, true) {
 		return nil
 	}
 
-	// Parse log level argument.
-	initialLogLevel := InfoLevel
-	if level != "" {
-		initialLogLevel = ParseLevel(level)
+	logBuffer = make(chan *logLine, 1024)
+
+	if logLevelFlag != "" {
+		initialLogLevel := ParseLevel(logLevelFlag)
 		if initialLogLevel == 0 {
-			fmt.Fprintf(os.Stderr, "log warning: invalid log level %q, falling back to level info\n", level)
+			fmt.Fprintf(os.Stderr, "log warning: invalid log level \"%s\", falling back to level info\n", logLevelFlag)
 			initialLogLevel = InfoLevel
 		}
-	}
 
-	// Setup writer.
-	if logToStdout {
-		GlobalWriter = NewStdoutWriter()
+		SetLogLevel(initialLogLevel)
 	} else {
-		// Create file log writer.
-		var err error
-		GlobalWriter, err = NewFileWriter(logDir)
-		if err != nil {
-			return fmt.Errorf("failed to initialize log file: %w", err)
-		}
+		// Setup slog here for the transition period.
+		setupSLog(GetLogLevel())
 	}
 
-	// Init logging systems.
-	SetLogLevel(initialLogLevel)
-	logBuffer = make(chan *logLine, 1024)
+	// get and set file loglevels
+	pkgLogLevels := pkgLogLevelsFlag
+	if len(pkgLogLevels) > 0 {
+		newPkgLevels := make(map[string]Severity)
+		for _, pair := range strings.Split(pkgLogLevels, ",") {
+			splitted := strings.Split(pair, "=")
+			if len(splitted) != 2 {
+				err = fmt.Errorf("log warning: invalid file log level \"%s\", ignoring", pair)
+				fmt.Fprintf(os.Stderr, "%s\n", err.Error())
+				break
+			}
+			fileLevel := ParseLevel(splitted[1])
+			if fileLevel == 0 {
+				err = fmt.Errorf("log warning: invalid file log level \"%s\", ignoring", pair)
+				fmt.Fprintf(os.Stderr, "%s\n", err.Error())
+				break
+			}
+			newPkgLevels[splitted[0]] = fileLevel
+		}
+		SetPkgLevels(newPkgLevels)
+	}
 
 	if !schedulingEnabled {
 		close(writeTrigger)
@@ -230,14 +237,6 @@ func Start(level string, logToStdout bool, logDir string) (err error) {
 	started.Set()
 	close(startedSignal)
 
-	// Delete all logs older than one month.
-	if !logToStdout {
-		err = CleanOldLogs(logDir, 30*24*time.Hour)
-		if err != nil {
-			Errorf("log: failed to clean old log files: %s", err)
-		}
-	}
-
 	return err
 }
 
@@ -247,5 +246,4 @@ func Shutdown() {
 		close(shutdownSignal)
 	}
 	shutdownWaitGroup.Wait()
-	GlobalWriter.Close()
 }
diff --git a/base/log/logging_test.go b/base/log/logging_test.go
index 6996389a..577ee51a 100644
--- a/base/log/logging_test.go
+++ b/base/log/logging_test.go
@@ -7,7 +7,7 @@ import (
 )
 
 func init() {
-	err := Start("info", true, "")
+	err := Start()
 	if err != nil {
 		panic(fmt.Sprintf("start failed: %s", err))
 	}
@@ -56,6 +56,9 @@ func TestLogging(t *testing.T) {
 	// wait logs to be written
 	time.Sleep(1 * time.Millisecond)
 
+	// just for show
+	UnSetPkgLevels()
+
 	// do not really shut down, we may need logging for other tests
 	// ShutdownLogging()
 }
diff --git a/base/log/output.go b/base/log/output.go
index 91acd525..a2947dc5 100644
--- a/base/log/output.go
+++ b/base/log/output.go
@@ -2,25 +2,78 @@ package log
 
 import (
 	"fmt"
+	"os"
 	"runtime/debug"
 	"sync"
 	"time"
-
-	"github.com/safing/portmaster/base/info"
 )
 
-// Adapter is used to write logs.
-type Adapter interface {
-	// Write is called for each log message.
-	WriteMessage(msg Message, duplicates uint64)
-}
+type (
+	// Adapter is used to write logs.
+	Adapter interface {
+		// Write is called for each log message.
+		Write(msg Message, duplicates uint64)
+	}
+
+	// AdapterFunc is a convenience type for implementing
+	// Adapter.
+	AdapterFunc func(msg Message, duplicates uint64)
+
+	// FormatFunc formats msg into a string.
+	FormatFunc func(msg Message, duplicates uint64) string
+
+	// SimpleFileAdapter implements Adapter and writes all
+	// messages to File.
+	SimpleFileAdapter struct {
+		Format FormatFunc
+		File   *os.File
+	}
+)
 
 var (
+	// StdoutAdapter is a simple file adapter that writes
+	// all logs to os.Stdout using a predefined format.
+	StdoutAdapter = &SimpleFileAdapter{
+		File:   os.Stdout,
+		Format: defaultColorFormater,
+	}
+
+	// StderrAdapter is a simple file adapter that writes
+	// all logs to os.Stdout using a predefined format.
+	StderrAdapter = &SimpleFileAdapter{
+		File:   os.Stderr,
+		Format: defaultColorFormater,
+	}
+)
+
+var (
+	adapter Adapter = StdoutAdapter
+
 	schedulingEnabled = false
 	writeTrigger      = make(chan struct{})
 )
 
-// EnableScheduling enables external scheduling of the logger. This will require to manually trigger writes via TriggerWrite whenever logs should be written. Please note that full buffers will also trigger writing. Must be called before Start() to have an effect.
+// SetAdapter configures the logging adapter to use.
+// This must be called before the log package is initialized.
+func SetAdapter(a Adapter) {
+	if initializing.IsSet() || a == nil {
+		return
+	}
+
+	adapter = a
+}
+
+// Write implements Adapter and calls fn.
+func (fn AdapterFunc) Write(msg Message, duplicates uint64) {
+	fn(msg, duplicates)
+}
+
+// Write implements Adapter and writes msg the underlying file.
+func (fileAdapter *SimpleFileAdapter) Write(msg Message, duplicates uint64) {
+	fmt.Fprintln(fileAdapter.File, fileAdapter.Format(msg, duplicates))
+}
+
+// EnableScheduling enables external scheduling of the logger. This will require to manually trigger writes via TriggerWrite whenevery logs should be written. Please note that full buffers will also trigger writing. Must be called before Start() to have an effect.
 func EnableScheduling() {
 	if !initializing.IsSet() {
 		schedulingEnabled = true
@@ -42,47 +95,27 @@ func TriggerWriterChannel() chan struct{} {
 	return writeTrigger
 }
 
+func defaultColorFormater(line Message, duplicates uint64) string {
+	return formatLine(line.(*logLine), duplicates, true) //nolint:forcetypeassert // TODO: improve
+}
+
 func startWriter() {
-	if GlobalWriter.isStdout {
-		fmt.Fprintf(GlobalWriter,
-			"%s%s%s %sBOF %s%s\n",
+	fmt.Printf(
+		"%s%s%s %sBOF %s%s\n",
 
-			dimColor(),
-			time.Now().Format(timeFormat),
-			endDimColor(),
+		dimColor(),
+		time.Now().Format(timeFormat),
+		endDimColor(),
 
-			blueColor(),
-			rightArrow,
-			endColor(),
-		)
-	} else {
-		fmt.Fprintf(GlobalWriter,
-			"%s BOF %s\n",
-			time.Now().Format(timeFormat),
-			rightArrow,
-		)
-	}
-	writeVersion()
+		blueColor(),
+		rightArrow,
+		endColor(),
+	)
 
 	shutdownWaitGroup.Add(1)
 	go writerManager()
 }
 
-func writeVersion() {
-	if GlobalWriter.isStdout {
-		fmt.Fprintf(GlobalWriter, "%s%s%s running %s%s%s\n",
-			dimColor(),
-			time.Now().Format(timeFormat),
-			endDimColor(),
-
-			blueColor(),
-			info.CondensedVersion(),
-			endColor())
-	} else {
-		fmt.Fprintf(GlobalWriter, "%s running %s\n", time.Now().Format(timeFormat), info.CondensedVersion())
-	}
-}
-
 func writerManager() {
 	defer shutdownWaitGroup.Done()
 
@@ -96,17 +129,18 @@ func writerManager() {
 	}
 }
 
-func writer() error {
-	var err error
+// defer should be able to edit the err. So naked return is required.
+// nolint:golint,nakedret
+func writer() (err error) {
 	defer func() {
 		// recover from panic
 		panicVal := recover()
 		if panicVal != nil {
-			_, err = fmt.Fprintf(GlobalWriter, "%s", panicVal)
+			err = fmt.Errorf("%s", panicVal)
 
 			// write stack to stderr
 			fmt.Fprintf(
-				GlobalWriter,
+				os.Stderr,
 				`===== Error Report =====
 Message: %s
 StackTrace:
@@ -135,7 +169,7 @@ StackTrace:
 		case <-forceEmptyingOfBuffer: // log buffer is full!
 		case <-shutdownSignal: // shutting down
 			finalizeWriting()
-			return err
+			return
 		}
 
 		// wait for timeslot to log
@@ -144,7 +178,7 @@ StackTrace:
 		case <-forceEmptyingOfBuffer: // log buffer is full!
 		case <-shutdownSignal: // shutting down
 			finalizeWriting()
-			return err
+			return
 		}
 
 		// write all the logs!
@@ -167,7 +201,7 @@ StackTrace:
 				}
 
 				// if currentLine and line are _not_ equal, output currentLine
-				GlobalWriter.WriteMessage(currentLine, duplicates)
+				adapter.Write(currentLine, duplicates)
 				// add to unexpected logs
 				addUnexpectedLogs(currentLine)
 				// reset duplicate counter
@@ -181,7 +215,7 @@ StackTrace:
 
 		// write final line
 		if currentLine != nil {
-			GlobalWriter.WriteMessage(currentLine, duplicates)
+			adapter.Write(currentLine, duplicates)
 			// add to unexpected logs
 			addUnexpectedLogs(currentLine)
 		}
@@ -191,7 +225,7 @@ StackTrace:
 		case <-time.After(10 * time.Millisecond):
 		case <-shutdownSignal:
 			finalizeWriting()
-			return err
+			return
 		}
 
 	}
@@ -201,27 +235,19 @@ func finalizeWriting() {
 	for {
 		select {
 		case line := <-logBuffer:
-			GlobalWriter.WriteMessage(line, 0)
+			adapter.Write(line, 0)
 		case <-time.After(10 * time.Millisecond):
-			if GlobalWriter.isStdout {
-				fmt.Fprintf(GlobalWriter,
-					"%s%s%s %sEOF %s%s\n",
+			fmt.Printf(
+				"%s%s%s %sEOF %s%s\n",
 
-					dimColor(),
-					time.Now().Format(timeFormat),
-					endDimColor(),
+				dimColor(),
+				time.Now().Format(timeFormat),
+				endDimColor(),
 
-					blueColor(),
-					leftArrow,
-					endColor(),
-				)
-			} else {
-				fmt.Fprintf(GlobalWriter,
-					"%s EOF %s\n",
-					time.Now().Format(timeFormat),
-					leftArrow,
-				)
-			}
+				blueColor(),
+				leftArrow,
+				endColor(),
+			)
 			return
 		}
 	}
diff --git a/base/log/slog.go b/base/log/slog.go
index 5901c146..d0f09aad 100644
--- a/base/log/slog.go
+++ b/base/log/slog.go
@@ -6,36 +6,54 @@ import (
 	"runtime"
 
 	"github.com/lmittmann/tint"
+	"github.com/mattn/go-colorable"
+	"github.com/mattn/go-isatty"
 )
 
-func setupSLog(level Severity) {
-	// Set highest possible level, so it can be changed in runtime.
-	handlerLogLevel := level.toSLogLevel()
+func setupSLog(logLevel Severity) {
+	// Convert to slog level.
+	var level slog.Level
+	switch logLevel {
+	case TraceLevel:
+		level = slog.LevelDebug
+	case DebugLevel:
+		level = slog.LevelDebug
+	case InfoLevel:
+		level = slog.LevelInfo
+	case WarningLevel:
+		level = slog.LevelWarn
+	case ErrorLevel:
+		level = slog.LevelError
+	case CriticalLevel:
+		level = slog.LevelError
+	}
 
+	// Setup logging.
+	// Define output.
+	logOutput := os.Stdout
 	// Create handler depending on OS.
 	var logHandler slog.Handler
 	switch runtime.GOOS {
 	case "windows":
 		logHandler = tint.NewHandler(
-			GlobalWriter,
+			colorable.NewColorable(logOutput),
 			&tint.Options{
 				AddSource:  true,
-				Level:      handlerLogLevel,
+				Level:      level,
 				TimeFormat: timeFormat,
-				NoColor:    !GlobalWriter.IsStdout(), // FIXME: also check for tty.
 			},
 		)
 	case "linux":
-		logHandler = tint.NewHandler(GlobalWriter, &tint.Options{
+		logHandler = tint.NewHandler(logOutput, &tint.Options{
 			AddSource:  true,
-			Level:      handlerLogLevel,
+			Level:      level,
 			TimeFormat: timeFormat,
-			NoColor:    !GlobalWriter.IsStdout(), // FIXME: also check for tty.
+			NoColor:    !isatty.IsTerminal(logOutput.Fd()),
 		})
 	default:
 		logHandler = tint.NewHandler(os.Stdout, &tint.Options{
 			AddSource:  true,
-			Level:      handlerLogLevel,
+			Level:      level,
 			TimeFormat: timeFormat,
 			NoColor:    true,
 		})
@@ -43,6 +61,5 @@ func setupSLog(level Severity) {
 
 	// Set as default logger.
 	slog.SetDefault(slog.New(logHandler))
-	// Set actual log level.
-	slog.SetLogLoggerLevel(handlerLogLevel)
+	slog.SetLogLoggerLevel(level)
 }
diff --git a/base/log/trace.go b/base/log/trace.go
index 2226339c..640594d4 100644
--- a/base/log/trace.go
+++ b/base/log/trace.go
@@ -4,6 +4,7 @@ import (
 	"context"
 	"fmt"
 	"runtime"
+	"strings"
 	"sync"
 	"sync/atomic"
 	"time"
@@ -23,8 +24,36 @@ var key = ContextTracerKey{}
 // AddTracer adds a ContextTracer to the returned Context. Will return a nil ContextTracer if logging level is not set to trace. Will return a nil ContextTracer if one already exists. Will return a nil ContextTracer in case of an error. Will return a nil context if nil.
 func AddTracer(ctx context.Context) (context.Context, *ContextTracer) {
 	if ctx != nil && fastcheck(TraceLevel) {
-		// Check log level.
-		if atomic.LoadUint32(logLevel) > uint32(TraceLevel) {
+		// check pkg levels
+		if pkgLevelsActive.IsSet() {
+			// get file
+			_, file, _, ok := runtime.Caller(1)
+			if !ok {
+				// cannot get file, ignore
+				return ctx, nil
+			}
+
+			pathSegments := strings.Split(file, "/")
+			if len(pathSegments) < 2 {
+				// file too short for package levels
+				return ctx, nil
+			}
+			pkgLevelsLock.Lock()
+			severity, ok := pkgLevels[pathSegments[len(pathSegments)-2]]
+			pkgLevelsLock.Unlock()
+			if ok {
+				// check against package level
+				if TraceLevel < severity {
+					return ctx, nil
+				}
+			} else {
+				// no package level set, check against global level
+				if uint32(TraceLevel) < atomic.LoadUint32(logLevel) {
+					return ctx, nil
+				}
+			}
+		} else if uint32(TraceLevel) < atomic.LoadUint32(logLevel) {
+			// no package levels set, check against global level
 			return ctx, nil
 		}
 
diff --git a/base/log/writer.go b/base/log/writer.go
deleted file mode 100644
index 6c10716f..00000000
--- a/base/log/writer.go
+++ /dev/null
@@ -1,119 +0,0 @@
-package log
-
-import (
-	"fmt"
-	"os"
-	"path/filepath"
-	"sync"
-	"time"
-)
-
-// GlobalWriter is the global log writer.
-var GlobalWriter *LogWriter = nil
-
-type LogWriter struct {
-	writeLock sync.Mutex
-	isStdout  bool
-	file      *os.File
-}
-
-// NewStdoutWriter creates a new log writer thet will write to the stdout.
-func NewStdoutWriter() *LogWriter {
-	return &LogWriter{
-		file:     os.Stdout,
-		isStdout: true,
-	}
-}
-
-// NewFileWriter creates a new log writer that will write to a file. The file path will be <dir>/2006-01-02_15-04-05.log (with current date and time)
-func NewFileWriter(dir string) (*LogWriter, error) {
-	// Make sure log dir exists, if not, create with strict permission, as logs can contain sensitive data.
-	_ = os.MkdirAll(dir, 0o700)
-
-	// Open new log file.
-	logFile := time.Now().UTC().Format("2006-01-02_15-04-05") + ".log"
-	file, err := os.Create(filepath.Join(dir, logFile))
-	if err != nil {
-		return nil, err
-	}
-
-	return &LogWriter{
-		file:     file,
-		isStdout: false,
-	}, nil
-}
-
-// Write writes the buffer to the writer.
-func (l *LogWriter) Write(buf []byte) (int, error) {
-	if l == nil {
-		return 0, fmt.Errorf("log writer not initialized")
-	}
-
-	// No need to lock in stdout context.
-	if !l.isStdout {
-		l.writeLock.Lock()
-		defer l.writeLock.Unlock()
-	}
-
-	return l.file.Write(buf)
-}
-
-// WriteMessage writes the message to the writer.
-func (l *LogWriter) WriteMessage(msg Message, duplicates uint64) {
-	if l == nil {
-		return
-	}
-
-	// No need to lock in stdout context.
-	if !l.isStdout {
-		l.writeLock.Lock()
-		defer l.writeLock.Unlock()
-	}
-
-	fmt.Fprintln(l.file, formatLine(msg.(*logLine), duplicates, l.isStdout))
-}
-
-// IsStdout returns true if writer was initialized with stdout.
-func (l *LogWriter) IsStdout() bool {
-	return l != nil && l.isStdout
-}
-
-// Close closes the writer.
-func (l *LogWriter) Close() {
-	if l != nil && !l.isStdout {
-		_ = l.file.Close()
-	}
-}
-
-// CleanOldLogs deletes all log files in given directory that are older than the given threshold.
-func CleanOldLogs(dir string, threshold time.Duration) error {
-	// Get current log file name.
-	var currentLogFile string
-	if GlobalWriter != nil && GlobalWriter.file != nil {
-		currentLogFile = GlobalWriter.file.Name()
-	}
-
-	// Read dir entries.
-	files, err := os.ReadDir(dir)
-	if err != nil {
-		return fmt.Errorf("failed to read dir: %w", err)
-	}
-
-	// Remove files older than threshold
-	deleteOlderThan := time.Now().Add(-threshold)
-	for _, f := range files {
-		// Skip directories and the current log file.
-		if f.IsDir() || f.Name() == currentLogFile {
-			continue
-		}
-
-		// Delete log files.
-		if fileInfo, err := f.Info(); err == nil {
-			if fileInfo.ModTime().Before(deleteOlderThan) {
-				_ = os.Remove(filepath.Join(dir, f.Name()))
-			}
-		}
-	}
-
-	return nil
-}
diff --git a/base/metrics/metrics_host.go b/base/metrics/metrics_host.go
index 2dd557f6..5b632a8d 100644
--- a/base/metrics/metrics_host.go
+++ b/base/metrics/metrics_host.go
@@ -10,6 +10,7 @@ import (
 	"github.com/shirou/gopsutil/mem"
 
 	"github.com/safing/portmaster/base/api"
+	"github.com/safing/portmaster/base/dataroot"
 	"github.com/safing/portmaster/base/log"
 )
 
@@ -208,9 +209,18 @@ func getDiskStat() *disk.UsageStat {
 		return diskStat
 	}
 
+	// Check if we have a data root.
+	dataRoot := dataroot.Root()
+	if dataRoot == nil {
+		log.Warning("metrics: cannot get disk stats without data root")
+		diskStat = nil
+		diskStatExpires = time.Now().Add(hostStatTTL)
+		return diskStat
+	}
+
 	// Refresh.
 	var err error
-	diskStat, err = disk.Usage(module.instance.DataDir())
+	diskStat, err = disk.Usage(dataRoot.Path)
 	if err != nil {
 		log.Warningf("metrics: failed to get load avg: %s", err)
 		diskStat = nil
diff --git a/base/metrics/module.go b/base/metrics/module.go
index 48a1c2b7..a1e5bd37 100644
--- a/base/metrics/module.go
+++ b/base/metrics/module.go
@@ -213,6 +213,4 @@ func New(instance instance) (*Metrics, error) {
 	return module, nil
 }
 
-type instance interface {
-	DataDir() string
-}
+type instance interface{}
diff --git a/base/notifications/module.go b/base/notifications/module.go
index bbbb88cd..f69d017e 100644
--- a/base/notifications/module.go
+++ b/base/notifications/module.go
@@ -36,47 +36,6 @@ func (n *Notifications) Stop() error {
 	return nil
 }
 
-// NotifyInfo is a helper method for quickly showing an info notification.
-// The notification will be activated immediately.
-// If the provided id is empty, an id will derived from msg.
-// ShowOnSystem is disabled.
-// If no actions are defined, a default "OK" (ID:"ack") action will be added.
-func (n *Notifications) NotifyInfo(id, title, msg string, actions ...Action) *Notification {
-	return NotifyInfo(id, title, msg, actions...)
-}
-
-// NotifyWarn is a helper method for quickly showing a warning notification
-// The notification will be activated immediately.
-// If the provided id is empty, an id will derived from msg.
-// ShowOnSystem is enabled.
-// If no actions are defined, a default "OK" (ID:"ack") action will be added.
-func (n *Notifications) NotifyWarn(id, title, msg string, actions ...Action) *Notification {
-	return NotifyWarn(id, title, msg, actions...)
-}
-
-// NotifyError is a helper method for quickly showing an error notification.
-// The notification will be activated immediately.
-// If the provided id is empty, an id will derived from msg.
-// ShowOnSystem is enabled.
-// If no actions are defined, a default "OK" (ID:"ack") action will be added.
-func (n *Notifications) NotifyError(id, title, msg string, actions ...Action) *Notification {
-	return NotifyError(id, title, msg, actions...)
-}
-
-// NotifyPrompt is a helper method for quickly showing a prompt notification.
-// The notification will be activated immediately.
-// If the provided id is empty, an id will derived from msg.
-// ShowOnSystem is disabled.
-// If no actions are defined, a default "OK" (ID:"ack") action will be added.
-func (n *Notifications) NotifyPrompt(id, title, msg string, actions ...Action) *Notification {
-	return NotifyPrompt(id, title, msg, actions...)
-}
-
-// Notify sends the given notification.
-func (n *Notifications) Notify(notification *Notification) *Notification {
-	return Notify(notification)
-}
-
 func prep() error {
 	return registerConfig()
 }
diff --git a/base/updater/doc.go b/base/updater/doc.go
new file mode 100644
index 00000000..829a5bd3
--- /dev/null
+++ b/base/updater/doc.go
@@ -0,0 +1,2 @@
+// Package updater is an update registry that manages updates and versions.
+package updater
diff --git a/base/updater/export.go b/base/updater/export.go
new file mode 100644
index 00000000..55b64a3f
--- /dev/null
+++ b/base/updater/export.go
@@ -0,0 +1,15 @@
+package updater
+
+// Export exports the list of resources.
+func (reg *ResourceRegistry) Export() map[string]*Resource {
+	reg.RLock()
+	defer reg.RUnlock()
+
+	// copy the map
+	copiedResources := make(map[string]*Resource)
+	for key, val := range reg.resources {
+		copiedResources[key] = val.Export()
+	}
+
+	return copiedResources
+}
diff --git a/base/updater/fetch.go b/base/updater/fetch.go
new file mode 100644
index 00000000..f324709d
--- /dev/null
+++ b/base/updater/fetch.go
@@ -0,0 +1,347 @@
+package updater
+
+import (
+	"bytes"
+	"context"
+	"errors"
+	"fmt"
+	"hash"
+	"io"
+	"net/http"
+	"net/url"
+	"os"
+	"path"
+	"path/filepath"
+	"time"
+
+	"github.com/safing/jess/filesig"
+	"github.com/safing/jess/lhash"
+	"github.com/safing/portmaster/base/log"
+	"github.com/safing/portmaster/base/utils/renameio"
+)
+
+func (reg *ResourceRegistry) fetchFile(ctx context.Context, client *http.Client, rv *ResourceVersion, tries int) error {
+	// backoff when retrying
+	if tries > 0 {
+		select {
+		case <-ctx.Done():
+			return nil // module is shutting down
+		case <-time.After(time.Duration(tries*tries) * time.Second):
+		}
+	}
+
+	// check destination dir
+	dirPath := filepath.Dir(rv.storagePath())
+	err := reg.storageDir.EnsureAbsPath(dirPath)
+	if err != nil {
+		return fmt.Errorf("could not create updates folder: %s", dirPath)
+	}
+
+	// If verification is enabled, download signature first.
+	var (
+		verifiedHash *lhash.LabeledHash
+		sigFileData  []byte
+	)
+	if rv.resource.VerificationOptions != nil {
+		verifiedHash, sigFileData, err = reg.fetchAndVerifySigFile(
+			ctx, client,
+			rv.resource.VerificationOptions,
+			rv.versionedSigPath(), rv.SigningMetadata(),
+			tries,
+		)
+		if err != nil {
+			switch rv.resource.VerificationOptions.DownloadPolicy {
+			case SignaturePolicyRequire:
+				return fmt.Errorf("signature verification failed: %w", err)
+			case SignaturePolicyWarn:
+				log.Warningf("%s: failed to verify downloaded signature of %s: %s", reg.Name, rv.versionedPath(), err)
+			case SignaturePolicyDisable:
+				log.Debugf("%s: failed to verify downloaded signature of %s: %s", reg.Name, rv.versionedPath(), err)
+			}
+		}
+	}
+
+	// open file for writing
+	atomicFile, err := renameio.TempFile(reg.tmpDir.Path, rv.storagePath())
+	if err != nil {
+		return fmt.Errorf("could not create temp file for download: %w", err)
+	}
+	defer atomicFile.Cleanup() //nolint:errcheck // ignore error for now, tmp dir will be cleaned later again anyway
+
+	// start file download
+	resp, downloadURL, err := reg.makeRequest(ctx, client, rv.versionedPath(), tries)
+	if err != nil {
+		return err
+	}
+	defer func() {
+		_ = resp.Body.Close()
+	}()
+
+	// Write to the hasher at the same time, if needed.
+	var hasher hash.Hash
+	var writeDst io.Writer = atomicFile
+	if verifiedHash != nil {
+		hasher = verifiedHash.Algorithm().RawHasher()
+		writeDst = io.MultiWriter(hasher, atomicFile)
+	}
+
+	// Download and write file.
+	n, err := io.Copy(writeDst, resp.Body)
+	if err != nil {
+		return fmt.Errorf("failed to download %q: %w", downloadURL, err)
+	}
+	if resp.ContentLength != n {
+		return fmt.Errorf("failed to finish download of %q: written %d out of %d bytes", downloadURL, n, resp.ContentLength)
+	}
+
+	// Before file is finalized, check if hash, if available.
+	if hasher != nil {
+		downloadDigest := hasher.Sum(nil)
+		if verifiedHash.EqualRaw(downloadDigest) {
+			log.Infof("%s: verified signature of %s", reg.Name, downloadURL)
+		} else {
+			switch rv.resource.VerificationOptions.DownloadPolicy {
+			case SignaturePolicyRequire:
+				return errors.New("file does not match signed checksum")
+			case SignaturePolicyWarn:
+				log.Warningf("%s: checksum does not match file from %s", reg.Name, downloadURL)
+			case SignaturePolicyDisable:
+				log.Debugf("%s: checksum does not match file from %s", reg.Name, downloadURL)
+			}
+
+			// Reset hasher to signal that the sig should not be written.
+			hasher = nil
+		}
+	}
+
+	// Write signature file, if we have one and if verification succeeded.
+	if len(sigFileData) > 0 && hasher != nil {
+		sigFilePath := rv.storagePath() + filesig.Extension
+		err := os.WriteFile(sigFilePath, sigFileData, 0o0644) //nolint:gosec
+		if err != nil {
+			switch rv.resource.VerificationOptions.DownloadPolicy {
+			case SignaturePolicyRequire:
+				return fmt.Errorf("failed to write signature file %s: %w", sigFilePath, err)
+			case SignaturePolicyWarn:
+				log.Warningf("%s: failed to write signature file %s: %s", reg.Name, sigFilePath, err)
+			case SignaturePolicyDisable:
+				log.Debugf("%s: failed to write signature file %s: %s", reg.Name, sigFilePath, err)
+			}
+		}
+	}
+
+	// finalize file
+	err = atomicFile.CloseAtomicallyReplace()
+	if err != nil {
+		return fmt.Errorf("%s: failed to finalize file %s: %w", reg.Name, rv.storagePath(), err)
+	}
+	// set permissions
+	if !onWindows {
+		// TODO: only set executable files to 0755, set other to 0644
+		err = os.Chmod(rv.storagePath(), 0o0755) //nolint:gosec // See TODO above.
+		if err != nil {
+			log.Warningf("%s: failed to set permissions on downloaded file %s: %s", reg.Name, rv.storagePath(), err)
+		}
+	}
+
+	log.Debugf("%s: fetched %s and stored to %s", reg.Name, downloadURL, rv.storagePath())
+	return nil
+}
+
+func (reg *ResourceRegistry) fetchMissingSig(ctx context.Context, client *http.Client, rv *ResourceVersion, tries int) error {
+	// backoff when retrying
+	if tries > 0 {
+		select {
+		case <-ctx.Done():
+			return nil // module is shutting down
+		case <-time.After(time.Duration(tries*tries) * time.Second):
+		}
+	}
+
+	// Check destination dir.
+	dirPath := filepath.Dir(rv.storagePath())
+	err := reg.storageDir.EnsureAbsPath(dirPath)
+	if err != nil {
+		return fmt.Errorf("could not create updates folder: %s", dirPath)
+	}
+
+	// Download and verify the missing signature.
+	verifiedHash, sigFileData, err := reg.fetchAndVerifySigFile(
+		ctx, client,
+		rv.resource.VerificationOptions,
+		rv.versionedSigPath(), rv.SigningMetadata(),
+		tries,
+	)
+	if err != nil {
+		switch rv.resource.VerificationOptions.DownloadPolicy {
+		case SignaturePolicyRequire:
+			return fmt.Errorf("signature verification failed: %w", err)
+		case SignaturePolicyWarn:
+			log.Warningf("%s: failed to verify downloaded signature of %s: %s", reg.Name, rv.versionedPath(), err)
+		case SignaturePolicyDisable:
+			log.Debugf("%s: failed to verify downloaded signature of %s: %s", reg.Name, rv.versionedPath(), err)
+		}
+		return nil
+	}
+
+	// Check if the signature matches the resource file.
+	ok, err := verifiedHash.MatchesFile(rv.storagePath())
+	if err != nil {
+		switch rv.resource.VerificationOptions.DownloadPolicy {
+		case SignaturePolicyRequire:
+			return fmt.Errorf("error while verifying resource file: %w", err)
+		case SignaturePolicyWarn:
+			log.Warningf("%s: error while verifying resource file %s", reg.Name, rv.storagePath())
+		case SignaturePolicyDisable:
+			log.Debugf("%s: error while verifying resource file %s", reg.Name, rv.storagePath())
+		}
+		return nil
+	}
+	if !ok {
+		switch rv.resource.VerificationOptions.DownloadPolicy {
+		case SignaturePolicyRequire:
+			return errors.New("resource file does not match signed checksum")
+		case SignaturePolicyWarn:
+			log.Warningf("%s: checksum does not match resource file from %s", reg.Name, rv.storagePath())
+		case SignaturePolicyDisable:
+			log.Debugf("%s: checksum does not match resource file from %s", reg.Name, rv.storagePath())
+		}
+		return nil
+	}
+
+	// Write signature file.
+	err = os.WriteFile(rv.storageSigPath(), sigFileData, 0o0644) //nolint:gosec
+	if err != nil {
+		switch rv.resource.VerificationOptions.DownloadPolicy {
+		case SignaturePolicyRequire:
+			return fmt.Errorf("failed to write signature file %s: %w", rv.storageSigPath(), err)
+		case SignaturePolicyWarn:
+			log.Warningf("%s: failed to write signature file %s: %s", reg.Name, rv.storageSigPath(), err)
+		case SignaturePolicyDisable:
+			log.Debugf("%s: failed to write signature file %s: %s", reg.Name, rv.storageSigPath(), err)
+		}
+	}
+
+	log.Debugf("%s: fetched %s and stored to %s", reg.Name, rv.versionedSigPath(), rv.storageSigPath())
+	return nil
+}
+
+func (reg *ResourceRegistry) fetchAndVerifySigFile(ctx context.Context, client *http.Client, verifOpts *VerificationOptions, sigFilePath string, requiredMetadata map[string]string, tries int) (*lhash.LabeledHash, []byte, error) {
+	// Download signature file.
+	resp, _, err := reg.makeRequest(ctx, client, sigFilePath, tries)
+	if err != nil {
+		return nil, nil, err
+	}
+	defer func() {
+		_ = resp.Body.Close()
+	}()
+	sigFileData, err := io.ReadAll(resp.Body)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	// Extract all signatures.
+	sigs, err := filesig.ParseSigFile(sigFileData)
+	switch {
+	case len(sigs) == 0 && err != nil:
+		return nil, nil, fmt.Errorf("failed to parse signature file: %w", err)
+	case len(sigs) == 0:
+		return nil, nil, errors.New("no signatures found in signature file")
+	case err != nil:
+		return nil, nil, fmt.Errorf("failed to parse signature file: %w", err)
+	}
+
+	// Verify all signatures.
+	var verifiedHash *lhash.LabeledHash
+	for _, sig := range sigs {
+		fd, err := filesig.VerifyFileData(
+			sig,
+			requiredMetadata,
+			verifOpts.TrustStore,
+		)
+		if err != nil {
+			return nil, sigFileData, err
+		}
+
+		// Save or check verified hash.
+		if verifiedHash == nil {
+			verifiedHash = fd.FileHash()
+		} else if !fd.FileHash().Equal(verifiedHash) {
+			// Return an error if two valid hashes mismatch.
+			// For simplicity, all hash algorithms must be the same for now.
+			return nil, sigFileData, errors.New("file hashes from different signatures do not match")
+		}
+	}
+
+	return verifiedHash, sigFileData, nil
+}
+
+func (reg *ResourceRegistry) fetchData(ctx context.Context, client *http.Client, downloadPath string, tries int) (fileData []byte, downloadedFrom string, err error) {
+	// backoff when retrying
+	if tries > 0 {
+		select {
+		case <-ctx.Done():
+			return nil, "", nil // module is shutting down
+		case <-time.After(time.Duration(tries*tries) * time.Second):
+		}
+	}
+
+	// start file download
+	resp, downloadURL, err := reg.makeRequest(ctx, client, downloadPath, tries)
+	if err != nil {
+		return nil, downloadURL, err
+	}
+	defer func() {
+		_ = resp.Body.Close()
+	}()
+
+	// download and write file
+	buf := bytes.NewBuffer(make([]byte, 0, resp.ContentLength))
+	n, err := io.Copy(buf, resp.Body)
+	if err != nil {
+		return nil, downloadURL, fmt.Errorf("failed to download %q: %w", downloadURL, err)
+	}
+	if resp.ContentLength != n {
+		return nil, downloadURL, fmt.Errorf("failed to finish download of %q: written %d out of %d bytes", downloadURL, n, resp.ContentLength)
+	}
+
+	return buf.Bytes(), downloadURL, nil
+}
+
+func (reg *ResourceRegistry) makeRequest(ctx context.Context, client *http.Client, downloadPath string, tries int) (resp *http.Response, downloadURL string, err error) {
+	// parse update URL
+	updateBaseURL := reg.UpdateURLs[tries%len(reg.UpdateURLs)]
+	u, err := url.Parse(updateBaseURL)
+	if err != nil {
+		return nil, "", fmt.Errorf("failed to parse update URL %q: %w", updateBaseURL, err)
+	}
+	// add download path
+	u.Path = path.Join(u.Path, downloadPath)
+	// compile URL
+	downloadURL = u.String()
+
+	// create request
+	req, err := http.NewRequestWithContext(ctx, http.MethodGet, downloadURL, http.NoBody)
+	if err != nil {
+		return nil, "", fmt.Errorf("failed to create request for %q: %w", downloadURL, err)
+	}
+
+	// set user agent
+	if reg.UserAgent != "" {
+		req.Header.Set("User-Agent", reg.UserAgent)
+	}
+
+	// start request
+	resp, err = client.Do(req)
+	if err != nil {
+		return nil, "", fmt.Errorf("failed to make request to %q: %w", downloadURL, err)
+	}
+
+	// check return code
+	if resp.StatusCode != http.StatusOK {
+		_ = resp.Body.Close()
+		return nil, "", fmt.Errorf("failed to fetch %q: %d %s", downloadURL, resp.StatusCode, resp.Status)
+	}
+
+	return resp, downloadURL, err
+}
diff --git a/base/updater/file.go b/base/updater/file.go
new file mode 100644
index 00000000..90b7d356
--- /dev/null
+++ b/base/updater/file.go
@@ -0,0 +1,156 @@
+package updater
+
+import (
+	"errors"
+	"io"
+	"io/fs"
+	"os"
+	"strings"
+
+	semver "github.com/hashicorp/go-version"
+
+	"github.com/safing/jess/filesig"
+	"github.com/safing/portmaster/base/log"
+	"github.com/safing/portmaster/base/utils"
+)
+
+// File represents a file from the update system.
+type File struct {
+	resource      *Resource
+	version       *ResourceVersion
+	notifier      *notifier
+	versionedPath string
+	storagePath   string
+}
+
+// Identifier returns the identifier of the file.
+func (file *File) Identifier() string {
+	return file.resource.Identifier
+}
+
+// Version returns the version of the file.
+func (file *File) Version() string {
+	return file.version.VersionNumber
+}
+
+// SemVer returns the semantic version of the file.
+func (file *File) SemVer() *semver.Version {
+	return file.version.semVer
+}
+
+// EqualsVersion normalizes the given version and checks equality with semver.
+func (file *File) EqualsVersion(version string) bool {
+	return file.version.EqualsVersion(version)
+}
+
+// Path returns the absolute filepath of the file.
+func (file *File) Path() string {
+	return file.storagePath
+}
+
+// SigningMetadata returns the metadata to be included in signatures.
+func (file *File) SigningMetadata() map[string]string {
+	return map[string]string{
+		"id":      file.Identifier(),
+		"version": file.Version(),
+	}
+}
+
+// Verify verifies the given file.
+func (file *File) Verify() ([]*filesig.FileData, error) {
+	// Check if verification is configured.
+	if file.resource.VerificationOptions == nil {
+		return nil, ErrVerificationNotConfigured
+	}
+
+	// Verify file.
+	fileData, err := filesig.VerifyFile(
+		file.storagePath,
+		file.storagePath+filesig.Extension,
+		file.SigningMetadata(),
+		file.resource.VerificationOptions.TrustStore,
+	)
+	if err != nil {
+		switch file.resource.VerificationOptions.DiskLoadPolicy {
+		case SignaturePolicyRequire:
+			return nil, err
+		case SignaturePolicyWarn:
+			log.Warningf("%s: failed to verify %s: %s", file.resource.registry.Name, file.storagePath, err)
+		case SignaturePolicyDisable:
+			log.Debugf("%s: failed to verify %s: %s", file.resource.registry.Name, file.storagePath, err)
+		}
+	}
+
+	return fileData, nil
+}
+
+// Blacklist notifies the update system that this file is somehow broken, and should be ignored from now on, until restarted.
+func (file *File) Blacklist() error {
+	return file.resource.Blacklist(file.version.VersionNumber)
+}
+
+// markActiveWithLocking marks the file as active, locking the resource in the process.
+func (file *File) markActiveWithLocking() {
+	file.resource.Lock()
+	defer file.resource.Unlock()
+
+	// update last used version
+	if file.resource.ActiveVersion != file.version {
+		log.Debugf("updater: setting active version of resource %s from %s to %s", file.resource.Identifier, file.resource.ActiveVersion, file.version.VersionNumber)
+		file.resource.ActiveVersion = file.version
+	}
+}
+
+// Unpacker describes the function that is passed to
+// File.Unpack. It receives a reader to the compressed/packed
+// file and should return a reader that provides
+// unpacked file contents. If the returned reader implements
+// io.Closer it's close method is invoked when an error
+// or io.EOF is returned from Read().
+type Unpacker func(io.Reader) (io.Reader, error)
+
+// Unpack returns the path to the unpacked version of file and
+// unpacks it on demand using unpacker.
+func (file *File) Unpack(suffix string, unpacker Unpacker) (string, error) {
+	path := strings.TrimSuffix(file.Path(), suffix)
+
+	if suffix == "" {
+		path += "-unpacked"
+	}
+
+	_, err := os.Stat(path)
+	if err == nil {
+		return path, nil
+	}
+
+	if !errors.Is(err, fs.ErrNotExist) {
+		return "", err
+	}
+
+	f, err := os.Open(file.Path())
+	if err != nil {
+		return "", err
+	}
+	defer func() {
+		_ = f.Close()
+	}()
+
+	r, err := unpacker(f)
+	if err != nil {
+		return "", err
+	}
+
+	ioErr := utils.CreateAtomic(path, r, &utils.AtomicFileOptions{
+		TempDir: file.resource.registry.TmpDir().Path,
+	})
+
+	if c, ok := r.(io.Closer); ok {
+		if err := c.Close(); err != nil && ioErr == nil {
+			// if ioErr is already set we ignore the error from
+			// closing the unpacker.
+			ioErr = err
+		}
+	}
+
+	return path, ioErr
+}
diff --git a/base/updater/filename.go b/base/updater/filename.go
new file mode 100644
index 00000000..69e9db00
--- /dev/null
+++ b/base/updater/filename.go
@@ -0,0 +1,57 @@
+package updater
+
+import (
+	"path"
+	"regexp"
+	"strings"
+)
+
+var (
+	fileVersionRegex = regexp.MustCompile(`_v[0-9]+-[0-9]+-[0-9]+(-[a-z]+)?`)
+	rawVersionRegex  = regexp.MustCompile(`^[0-9]+\.[0-9]+\.[0-9]+(-[a-z]+)?$`)
+)
+
+// GetIdentifierAndVersion splits the given file path into its identifier and version.
+func GetIdentifierAndVersion(versionedPath string) (identifier, version string, ok bool) {
+	dirPath, filename := path.Split(versionedPath)
+
+	// Extract version from filename.
+	rawVersion := fileVersionRegex.FindString(filename)
+	if rawVersion == "" {
+		// No version present in file, making it invalid.
+		return "", "", false
+	}
+
+	// Trim the `_v` that gets caught by the regex and
+	// replace `-` with `.` to get the version string.
+	version = strings.Replace(strings.TrimLeft(rawVersion, "_v"), "-", ".", 2)
+
+	// Put the filename back together without version.
+	i := strings.Index(filename, rawVersion)
+	if i < 0 {
+		// extracted version not in string (impossible)
+		return "", "", false
+	}
+	filename = filename[:i] + filename[i+len(rawVersion):]
+
+	// Put the full path back together and return it.
+	// `dirPath + filename` is guaranteed by path.Split()
+	return dirPath + filename, version, true
+}
+
+// GetVersionedPath combines the identifier and version and returns it as a file path.
+func GetVersionedPath(identifier, version string) (versionedPath string) {
+	identifierPath, filename := path.Split(identifier)
+
+	// Split the filename where the version should go.
+	splittedFilename := strings.SplitN(filename, ".", 2)
+	// Replace `.` with `-` for the filename format.
+	transformedVersion := strings.Replace(version, ".", "-", 2)
+
+	// Put everything back together and return it.
+	versionedPath = identifierPath + splittedFilename[0] + "_v" + transformedVersion
+	if len(splittedFilename) > 1 {
+		versionedPath += "." + splittedFilename[1]
+	}
+	return versionedPath
+}
diff --git a/base/updater/filename_test.go b/base/updater/filename_test.go
new file mode 100644
index 00000000..cf5fb922
--- /dev/null
+++ b/base/updater/filename_test.go
@@ -0,0 +1,80 @@
+package updater
+
+import (
+	"regexp"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func testRegexMatch(t *testing.T, testRegex *regexp.Regexp, testString string, shouldMatch bool) {
+	t.Helper()
+
+	if testRegex.MatchString(testString) != shouldMatch {
+		if shouldMatch {
+			t.Errorf("regex %s should match %s", testRegex, testString)
+		} else {
+			t.Errorf("regex %s should not match %s", testRegex, testString)
+		}
+	}
+}
+
+func testRegexFind(t *testing.T, testRegex *regexp.Regexp, testString string, shouldMatch bool) {
+	t.Helper()
+
+	if (testRegex.FindString(testString) != "") != shouldMatch {
+		if shouldMatch {
+			t.Errorf("regex %s should find %s", testRegex, testString)
+		} else {
+			t.Errorf("regex %s should not find %s", testRegex, testString)
+		}
+	}
+}
+
+func testVersionTransformation(t *testing.T, testFilename, testIdentifier, testVersion string) {
+	t.Helper()
+
+	identifier, version, ok := GetIdentifierAndVersion(testFilename)
+	if !ok {
+		t.Errorf("failed to get identifier and version of %s", testFilename)
+	}
+	assert.Equal(t, testIdentifier, identifier, "identifier does not match")
+	assert.Equal(t, testVersion, version, "version does not match")
+
+	versionedPath := GetVersionedPath(testIdentifier, testVersion)
+	assert.Equal(t, testFilename, versionedPath, "filename (versioned path) does not match")
+}
+
+func TestRegexes(t *testing.T) {
+	t.Parallel()
+
+	testRegexMatch(t, rawVersionRegex, "0.1.2", true)
+	testRegexMatch(t, rawVersionRegex, "0.1.2-beta", true)
+	testRegexMatch(t, rawVersionRegex, "0.1.2-staging", true)
+	testRegexMatch(t, rawVersionRegex, "12.13.14", true)
+
+	testRegexMatch(t, rawVersionRegex, "v0.1.2", false)
+	testRegexMatch(t, rawVersionRegex, "0.", false)
+	testRegexMatch(t, rawVersionRegex, "0.1", false)
+	testRegexMatch(t, rawVersionRegex, "0.1.", false)
+	testRegexMatch(t, rawVersionRegex, ".1.2", false)
+	testRegexMatch(t, rawVersionRegex, ".1.", false)
+	testRegexMatch(t, rawVersionRegex, "012345", false)
+
+	testRegexFind(t, fileVersionRegex, "/path/to/file_v0-0-0", true)
+	testRegexFind(t, fileVersionRegex, "/path/to/file_v1-2-3", true)
+	testRegexFind(t, fileVersionRegex, "/path/to/file_v1-2-3.exe", true)
+
+	testRegexFind(t, fileVersionRegex, "/path/to/file-v1-2-3", false)
+	testRegexFind(t, fileVersionRegex, "/path/to/file_v1.2.3", false)
+	testRegexFind(t, fileVersionRegex, "/path/to/file_1-2-3", false)
+	testRegexFind(t, fileVersionRegex, "/path/to/file_v1-2", false)
+	testRegexFind(t, fileVersionRegex, "/path/to/file-v1-2-3", false)
+
+	testVersionTransformation(t, "/path/to/file_v0-0-0", "/path/to/file", "0.0.0")
+	testVersionTransformation(t, "/path/to/file_v1-2-3", "/path/to/file", "1.2.3")
+	testVersionTransformation(t, "/path/to/file_v1-2-3-beta", "/path/to/file", "1.2.3-beta")
+	testVersionTransformation(t, "/path/to/file_v1-2-3-staging", "/path/to/file", "1.2.3-staging")
+	testVersionTransformation(t, "/path/to/file_v1-2-3.exe", "/path/to/file.exe", "1.2.3")
+	testVersionTransformation(t, "/path/to/file_v1-2-3-staging.exe", "/path/to/file.exe", "1.2.3-staging")
+}
diff --git a/base/updater/get.go b/base/updater/get.go
new file mode 100644
index 00000000..d50d28b3
--- /dev/null
+++ b/base/updater/get.go
@@ -0,0 +1,91 @@
+package updater
+
+import (
+	"context"
+	"errors"
+	"fmt"
+	"net/http"
+
+	"github.com/safing/portmaster/base/log"
+)
+
+// Errors returned by the updater package.
+var (
+	ErrNotFound                  = errors.New("the requested file could not be found")
+	ErrNotAvailableLocally       = errors.New("the requested file is not available locally")
+	ErrVerificationNotConfigured = errors.New("verification not configured for this resource")
+)
+
+// GetFile returns the selected (mostly newest) file with the given
+// identifier or an error, if it fails.
+func (reg *ResourceRegistry) GetFile(identifier string) (*File, error) {
+	reg.RLock()
+	res, ok := reg.resources[identifier]
+	reg.RUnlock()
+	if !ok {
+		return nil, ErrNotFound
+	}
+
+	file := res.GetFile()
+	// check if file is available locally
+	if file.version.Available {
+		file.markActiveWithLocking()
+
+		// Verify file, if configured.
+		_, err := file.Verify()
+		if err != nil && !errors.Is(err, ErrVerificationNotConfigured) {
+			// TODO: If verification is required, try deleting the resource and downloading it again.
+			return nil, fmt.Errorf("failed to verify file: %w", err)
+		}
+
+		return file, nil
+	}
+
+	// check if online
+	if !reg.Online {
+		return nil, ErrNotAvailableLocally
+	}
+
+	// check download dir
+	err := reg.tmpDir.Ensure()
+	if err != nil {
+		return nil, fmt.Errorf("could not prepare tmp directory for download: %w", err)
+	}
+
+	// Start registry operation.
+	reg.state.StartOperation(StateFetching)
+	defer reg.state.EndOperation()
+
+	// download file
+	log.Tracef("%s: starting download of %s", reg.Name, file.versionedPath)
+	client := &http.Client{}
+	for tries := range 5 {
+		err = reg.fetchFile(context.TODO(), client, file.version, tries)
+		if err != nil {
+			log.Tracef("%s: failed to download %s: %s, retrying (%d)", reg.Name, file.versionedPath, err, tries+1)
+		} else {
+			file.markActiveWithLocking()
+
+			// TODO: We just download the file - should we verify it again?
+			return file, nil
+		}
+	}
+	log.Warningf("%s: failed to download %s: %s", reg.Name, file.versionedPath, err)
+	return nil, err
+}
+
+// GetVersion returns the selected version of the given identifier.
+// The returned resource version may not be modified.
+func (reg *ResourceRegistry) GetVersion(identifier string) (*ResourceVersion, error) {
+	reg.RLock()
+	res, ok := reg.resources[identifier]
+	reg.RUnlock()
+	if !ok {
+		return nil, ErrNotFound
+	}
+
+	res.Lock()
+	defer res.Unlock()
+
+	return res.SelectedVersion, nil
+}
diff --git a/base/updater/indexes.go b/base/updater/indexes.go
new file mode 100644
index 00000000..81a373a3
--- /dev/null
+++ b/base/updater/indexes.go
@@ -0,0 +1,109 @@
+package updater
+
+import (
+	"encoding/json"
+	"errors"
+	"fmt"
+	"time"
+)
+
+const (
+	baseIndexExtension = ".json"
+	v2IndexExtension   = ".v2.json"
+)
+
+// Index describes an index file pulled by the updater.
+type Index struct {
+	// Path is the path to the index file
+	// on the update server.
+	Path string
+
+	// Channel holds the release channel name of the index.
+	// It must match the filename without extension.
+	Channel string
+
+	// PreRelease signifies that all versions of this index should be marked as
+	// pre-releases, no matter if the versions actually have a pre-release tag or
+	// not.
+	PreRelease bool
+
+	// AutoDownload specifies whether new versions should be automatically downloaded.
+	AutoDownload bool
+
+	// LastRelease holds the time of the last seen release of this index.
+	LastRelease time.Time
+}
+
+// IndexFile represents an index file.
+type IndexFile struct {
+	Channel   string
+	Published time.Time
+
+	Releases map[string]string
+}
+
+var (
+	// ErrIndexChecksumMismatch is returned when an index does not match its
+	// signed checksum.
+	ErrIndexChecksumMismatch = errors.New("index checksum does mot match signature")
+
+	// ErrIndexFromFuture is returned when an index is parsed with a
+	// Published timestamp that lies in the future.
+	ErrIndexFromFuture = errors.New("index is from the future")
+
+	// ErrIndexIsOlder is returned when an index is parsed with an older
+	// Published timestamp than the current Published timestamp.
+	ErrIndexIsOlder = errors.New("index is older than the current one")
+
+	// ErrIndexChannelMismatch is returned when an index is parsed with a
+	// different channel that the expected one.
+	ErrIndexChannelMismatch = errors.New("index does not match the expected channel")
+)
+
+// ParseIndexFile parses an index file and checks if it is valid.
+func ParseIndexFile(indexData []byte, channel string, lastIndexRelease time.Time) (*IndexFile, error) {
+	// Load into struct.
+	indexFile := &IndexFile{}
+	err := json.Unmarshal(indexData, indexFile)
+	if err != nil {
+		return nil, fmt.Errorf("failed to parse signed index data: %w", err)
+	}
+
+	// Fallback to old format if there are no releases and no channel is defined.
+	// TODO: Remove in v1
+	if len(indexFile.Releases) == 0 && indexFile.Channel == "" {
+		return loadOldIndexFormat(indexData, channel)
+	}
+
+	// Check the index metadata.
+	switch {
+	case !indexFile.Published.IsZero() && time.Now().Before(indexFile.Published):
+		return indexFile, ErrIndexFromFuture
+
+	case !indexFile.Published.IsZero() &&
+		!lastIndexRelease.IsZero() &&
+		lastIndexRelease.After(indexFile.Published):
+		return indexFile, ErrIndexIsOlder
+
+	case channel != "" &&
+		indexFile.Channel != "" &&
+		channel != indexFile.Channel:
+		return indexFile, ErrIndexChannelMismatch
+	}
+
+	return indexFile, nil
+}
+
+func loadOldIndexFormat(indexData []byte, channel string) (*IndexFile, error) {
+	releases := make(map[string]string)
+	err := json.Unmarshal(indexData, &releases)
+	if err != nil {
+		return nil, err
+	}
+
+	return &IndexFile{
+		Channel: channel,
+		// Do NOT define `Published`, as this would break the "is newer" check.
+		Releases: releases,
+	}, nil
+}
diff --git a/base/updater/indexes_test.go b/base/updater/indexes_test.go
new file mode 100644
index 00000000..a85046cd
--- /dev/null
+++ b/base/updater/indexes_test.go
@@ -0,0 +1,57 @@
+package updater
+
+import (
+	"testing"
+	"time"
+
+	"github.com/stretchr/testify/assert"
+)
+
+var (
+	oldFormat = `{
+	"all/ui/modules/assets.zip": "0.3.0",
+	"all/ui/modules/portmaster.zip": "0.2.4",
+	"linux_amd64/core/portmaster-core": "0.8.13"
+}`
+
+	newFormat = `{
+	"Channel": "stable",
+	"Published": "2022-01-02T00:00:00Z",
+	"Releases": {
+		"all/ui/modules/assets.zip": "0.3.0",
+		"all/ui/modules/portmaster.zip": "0.2.4",
+		"linux_amd64/core/portmaster-core": "0.8.13"
+	}
+}`
+
+	formatTestChannel  = "stable"
+	formatTestReleases = map[string]string{
+		"all/ui/modules/assets.zip":        "0.3.0",
+		"all/ui/modules/portmaster.zip":    "0.2.4",
+		"linux_amd64/core/portmaster-core": "0.8.13",
+	}
+)
+
+func TestIndexParsing(t *testing.T) {
+	t.Parallel()
+
+	lastRelease, err := time.Parse(time.RFC3339, "2022-01-01T00:00:00Z")
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	oldIndexFile, err := ParseIndexFile([]byte(oldFormat), formatTestChannel, lastRelease)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	newIndexFile, err := ParseIndexFile([]byte(newFormat), formatTestChannel, lastRelease)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	assert.Equal(t, formatTestChannel, oldIndexFile.Channel, "channel should be the same")
+	assert.Equal(t, formatTestChannel, newIndexFile.Channel, "channel should be the same")
+	assert.Equal(t, formatTestReleases, oldIndexFile.Releases, "releases should be the same")
+	assert.Equal(t, formatTestReleases, newIndexFile.Releases, "releases should be the same")
+}
diff --git a/base/updater/notifier.go b/base/updater/notifier.go
new file mode 100644
index 00000000..66b2832d
--- /dev/null
+++ b/base/updater/notifier.go
@@ -0,0 +1,33 @@
+package updater
+
+import (
+	"github.com/tevino/abool"
+)
+
+type notifier struct {
+	upgradeAvailable *abool.AtomicBool
+	notifyChannel    chan struct{}
+}
+
+func newNotifier() *notifier {
+	return &notifier{
+		upgradeAvailable: abool.NewBool(false),
+		notifyChannel:    make(chan struct{}),
+	}
+}
+
+func (n *notifier) markAsUpgradeable() {
+	if n.upgradeAvailable.SetToIf(false, true) {
+		close(n.notifyChannel)
+	}
+}
+
+// UpgradeAvailable returns whether an upgrade is available for this file.
+func (file *File) UpgradeAvailable() bool {
+	return file.notifier.upgradeAvailable.IsSet()
+}
+
+// WaitForAvailableUpgrade blocks (selectable) until an upgrade for this file is available.
+func (file *File) WaitForAvailableUpgrade() <-chan struct{} {
+	return file.notifier.notifyChannel
+}
diff --git a/base/updater/registry.go b/base/updater/registry.go
new file mode 100644
index 00000000..8deda74e
--- /dev/null
+++ b/base/updater/registry.go
@@ -0,0 +1,270 @@
+package updater
+
+import (
+	"errors"
+	"fmt"
+	"os"
+	"path/filepath"
+	"runtime"
+	"strings"
+	"sync"
+
+	"github.com/safing/portmaster/base/log"
+	"github.com/safing/portmaster/base/utils"
+)
+
+const (
+	onWindows = runtime.GOOS == "windows"
+)
+
+// ResourceRegistry is a registry for managing update resources.
+type ResourceRegistry struct {
+	sync.RWMutex
+
+	Name       string
+	storageDir *utils.DirStructure
+	tmpDir     *utils.DirStructure
+	indexes    []*Index
+	state      *RegistryState
+
+	resources        map[string]*Resource
+	UpdateURLs       []string
+	UserAgent        string
+	MandatoryUpdates []string
+	AutoUnpack       []string
+
+	// Verification holds a map of VerificationOptions assigned to their
+	// applicable identifier path prefix.
+	// Use an empty string to denote the default.
+	// Use empty options to disable verification for a path prefix.
+	Verification map[string]*VerificationOptions
+
+	// UsePreReleases signifies that pre-releases should be used when selecting a
+	// version. Even if false, a pre-release version will still be used if it is
+	// defined as the current version by an index.
+	UsePreReleases bool
+
+	// DevMode specifies if a local 0.0.0 version should be always chosen, when available.
+	DevMode bool
+
+	// Online specifies if resources may be downloaded if not available locally.
+	Online bool
+
+	// StateNotifyFunc may be set to receive any changes to the registry state.
+	// The specified function may lock the state, but may not block or take a
+	// lot of time.
+	StateNotifyFunc func(*RegistryState)
+}
+
+// AddIndex adds a new index to the resource registry.
+// The order is important, as indexes added later will override the current
+// release from earlier indexes.
+func (reg *ResourceRegistry) AddIndex(idx Index) {
+	reg.Lock()
+	defer reg.Unlock()
+
+	// Get channel name from path.
+	idx.Channel = strings.TrimSuffix(
+		filepath.Base(idx.Path), filepath.Ext(idx.Path),
+	)
+
+	reg.indexes = append(reg.indexes, &idx)
+}
+
+// PreInitUpdateState sets the initial update state of the registry before initialization.
+func (reg *ResourceRegistry) PreInitUpdateState(s UpdateState) error {
+	if reg.state != nil {
+		return errors.New("registry already initialized")
+	}
+
+	reg.state = &RegistryState{
+		Updates: s,
+	}
+	return nil
+}
+
+// Initialize initializes a raw registry struct and makes it ready for usage.
+func (reg *ResourceRegistry) Initialize(storageDir *utils.DirStructure) error {
+	// check if storage dir is available
+	err := storageDir.Ensure()
+	if err != nil {
+		return err
+	}
+
+	// set default name
+	if reg.Name == "" {
+		reg.Name = "updater"
+	}
+
+	// initialize private attributes
+	reg.storageDir = storageDir
+	reg.tmpDir = storageDir.ChildDir("tmp", 0o0700)
+	reg.resources = make(map[string]*Resource)
+	if reg.state == nil {
+		reg.state = &RegistryState{}
+	}
+	reg.state.ID = StateReady
+	reg.state.reg = reg
+
+	// remove tmp dir to delete old entries
+	err = reg.Cleanup()
+	if err != nil {
+		log.Warningf("%s: failed to remove tmp dir: %s", reg.Name, err)
+	}
+
+	// (re-)create tmp dir
+	err = reg.tmpDir.Ensure()
+	if err != nil {
+		log.Warningf("%s: failed to create tmp dir: %s", reg.Name, err)
+	}
+
+	// Check verification options.
+	if reg.Verification != nil {
+		for prefix, opts := range reg.Verification {
+			// Check if verification is disable for this prefix.
+			if opts == nil {
+				continue
+			}
+
+			// If enabled, a trust store is required.
+			if opts.TrustStore == nil {
+				return fmt.Errorf("verification enabled for prefix %q, but no trust store configured", prefix)
+			}
+
+			// DownloadPolicy must be equal or stricter than DiskLoadPolicy.
+			if opts.DiskLoadPolicy < opts.DownloadPolicy {
+				return errors.New("verification download policy must be equal or stricter than the disk load policy")
+			}
+
+			// Warn if all policies are disabled.
+			if opts.DownloadPolicy == SignaturePolicyDisable &&
+				opts.DiskLoadPolicy == SignaturePolicyDisable {
+				log.Warningf("%s: verification enabled for prefix %q, but all policies set to disable", reg.Name, prefix)
+			}
+		}
+	}
+
+	return nil
+}
+
+// StorageDir returns the main storage dir of the resource registry.
+func (reg *ResourceRegistry) StorageDir() *utils.DirStructure {
+	return reg.storageDir
+}
+
+// TmpDir returns the temporary working dir of the resource registry.
+func (reg *ResourceRegistry) TmpDir() *utils.DirStructure {
+	return reg.tmpDir
+}
+
+// SetDevMode sets the development mode flag.
+func (reg *ResourceRegistry) SetDevMode(on bool) {
+	reg.Lock()
+	defer reg.Unlock()
+
+	reg.DevMode = on
+}
+
+// SetUsePreReleases sets the UsePreReleases flag.
+func (reg *ResourceRegistry) SetUsePreReleases(yes bool) {
+	reg.Lock()
+	defer reg.Unlock()
+
+	reg.UsePreReleases = yes
+}
+
+// AddResource adds a resource to the registry. Does _not_ select new version.
+func (reg *ResourceRegistry) AddResource(identifier, version string, index *Index, available, currentRelease, preRelease bool) error {
+	reg.Lock()
+	defer reg.Unlock()
+
+	err := reg.addResource(identifier, version, index, available, currentRelease, preRelease)
+	return err
+}
+
+func (reg *ResourceRegistry) addResource(identifier, version string, index *Index, available, currentRelease, preRelease bool) error {
+	res, ok := reg.resources[identifier]
+	if !ok {
+		res = reg.newResource(identifier)
+		reg.resources[identifier] = res
+	}
+	res.Index = index
+
+	return res.AddVersion(version, available, currentRelease, preRelease)
+}
+
+// AddResources adds resources to the registry. Errors are logged, the last one is returned. Despite errors, non-failing resources are still added. Does _not_ select new versions.
+func (reg *ResourceRegistry) AddResources(versions map[string]string, index *Index, available, currentRelease, preRelease bool) error {
+	reg.Lock()
+	defer reg.Unlock()
+
+	// add versions and their flags to registry
+	var lastError error
+	for identifier, version := range versions {
+		lastError = reg.addResource(identifier, version, index, available, currentRelease, preRelease)
+		if lastError != nil {
+			log.Warningf("%s: failed to add resource %s: %s", reg.Name, identifier, lastError)
+		}
+	}
+
+	return lastError
+}
+
+// SelectVersions selects new resource versions depending on the current registry state.
+func (reg *ResourceRegistry) SelectVersions() {
+	reg.RLock()
+	defer reg.RUnlock()
+
+	for _, res := range reg.resources {
+		res.Lock()
+		res.selectVersion()
+		res.Unlock()
+	}
+}
+
+// GetSelectedVersions returns a list of the currently selected versions.
+func (reg *ResourceRegistry) GetSelectedVersions() (versions map[string]string) {
+	reg.RLock()
+	defer reg.RUnlock()
+
+	for _, res := range reg.resources {
+		res.Lock()
+		versions[res.Identifier] = res.SelectedVersion.VersionNumber
+		res.Unlock()
+	}
+
+	return
+}
+
+// Purge deletes old updates, retaining a certain amount, specified by the keep
+// parameter. Will at least keep 2 updates per resource.
+func (reg *ResourceRegistry) Purge(keep int) {
+	reg.RLock()
+	defer reg.RUnlock()
+
+	for _, res := range reg.resources {
+		res.Purge(keep)
+	}
+}
+
+// ResetResources removes all resources from the registry.
+func (reg *ResourceRegistry) ResetResources() {
+	reg.Lock()
+	defer reg.Unlock()
+
+	reg.resources = make(map[string]*Resource)
+}
+
+// ResetIndexes removes all indexes from the registry.
+func (reg *ResourceRegistry) ResetIndexes() {
+	reg.Lock()
+	defer reg.Unlock()
+
+	reg.indexes = make([]*Index, 0, len(reg.indexes))
+}
+
+// Cleanup removes temporary files.
+func (reg *ResourceRegistry) Cleanup() error {
+	// delete download tmp dir
+	return os.RemoveAll(reg.tmpDir.Path)
+}
diff --git a/base/updater/registry_test.go b/base/updater/registry_test.go
new file mode 100644
index 00000000..a8978f68
--- /dev/null
+++ b/base/updater/registry_test.go
@@ -0,0 +1,35 @@
+package updater
+
+import (
+	"os"
+	"testing"
+
+	"github.com/safing/portmaster/base/utils"
+)
+
+var registry *ResourceRegistry
+
+func TestMain(m *testing.M) {
+	// setup
+	tmpDir, err := os.MkdirTemp("", "ci-portmaster-")
+	if err != nil {
+		panic(err)
+	}
+	registry = &ResourceRegistry{
+		UsePreReleases: true,
+		DevMode:        true,
+		Online:         true,
+	}
+	err = registry.Initialize(utils.NewDirStructure(tmpDir, 0o0777))
+	if err != nil {
+		panic(err)
+	}
+
+	// run
+	// call flag.Parse() here if TestMain uses flags
+	ret := m.Run()
+
+	// teardown
+	_ = os.RemoveAll(tmpDir)
+	os.Exit(ret)
+}
diff --git a/base/updater/resource.go b/base/updater/resource.go
new file mode 100644
index 00000000..325f70cc
--- /dev/null
+++ b/base/updater/resource.go
@@ -0,0 +1,582 @@
+package updater
+
+import (
+	"errors"
+	"io/fs"
+	"os"
+	"path/filepath"
+	"sort"
+	"strings"
+	"sync"
+
+	semver "github.com/hashicorp/go-version"
+
+	"github.com/safing/jess/filesig"
+	"github.com/safing/portmaster/base/log"
+	"github.com/safing/portmaster/base/utils"
+)
+
+var devVersion *semver.Version
+
+func init() {
+	var err error
+	devVersion, err = semver.NewVersion("0")
+	if err != nil {
+		panic(err)
+	}
+}
+
+// Resource represents a resource (via an identifier) and multiple file versions.
+type Resource struct {
+	sync.Mutex
+	registry *ResourceRegistry
+	notifier *notifier
+
+	// Identifier is the unique identifier for that resource.
+	// It forms a file path using a forward-slash as the
+	// path separator.
+	Identifier string
+
+	// Versions holds all available resource versions.
+	Versions []*ResourceVersion
+
+	// ActiveVersion is the last version of the resource
+	// that someone requested using GetFile().
+	ActiveVersion *ResourceVersion
+
+	// SelectedVersion is newest, selectable version of
+	// that resource that is available. A version
+	// is selectable if it's not blacklisted by the user.
+	// Note that it's not guaranteed that the selected version
+	// is available locally. In that case, GetFile will attempt
+	// to download the latest version from the updates servers
+	// specified in the resource registry.
+	SelectedVersion *ResourceVersion
+
+	// VerificationOptions holds the verification options for this resource.
+	VerificationOptions *VerificationOptions
+
+	// Index holds a reference to the index this resource was last defined in.
+	// Will be nil if resource was only found on disk.
+	Index *Index
+}
+
+// ResourceVersion represents a single version of a resource.
+type ResourceVersion struct {
+	resource *Resource
+
+	// VersionNumber is the string representation of the resource
+	// version.
+	VersionNumber string
+	semVer        *semver.Version
+
+	// Available indicates if this version is available locally.
+	Available bool
+
+	// SigAvailable indicates if the signature of this version is available locally.
+	SigAvailable bool
+
+	// CurrentRelease indicates that this is the current release that should be
+	// selected, if possible.
+	CurrentRelease bool
+
+	// PreRelease indicates that this version is pre-release.
+	PreRelease bool
+
+	// Blacklisted may be set to true if this version should
+	// be skipped and not used. This is useful if the version
+	// is known to be broken.
+	Blacklisted bool
+}
+
+func (rv *ResourceVersion) String() string {
+	return rv.VersionNumber
+}
+
+// SemVer returns the semantic version of the resource.
+func (rv *ResourceVersion) SemVer() *semver.Version {
+	return rv.semVer
+}
+
+// EqualsVersion normalizes the given version and checks equality with semver.
+func (rv *ResourceVersion) EqualsVersion(version string) bool {
+	cmpSemVer, err := semver.NewVersion(version)
+	if err != nil {
+		return false
+	}
+
+	return rv.semVer.Equal(cmpSemVer)
+}
+
+// isSelectable returns true if the version represented by rv is selectable.
+// A version is selectable if it's not blacklisted and either already locally
+// available or ready to be downloaded.
+func (rv *ResourceVersion) isSelectable() bool {
+	switch {
+	case rv.Blacklisted:
+		// Should not be used.
+		return false
+	case rv.Available:
+		// Is available locally, use!
+		return true
+	case !rv.resource.registry.Online:
+		// Cannot download, because registry is set to offline.
+		return false
+	case rv.resource.Index == nil:
+		// Cannot download, because resource is not part of an index.
+		return false
+	case !rv.resource.Index.AutoDownload:
+		// Cannot download, because index may not automatically download.
+		return false
+	default:
+		// Is not available locally, but we are allowed to download it on request!
+		return true
+	}
+}
+
+// isBetaVersionNumber checks if rv is marked as a beta version by checking
+// the version string. It does not honor the BetaRelease field of rv!
+func (rv *ResourceVersion) isBetaVersionNumber() bool { //nolint:unused
+	// "b" suffix check if for backwards compatibility
+	// new versions should use the pre-release suffix as
+	// declared by https://semver.org
+	// i.e. 1.2.3-beta
+	switch rv.semVer.Prerelease() {
+	case "b", "beta":
+		return true
+	default:
+		return false
+	}
+}
+
+// Export makes a copy of the resource with only the exposed information.
+// Attributes are copied and safe to access.
+// Any ResourceVersion must not be modified.
+func (res *Resource) Export() *Resource {
+	res.Lock()
+	defer res.Unlock()
+
+	// Copy attibutes.
+	export := &Resource{
+		Identifier:      res.Identifier,
+		Versions:        make([]*ResourceVersion, len(res.Versions)),
+		ActiveVersion:   res.ActiveVersion,
+		SelectedVersion: res.SelectedVersion,
+	}
+	// Copy Versions slice.
+	copy(export.Versions, res.Versions)
+
+	return export
+}
+
+// Len is the number of elements in the collection.
+// It implements sort.Interface for ResourceVersion.
+func (res *Resource) Len() int {
+	return len(res.Versions)
+}
+
+// Less reports whether the element with index i should
+// sort before the element with index j.
+// It implements sort.Interface for ResourceVersions.
+func (res *Resource) Less(i, j int) bool {
+	return res.Versions[i].semVer.GreaterThan(res.Versions[j].semVer)
+}
+
+// Swap swaps the elements with indexes i and j.
+// It implements sort.Interface for ResourceVersions.
+func (res *Resource) Swap(i, j int) {
+	res.Versions[i], res.Versions[j] = res.Versions[j], res.Versions[i]
+}
+
+// available returns whether any version of the resource is available.
+func (res *Resource) available() bool {
+	for _, rv := range res.Versions {
+		if rv.Available {
+			return true
+		}
+	}
+	return false
+}
+
+// inUse returns true if the resource is currently in use.
+func (res *Resource) inUse() bool {
+	return res.ActiveVersion != nil
+}
+
+// AnyVersionAvailable returns true if any version of
+// res is locally available.
+func (res *Resource) AnyVersionAvailable() bool {
+	res.Lock()
+	defer res.Unlock()
+
+	return res.available()
+}
+
+func (reg *ResourceRegistry) newResource(identifier string) *Resource {
+	return &Resource{
+		registry:            reg,
+		Identifier:          identifier,
+		Versions:            make([]*ResourceVersion, 0, 1),
+		VerificationOptions: reg.GetVerificationOptions(identifier),
+	}
+}
+
+// AddVersion adds a resource version to a resource.
+func (res *Resource) AddVersion(version string, available, currentRelease, preRelease bool) error {
+	res.Lock()
+	defer res.Unlock()
+
+	// reset current release flags
+	if currentRelease {
+		for _, rv := range res.Versions {
+			rv.CurrentRelease = false
+		}
+	}
+
+	var rv *ResourceVersion
+	// check for existing version
+	for _, possibleMatch := range res.Versions {
+		if possibleMatch.VersionNumber == version {
+			rv = possibleMatch
+			break
+		}
+	}
+
+	// create new version if none found
+	if rv == nil {
+		// parse to semver
+		sv, err := semver.NewVersion(version)
+		if err != nil {
+			return err
+		}
+
+		rv = &ResourceVersion{
+			resource:      res,
+			VersionNumber: sv.String(), // Use normalized version.
+			semVer:        sv,
+		}
+		res.Versions = append(res.Versions, rv)
+	}
+
+	// set flags
+	if available {
+		rv.Available = true
+
+		// If available and signatures are enabled for this resource, check if the
+		// signature is available.
+		if res.VerificationOptions != nil && utils.PathExists(rv.storageSigPath()) {
+			rv.SigAvailable = true
+		}
+	}
+	if currentRelease {
+		rv.CurrentRelease = true
+	}
+	if preRelease || rv.semVer.Prerelease() != "" {
+		rv.PreRelease = true
+	}
+
+	return nil
+}
+
+// GetFile returns the selected version as a *File.
+func (res *Resource) GetFile() *File {
+	res.Lock()
+	defer res.Unlock()
+
+	// check for notifier
+	if res.notifier == nil {
+		// create new notifier
+		res.notifier = newNotifier()
+	}
+
+	// check if version is selected
+	if res.SelectedVersion == nil {
+		res.selectVersion()
+	}
+
+	// create file
+	return &File{
+		resource:      res,
+		version:       res.SelectedVersion,
+		notifier:      res.notifier,
+		versionedPath: res.SelectedVersion.versionedPath(),
+		storagePath:   res.SelectedVersion.storagePath(),
+	}
+}
+
+//nolint:gocognit // function already kept as simple as possible
+func (res *Resource) selectVersion() {
+	sort.Sort(res)
+
+	// export after we finish
+	var fallback bool
+	defer func() {
+		if fallback {
+			log.Tracef("updater: selected version %s (as fallback) for resource %s", res.SelectedVersion, res.Identifier)
+		} else {
+			log.Debugf("updater: selected version %s for resource %s", res.SelectedVersion, res.Identifier)
+		}
+
+		if res.inUse() &&
+			res.SelectedVersion != res.ActiveVersion && // new selected version does not match previously selected version
+			res.notifier != nil {
+
+			res.notifier.markAsUpgradeable()
+			res.notifier = nil
+
+			log.Debugf("updater: active version of %s is %s, update available", res.Identifier, res.ActiveVersion.VersionNumber)
+		}
+	}()
+
+	if len(res.Versions) == 0 {
+		// TODO: find better way to deal with an empty version slice (which should not happen)
+		res.SelectedVersion = nil
+		return
+	}
+
+	// Target selection
+
+	// 1) Dev release if dev mode is active and ignore blacklisting
+	if res.registry.DevMode {
+		// Get last version, as this will be v0.0.0, if available.
+		rv := res.Versions[len(res.Versions)-1]
+		// Check if it's v0.0.0.
+		if rv.semVer.Equal(devVersion) && rv.Available {
+			res.SelectedVersion = rv
+			return
+		}
+	}
+
+	// 2) Find the current release. This may be also be a pre-release.
+	for _, rv := range res.Versions {
+		if rv.CurrentRelease {
+			if rv.isSelectable() {
+				res.SelectedVersion = rv
+				return
+			}
+			// There can only be once current release,
+			// so we can abort after finding one.
+			break
+		}
+	}
+
+	// 3) If UsePreReleases is set, find any newest version.
+	if res.registry.UsePreReleases {
+		for _, rv := range res.Versions {
+			if rv.isSelectable() {
+				res.SelectedVersion = rv
+				return
+			}
+		}
+	}
+
+	// 4) Find the newest stable version.
+	for _, rv := range res.Versions {
+		if !rv.PreRelease && rv.isSelectable() {
+			res.SelectedVersion = rv
+			return
+		}
+	}
+
+	// 5) Default to newest.
+	res.SelectedVersion = res.Versions[0]
+	fallback = true
+}
+
+// Blacklist blacklists the specified version and selects a new version.
+func (res *Resource) Blacklist(version string) error {
+	res.Lock()
+	defer res.Unlock()
+
+	// count available and valid versions
+	valid := 0
+	for _, rv := range res.Versions {
+		if rv.semVer.Equal(devVersion) {
+			continue // ignore dev versions
+		}
+		if !rv.Blacklisted {
+			valid++
+		}
+	}
+	if valid <= 1 {
+		return errors.New("cannot blacklist last version") // last one, cannot blacklist!
+	}
+
+	// find version and blacklist
+	for _, rv := range res.Versions {
+		if rv.VersionNumber == version {
+			// blacklist and update
+			rv.Blacklisted = true
+			res.selectVersion()
+			return nil
+		}
+	}
+
+	return errors.New("could not find version")
+}
+
+// Purge deletes old updates, retaining a certain amount, specified by
+// the keep parameter. Purge will always keep at least 2 versions so
+// specifying a smaller keep value will have no effect.
+func (res *Resource) Purge(keepExtra int) { //nolint:gocognit
+	res.Lock()
+	defer res.Unlock()
+
+	// If there is any blacklisted version within the resource, pause purging.
+	// In this case we may need extra available versions beyond what would be
+	// available after purging.
+	for _, rv := range res.Versions {
+		if rv.Blacklisted {
+			log.Debugf(
+				"%s: pausing purging of resource %s, as it contains blacklisted items",
+				res.registry.Name,
+				rv.resource.Identifier,
+			)
+			return
+		}
+	}
+
+	// Safeguard the amount of extra version to keep.
+	if keepExtra < 2 {
+		keepExtra = 2
+	}
+
+	// Search for purge boundary.
+	var purgeBoundary int
+	var skippedActiveVersion bool
+	var skippedSelectedVersion bool
+	var skippedStableVersion bool
+boundarySearch:
+	for i, rv := range res.Versions {
+		// Check if required versions are already skipped.
+		switch {
+		case !skippedActiveVersion && res.ActiveVersion != nil:
+			// Skip versions until the active version, if it's set.
+		case !skippedSelectedVersion && res.SelectedVersion != nil:
+			// Skip versions until the selected version, if it's set.
+		case !skippedStableVersion:
+			// Skip versions until the stable version.
+		default:
+			// All required version skipped, set purge boundary.
+			purgeBoundary = i + keepExtra
+			break boundarySearch
+		}
+
+		// Check if current instance is a required version.
+		if rv == res.ActiveVersion {
+			skippedActiveVersion = true
+		}
+		if rv == res.SelectedVersion {
+			skippedSelectedVersion = true
+		}
+		if !rv.PreRelease {
+			skippedStableVersion = true
+		}
+	}
+
+	// Check if there is anything to purge at all.
+	if purgeBoundary <= keepExtra || purgeBoundary >= len(res.Versions) {
+		return
+	}
+
+	// Purge everything beyond the purge boundary.
+	for _, rv := range res.Versions[purgeBoundary:] {
+		// Only remove if resource file is actually available.
+		if !rv.Available {
+			continue
+		}
+
+		// Remove resource file.
+		storagePath := rv.storagePath()
+		err := os.Remove(storagePath)
+		if err != nil {
+			if !errors.Is(err, fs.ErrNotExist) {
+				log.Warningf("%s: failed to purge resource %s v%s: %s", res.registry.Name, rv.resource.Identifier, rv.VersionNumber, err)
+			}
+		} else {
+			log.Tracef("%s: purged resource %s v%s", res.registry.Name, rv.resource.Identifier, rv.VersionNumber)
+		}
+
+		// Remove resource signature file.
+		err = os.Remove(rv.storageSigPath())
+		if err != nil {
+			if !errors.Is(err, fs.ErrNotExist) {
+				log.Warningf("%s: failed to purge resource signature %s v%s: %s", res.registry.Name, rv.resource.Identifier, rv.VersionNumber, err)
+			}
+		} else {
+			log.Tracef("%s: purged resource signature %s v%s", res.registry.Name, rv.resource.Identifier, rv.VersionNumber)
+		}
+
+		// Remove unpacked version of resource.
+		ext := filepath.Ext(storagePath)
+		if ext == "" {
+			// Nothing to do if file does not have an extension.
+			continue
+		}
+		unpackedPath := strings.TrimSuffix(storagePath, ext)
+
+		// Remove if it exists, or an error occurs on access.
+		_, err = os.Stat(unpackedPath)
+		if err == nil || !errors.Is(err, fs.ErrNotExist) {
+			err = os.Remove(unpackedPath)
+			if err != nil {
+				log.Warningf("%s: failed to purge unpacked resource %s v%s: %s", res.registry.Name, rv.resource.Identifier, rv.VersionNumber, err)
+			} else {
+				log.Tracef("%s: purged unpacked resource %s v%s", res.registry.Name, rv.resource.Identifier, rv.VersionNumber)
+			}
+		}
+	}
+
+	// remove entries of deleted files
+	res.Versions = res.Versions[purgeBoundary:]
+}
+
+// SigningMetadata returns the metadata to be included in signatures.
+func (rv *ResourceVersion) SigningMetadata() map[string]string {
+	return map[string]string{
+		"id":      rv.resource.Identifier,
+		"version": rv.VersionNumber,
+	}
+}
+
+// GetFile returns the version as a *File.
+// It locks the resource for doing so.
+func (rv *ResourceVersion) GetFile() *File {
+	rv.resource.Lock()
+	defer rv.resource.Unlock()
+
+	// check for notifier
+	if rv.resource.notifier == nil {
+		// create new notifier
+		rv.resource.notifier = newNotifier()
+	}
+
+	// create file
+	return &File{
+		resource:      rv.resource,
+		version:       rv,
+		notifier:      rv.resource.notifier,
+		versionedPath: rv.versionedPath(),
+		storagePath:   rv.storagePath(),
+	}
+}
+
+// versionedPath returns the versioned identifier.
+func (rv *ResourceVersion) versionedPath() string {
+	return GetVersionedPath(rv.resource.Identifier, rv.VersionNumber)
+}
+
+// versionedSigPath returns the versioned identifier of the file signature.
+func (rv *ResourceVersion) versionedSigPath() string {
+	return GetVersionedPath(rv.resource.Identifier, rv.VersionNumber) + filesig.Extension
+}
+
+// storagePath returns the absolute storage path.
+func (rv *ResourceVersion) storagePath() string {
+	return filepath.Join(rv.resource.registry.storageDir.Path, filepath.FromSlash(rv.versionedPath()))
+}
+
+// storageSigPath returns the absolute storage path of the file signature.
+func (rv *ResourceVersion) storageSigPath() string {
+	return rv.storagePath() + filesig.Extension
+}
diff --git a/base/updater/resource_test.go b/base/updater/resource_test.go
new file mode 100644
index 00000000..ceb51e9f
--- /dev/null
+++ b/base/updater/resource_test.go
@@ -0,0 +1,119 @@
+package updater
+
+import (
+	"fmt"
+	"testing"
+
+	semver "github.com/hashicorp/go-version"
+	"github.com/stretchr/testify/assert"
+)
+
+func TestVersionSelection(t *testing.T) {
+	t.Parallel()
+
+	res := registry.newResource("test/a")
+
+	err := res.AddVersion("1.2.2", true, false, false)
+	if err != nil {
+		t.Fatal(err)
+	}
+	err = res.AddVersion("1.2.3", true, false, false)
+	if err != nil {
+		t.Fatal(err)
+	}
+	err = res.AddVersion("1.2.4-beta", true, false, false)
+	if err != nil {
+		t.Fatal(err)
+	}
+	err = res.AddVersion("1.2.4-staging", true, false, false)
+	if err != nil {
+		t.Fatal(err)
+	}
+	err = res.AddVersion("1.2.5", false, false, false)
+	if err != nil {
+		t.Fatal(err)
+	}
+	err = res.AddVersion("1.2.6-beta", false, false, false)
+	if err != nil {
+		t.Fatal(err)
+	}
+	err = res.AddVersion("0", true, false, false)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	registry.UsePreReleases = true
+	registry.DevMode = true
+	registry.Online = true
+	res.Index = &Index{AutoDownload: true}
+
+	res.selectVersion()
+	if res.SelectedVersion.VersionNumber != "0.0.0" {
+		t.Errorf("selected version should be 0.0.0, not %s", res.SelectedVersion.VersionNumber)
+	}
+
+	registry.DevMode = false
+	res.selectVersion()
+	if res.SelectedVersion.VersionNumber != "1.2.6-beta" {
+		t.Errorf("selected version should be 1.2.6-beta, not %s", res.SelectedVersion.VersionNumber)
+	}
+
+	registry.UsePreReleases = false
+	res.selectVersion()
+	if res.SelectedVersion.VersionNumber != "1.2.5" {
+		t.Errorf("selected version should be 1.2.5, not %s", res.SelectedVersion.VersionNumber)
+	}
+
+	registry.Online = false
+	res.selectVersion()
+	if res.SelectedVersion.VersionNumber != "1.2.3" {
+		t.Errorf("selected version should be 1.2.3, not %s", res.SelectedVersion.VersionNumber)
+	}
+
+	f123 := res.GetFile()
+	f123.markActiveWithLocking()
+
+	err = res.Blacklist("1.2.3")
+	if err != nil {
+		t.Fatal(err)
+	}
+	if res.SelectedVersion.VersionNumber != "1.2.2" {
+		t.Errorf("selected version should be 1.2.2, not %s", res.SelectedVersion.VersionNumber)
+	}
+
+	if !f123.UpgradeAvailable() {
+		t.Error("upgrade should be available (flag)")
+	}
+	select {
+	case <-f123.WaitForAvailableUpgrade():
+	default:
+		t.Error("upgrade should be available (chan)")
+	}
+
+	t.Logf("resource: %+v", res)
+	for _, rv := range res.Versions {
+		t.Logf("version %s: %+v", rv.VersionNumber, rv)
+	}
+}
+
+func TestVersionParsing(t *testing.T) {
+	t.Parallel()
+
+	assert.Equal(t, "1.2.3", parseVersion("1.2.3"))
+	assert.Equal(t, "1.2.0", parseVersion("1.2.0"))
+	assert.Equal(t, "0.2.0", parseVersion("0.2.0"))
+	assert.Equal(t, "0.0.0", parseVersion("0"))
+	assert.Equal(t, "1.2.3-b", parseVersion("1.2.3-b"))
+	assert.Equal(t, "1.2.3-b", parseVersion("1.2.3b"))
+	assert.Equal(t, "1.2.3-beta", parseVersion("1.2.3-beta"))
+	assert.Equal(t, "1.2.3-beta", parseVersion("1.2.3beta"))
+	assert.Equal(t, "1.2.3", parseVersion("01.02.03"))
+}
+
+func parseVersion(v string) string {
+	sv, err := semver.NewVersion(v)
+	if err != nil {
+		return fmt.Sprintf("failed to parse version: %s", err)
+	}
+	return sv.String()
+}
diff --git a/base/updater/signing.go b/base/updater/signing.go
new file mode 100644
index 00000000..cffd5cbe
--- /dev/null
+++ b/base/updater/signing.go
@@ -0,0 +1,49 @@
+package updater
+
+import (
+	"strings"
+
+	"github.com/safing/jess"
+)
+
+// VerificationOptions holds options for verification of files.
+type VerificationOptions struct {
+	TrustStore     jess.TrustStore
+	DownloadPolicy SignaturePolicy
+	DiskLoadPolicy SignaturePolicy
+}
+
+// GetVerificationOptions returns the verification options for the given identifier.
+func (reg *ResourceRegistry) GetVerificationOptions(identifier string) *VerificationOptions {
+	if reg.Verification == nil {
+		return nil
+	}
+
+	var (
+		longestPrefix = -1
+		bestMatch     *VerificationOptions
+	)
+	for prefix, opts := range reg.Verification {
+		if len(prefix) > longestPrefix && strings.HasPrefix(identifier, prefix) {
+			longestPrefix = len(prefix)
+			bestMatch = opts
+		}
+	}
+
+	return bestMatch
+}
+
+// SignaturePolicy defines behavior in case of errors.
+type SignaturePolicy uint8
+
+// Signature Policies.
+const (
+	// SignaturePolicyRequire fails on any error.
+	SignaturePolicyRequire = iota
+
+	// SignaturePolicyWarn only warns on errors.
+	SignaturePolicyWarn
+
+	// SignaturePolicyDisable only downloads signatures, but does not verify them.
+	SignaturePolicyDisable
+)
diff --git a/base/updater/state.go b/base/updater/state.go
new file mode 100644
index 00000000..20c27f46
--- /dev/null
+++ b/base/updater/state.go
@@ -0,0 +1,180 @@
+package updater
+
+import (
+	"sort"
+	"sync"
+	"time"
+
+	"github.com/safing/portmaster/base/utils"
+)
+
+// Registry States.
+const (
+	StateReady       = "ready"       // Default idle state.
+	StateChecking    = "checking"    // Downloading indexes.
+	StateDownloading = "downloading" // Downloading updates.
+	StateFetching    = "fetching"    // Fetching a single file.
+)
+
+// RegistryState describes the registry state.
+type RegistryState struct {
+	sync.Mutex
+	reg *ResourceRegistry
+
+	// ID holds the ID of the state the registry is currently in.
+	ID string
+
+	// Details holds further information about the current state.
+	Details any
+
+	// Updates holds generic information about the current status of pending
+	// and recently downloaded updates.
+	Updates UpdateState
+
+	// operationLock locks the operation of any state changing operation.
+	// This is separate from the registry lock, which locks access to the
+	// registry struct.
+	operationLock sync.Mutex
+}
+
+// StateDownloadingDetails holds details of the downloading state.
+type StateDownloadingDetails struct {
+	// Resources holds the resource IDs that are being downloaded.
+	Resources []string
+
+	// FinishedUpTo holds the index of Resources that is currently being
+	// downloaded. Previous resources have finished downloading.
+	FinishedUpTo int
+}
+
+// UpdateState holds generic information about the current status of pending
+// and recently downloaded updates.
+type UpdateState struct {
+	// LastCheckAt holds the time of the last update check.
+	LastCheckAt *time.Time
+	// LastCheckError holds the error of the last check.
+	LastCheckError error
+	// PendingDownload holds the resources that are pending download.
+	PendingDownload []string
+
+	// LastDownloadAt holds the time when resources were downloaded the last time.
+	LastDownloadAt *time.Time
+	// LastDownloadError holds the error of the last download.
+	LastDownloadError error
+	// LastDownload holds the resources that we downloaded the last time updates
+	// were downloaded.
+	LastDownload []string
+
+	// LastSuccessAt holds the time of the last successful update (check).
+	LastSuccessAt *time.Time
+}
+
+// GetState returns the current registry state.
+// The returned data must not be modified.
+func (reg *ResourceRegistry) GetState() RegistryState {
+	reg.state.Lock()
+	defer reg.state.Unlock()
+
+	return RegistryState{
+		ID:      reg.state.ID,
+		Details: reg.state.Details,
+		Updates: reg.state.Updates,
+	}
+}
+
+// StartOperation starts an operation.
+func (s *RegistryState) StartOperation(id string) bool {
+	defer s.notify()
+
+	s.operationLock.Lock()
+
+	s.Lock()
+	defer s.Unlock()
+
+	s.ID = id
+	return true
+}
+
+// UpdateOperationDetails updates the details of an operation.
+// The supplied struct should be a copy and must not be changed after calling
+// this function.
+func (s *RegistryState) UpdateOperationDetails(details any) {
+	defer s.notify()
+
+	s.Lock()
+	defer s.Unlock()
+
+	s.Details = details
+}
+
+// EndOperation ends an operation.
+func (s *RegistryState) EndOperation() {
+	defer s.notify()
+	defer s.operationLock.Unlock()
+
+	s.Lock()
+	defer s.Unlock()
+
+	s.ID = StateReady
+	s.Details = nil
+}
+
+// ReportUpdateCheck reports an update check to the registry state.
+func (s *RegistryState) ReportUpdateCheck(pendingDownload []string, failed error) {
+	defer s.notify()
+
+	sort.Strings(pendingDownload)
+
+	s.Lock()
+	defer s.Unlock()
+
+	now := time.Now()
+	s.Updates.LastCheckAt = &now
+	s.Updates.LastCheckError = failed
+	s.Updates.PendingDownload = pendingDownload
+
+	if failed == nil {
+		s.Updates.LastSuccessAt = &now
+	}
+}
+
+// ReportDownloads reports downloaded updates to the registry state.
+func (s *RegistryState) ReportDownloads(downloaded []string, failed error) {
+	defer s.notify()
+
+	sort.Strings(downloaded)
+
+	s.Lock()
+	defer s.Unlock()
+
+	now := time.Now()
+	s.Updates.LastDownloadAt = &now
+	s.Updates.LastDownloadError = failed
+	s.Updates.LastDownload = downloaded
+
+	// Remove downloaded resources from the pending list.
+	if len(s.Updates.PendingDownload) > 0 {
+		newPendingDownload := make([]string, 0, len(s.Updates.PendingDownload))
+		for _, pending := range s.Updates.PendingDownload {
+			if !utils.StringInSlice(downloaded, pending) {
+				newPendingDownload = append(newPendingDownload, pending)
+			}
+		}
+		s.Updates.PendingDownload = newPendingDownload
+	}
+
+	if failed == nil {
+		s.Updates.LastSuccessAt = &now
+	}
+}
+
+func (s *RegistryState) notify() {
+	switch {
+	case s.reg == nil:
+		return
+	case s.reg.StateNotifyFunc == nil:
+		return
+	}
+
+	s.reg.StateNotifyFunc(s)
+}
diff --git a/base/updater/storage.go b/base/updater/storage.go
new file mode 100644
index 00000000..cd05bdbd
--- /dev/null
+++ b/base/updater/storage.go
@@ -0,0 +1,272 @@
+package updater
+
+import (
+	"context"
+	"errors"
+	"fmt"
+	"io/fs"
+	"net/http"
+	"os"
+	"path/filepath"
+	"strings"
+
+	"github.com/safing/jess/filesig"
+	"github.com/safing/jess/lhash"
+	"github.com/safing/portmaster/base/log"
+	"github.com/safing/portmaster/base/utils"
+)
+
+// ScanStorage scans root within the storage dir and adds found
+// resources to the registry. If an error occurred, it is logged
+// and the last error is returned. Everything that was found
+// despite errors is added to the registry anyway. Leave root
+// empty to scan the full storage dir.
+func (reg *ResourceRegistry) ScanStorage(root string) error {
+	var lastError error
+
+	// prep root
+	if root == "" {
+		root = reg.storageDir.Path
+	} else {
+		var err error
+		root, err = filepath.Abs(root)
+		if err != nil {
+			return err
+		}
+		if !strings.HasPrefix(root, reg.storageDir.Path) {
+			return errors.New("supplied scan root path not within storage")
+		}
+	}
+
+	// walk fs
+	_ = filepath.Walk(root, func(path string, info os.FileInfo, err error) error {
+		// skip tmp dir (including errors trying to read it)
+		if strings.HasPrefix(path, reg.tmpDir.Path) {
+			return filepath.SkipDir
+		}
+
+		// handle walker error
+		if err != nil {
+			lastError = fmt.Errorf("%s: could not read %s: %w", reg.Name, path, err)
+			log.Warning(lastError.Error())
+			return nil
+		}
+
+		// Ignore file signatures.
+		if strings.HasSuffix(path, filesig.Extension) {
+			return nil
+		}
+
+		// get relative path to storage
+		relativePath, err := filepath.Rel(reg.storageDir.Path, path)
+		if err != nil {
+			lastError = fmt.Errorf("%s: could not get relative path of %s: %w", reg.Name, path, err)
+			log.Warning(lastError.Error())
+			return nil
+		}
+
+		// convert to identifier and version
+		relativePath = filepath.ToSlash(relativePath)
+		identifier, version, ok := GetIdentifierAndVersion(relativePath)
+		if !ok {
+			// file does not conform to format
+			return nil
+		}
+
+		// fully ignore directories that also have an identifier - these will be unpacked resources
+		if info.IsDir() {
+			return filepath.SkipDir
+		}
+
+		// save
+		err = reg.AddResource(identifier, version, nil, true, false, false)
+		if err != nil {
+			lastError = fmt.Errorf("%s: could not get add resource %s v%s: %w", reg.Name, identifier, version, err)
+			log.Warning(lastError.Error())
+		}
+		return nil
+	})
+
+	return lastError
+}
+
+// LoadIndexes loads the current release indexes from disk
+// or will fetch a new version if not available and the
+// registry is marked as online.
+func (reg *ResourceRegistry) LoadIndexes(ctx context.Context) error {
+	var firstErr error
+	client := &http.Client{}
+	for _, idx := range reg.getIndexes() {
+		err := reg.loadIndexFile(idx)
+		if err == nil {
+			log.Debugf("%s: loaded index %s", reg.Name, idx.Path)
+		} else if reg.Online {
+			// try to download the index file if a local disk version
+			// does not exist or we don't have permission to read it.
+			if errors.Is(err, fs.ErrNotExist) || errors.Is(err, fs.ErrPermission) {
+				err = reg.downloadIndex(ctx, client, idx)
+			}
+		}
+
+		if err != nil && firstErr == nil {
+			firstErr = err
+		}
+	}
+
+	return firstErr
+}
+
+// getIndexes returns a copy of the index.
+// The indexes itself are references.
+func (reg *ResourceRegistry) getIndexes() []*Index {
+	reg.RLock()
+	defer reg.RUnlock()
+
+	indexes := make([]*Index, len(reg.indexes))
+	copy(indexes, reg.indexes)
+	return indexes
+}
+
+func (reg *ResourceRegistry) loadIndexFile(idx *Index) error {
+	indexPath := filepath.Join(reg.storageDir.Path, filepath.FromSlash(idx.Path))
+	indexData, err := os.ReadFile(indexPath)
+	if err != nil {
+		return fmt.Errorf("failed to read index file %s: %w", idx.Path, err)
+	}
+
+	// Verify signature, if enabled.
+	if verifOpts := reg.GetVerificationOptions(idx.Path); verifOpts != nil {
+		// Load and check signature.
+		verifiedHash, _, err := reg.loadAndVerifySigFile(verifOpts, indexPath+filesig.Extension)
+		if err != nil {
+			switch verifOpts.DiskLoadPolicy {
+			case SignaturePolicyRequire:
+				return fmt.Errorf("failed to verify signature of index %s: %w", idx.Path, err)
+			case SignaturePolicyWarn:
+				log.Warningf("%s: failed to verify signature of index %s: %s", reg.Name, idx.Path, err)
+			case SignaturePolicyDisable:
+				log.Debugf("%s: failed to verify signature of index %s: %s", reg.Name, idx.Path, err)
+			}
+		}
+
+		// Check if signature checksum matches the index data.
+		if err == nil && !verifiedHash.Matches(indexData) {
+			switch verifOpts.DiskLoadPolicy {
+			case SignaturePolicyRequire:
+				return fmt.Errorf("index file %s does not match signature", idx.Path)
+			case SignaturePolicyWarn:
+				log.Warningf("%s: index file %s does not match signature", reg.Name, idx.Path)
+			case SignaturePolicyDisable:
+				log.Debugf("%s: index file %s does not match signature", reg.Name, idx.Path)
+			}
+		}
+	}
+
+	// Parse the index file.
+	indexFile, err := ParseIndexFile(indexData, idx.Channel, idx.LastRelease)
+	if err != nil {
+		return fmt.Errorf("failed to parse index file %s: %w", idx.Path, err)
+	}
+
+	// Update last seen release.
+	idx.LastRelease = indexFile.Published
+
+	// Warn if there aren't any releases in the index.
+	if len(indexFile.Releases) == 0 {
+		log.Debugf("%s: index %s has no releases", reg.Name, idx.Path)
+		return nil
+	}
+
+	// Add index releases to available resources.
+	err = reg.AddResources(indexFile.Releases, idx, false, true, idx.PreRelease)
+	if err != nil {
+		log.Warningf("%s: failed to add resource: %s", reg.Name, err)
+	}
+	return nil
+}
+
+func (reg *ResourceRegistry) loadAndVerifySigFile(verifOpts *VerificationOptions, sigFilePath string) (*lhash.LabeledHash, []byte, error) {
+	// Load signature file.
+	sigFileData, err := os.ReadFile(sigFilePath)
+	if err != nil {
+		return nil, nil, fmt.Errorf("failed to read signature file: %w", err)
+	}
+
+	// Extract all signatures.
+	sigs, err := filesig.ParseSigFile(sigFileData)
+	switch {
+	case len(sigs) == 0 && err != nil:
+		return nil, nil, fmt.Errorf("failed to parse signature file: %w", err)
+	case len(sigs) == 0:
+		return nil, nil, errors.New("no signatures found in signature file")
+	case err != nil:
+		return nil, nil, fmt.Errorf("failed to parse signature file: %w", err)
+	}
+
+	// Verify all signatures.
+	var verifiedHash *lhash.LabeledHash
+	for _, sig := range sigs {
+		fd, err := filesig.VerifyFileData(
+			sig,
+			nil,
+			verifOpts.TrustStore,
+		)
+		if err != nil {
+			return nil, sigFileData, err
+		}
+
+		// Save or check verified hash.
+		if verifiedHash == nil {
+			verifiedHash = fd.FileHash()
+		} else if !fd.FileHash().Equal(verifiedHash) {
+			// Return an error if two valid hashes mismatch.
+			// For simplicity, all hash algorithms must be the same for now.
+			return nil, sigFileData, errors.New("file hashes from different signatures do not match")
+		}
+	}
+
+	return verifiedHash, sigFileData, nil
+}
+
+// CreateSymlinks creates a directory structure with unversioned symlinks to the given updates list.
+func (reg *ResourceRegistry) CreateSymlinks(symlinkRoot *utils.DirStructure) error {
+	err := os.RemoveAll(symlinkRoot.Path)
+	if err != nil {
+		return fmt.Errorf("failed to wipe symlink root: %w", err)
+	}
+
+	err = symlinkRoot.Ensure()
+	if err != nil {
+		return fmt.Errorf("failed to create symlink root: %w", err)
+	}
+
+	reg.RLock()
+	defer reg.RUnlock()
+
+	for _, res := range reg.resources {
+		if res.SelectedVersion == nil {
+			return fmt.Errorf("no selected version available for %s", res.Identifier)
+		}
+
+		targetPath := res.SelectedVersion.storagePath()
+		linkPath := filepath.Join(symlinkRoot.Path, filepath.FromSlash(res.Identifier))
+		linkPathDir := filepath.Dir(linkPath)
+
+		err = symlinkRoot.EnsureAbsPath(linkPathDir)
+		if err != nil {
+			return fmt.Errorf("failed to create dir for link: %w", err)
+		}
+
+		relativeTargetPath, err := filepath.Rel(linkPathDir, targetPath)
+		if err != nil {
+			return fmt.Errorf("failed to get relative target path: %w", err)
+		}
+
+		err = os.Symlink(relativeTargetPath, linkPath)
+		if err != nil {
+			return fmt.Errorf("failed to link %s: %w", res.Identifier, err)
+		}
+	}
+
+	return nil
+}
diff --git a/base/updater/storage_test.go b/base/updater/storage_test.go
new file mode 100644
index 00000000..2e4122fa
--- /dev/null
+++ b/base/updater/storage_test.go
@@ -0,0 +1,68 @@
+package updater
+
+/*
+func testLoadLatestScope(t *testing.T, basePath, filePath, expectedIdentifier, expectedVersion string) {
+	fullPath := filepath.Join(basePath, filePath)
+
+	// create dir
+	dirPath := filepath.Dir(fullPath)
+	err := os.MkdirAll(dirPath, 0755)
+	if err != nil {
+		t.Fatalf("could not create test dir: %s\n", err)
+		return
+	}
+
+	// touch file
+	err = os.WriteFile(fullPath, []byte{}, 0644)
+	if err != nil {
+		t.Fatalf("could not create test file: %s\n", err)
+		return
+	}
+
+	// run loadLatestScope
+	latest, err := ScanForLatest(basePath, true)
+	if err != nil {
+		t.Errorf("could not update latest: %s\n", err)
+		return
+	}
+	for key, val := range latest {
+		localUpdates[key] = val
+	}
+
+	// test result
+	version, ok := localUpdates[expectedIdentifier]
+	if !ok {
+		t.Errorf("identifier %s not in map", expectedIdentifier)
+		t.Errorf("current map: %v", localUpdates)
+	}
+	if version != expectedVersion {
+		t.Errorf("unexpected version for %s: %s", filePath, version)
+	}
+}
+
+func TestLoadLatestScope(t *testing.T) {
+
+	updatesLock.Lock()
+	defer updatesLock.Unlock()
+
+	tmpDir, err := os.MkdirTemp("", "testing_")
+	if err != nil {
+		t.Fatalf("could not create test dir: %s\n", err)
+		return
+	}
+	defer os.RemoveAll(tmpDir)
+
+	testLoadLatestScope(t, tmpDir, "all/ui/assets_v1-2-3.zip", "all/ui/assets.zip", "1.2.3")
+	testLoadLatestScope(t, tmpDir, "all/ui/assets_v1-2-4b.zip", "all/ui/assets.zip", "1.2.4b")
+	testLoadLatestScope(t, tmpDir, "all/ui/assets_v1-2-5.zip", "all/ui/assets.zip", "1.2.5")
+	testLoadLatestScope(t, tmpDir, "all/ui/assets_v1-3-4.zip", "all/ui/assets.zip", "1.3.4")
+	testLoadLatestScope(t, tmpDir, "all/ui/assets_v2-3-4.zip", "all/ui/assets.zip", "2.3.4")
+	testLoadLatestScope(t, tmpDir, "all/ui/assets_v1-2-3.zip", "all/ui/assets.zip", "2.3.4")
+	testLoadLatestScope(t, tmpDir, "all/ui/assets_v1-2-4.zip", "all/ui/assets.zip", "2.3.4")
+	testLoadLatestScope(t, tmpDir, "all/ui/assets_v1-3-4.zip", "all/ui/assets.zip", "2.3.4")
+	testLoadLatestScope(t, tmpDir, "os_platform/portmaster/portmaster_v1-2-3", "os_platform/portmaster/portmaster", "1.2.3")
+	testLoadLatestScope(t, tmpDir, "os_platform/portmaster/portmaster_v2-1-1", "os_platform/portmaster/portmaster", "2.1.1")
+	testLoadLatestScope(t, tmpDir, "os_platform/portmaster/portmaster_v1-2-3", "os_platform/portmaster/portmaster", "2.1.1")
+
+}
+*/
diff --git a/base/updater/unpacking.go b/base/updater/unpacking.go
new file mode 100644
index 00000000..75d48921
--- /dev/null
+++ b/base/updater/unpacking.go
@@ -0,0 +1,195 @@
+package updater
+
+import (
+	"archive/zip"
+	"compress/gzip"
+	"errors"
+	"fmt"
+	"io"
+	"io/fs"
+	"os"
+	"path"
+	"path/filepath"
+	"strings"
+
+	"github.com/hashicorp/go-multierror"
+
+	"github.com/safing/portmaster/base/log"
+	"github.com/safing/portmaster/base/utils"
+)
+
+// MaxUnpackSize specifies the maximum size that will be unpacked.
+const MaxUnpackSize = 1000000000 // 1GB
+
+// UnpackGZIP unpacks a GZIP compressed reader r
+// and returns a new reader. It's suitable to be
+// used with registry.GetPackedFile.
+func UnpackGZIP(r io.Reader) (io.Reader, error) {
+	return gzip.NewReader(r)
+}
+
+// UnpackResources unpacks all resources defined in the AutoUnpack list.
+func (reg *ResourceRegistry) UnpackResources() error {
+	reg.RLock()
+	defer reg.RUnlock()
+
+	var multierr *multierror.Error
+	for _, res := range reg.resources {
+		if utils.StringInSlice(reg.AutoUnpack, res.Identifier) {
+			err := res.UnpackArchive()
+			if err != nil {
+				multierr = multierror.Append(
+					multierr,
+					fmt.Errorf("%s: %w", res.Identifier, err),
+				)
+			}
+		}
+	}
+
+	return multierr.ErrorOrNil()
+}
+
+const (
+	zipSuffix = ".zip"
+)
+
+// UnpackArchive unpacks the archive the resource refers to. The contents are
+// unpacked into a directory with the same name as the file, excluding the
+// suffix. If the destination folder already exists, it is assumed that the
+// contents have already been correctly unpacked.
+func (res *Resource) UnpackArchive() error {
+	res.Lock()
+	defer res.Unlock()
+
+	// Only unpack selected versions.
+	if res.SelectedVersion == nil {
+		return nil
+	}
+
+	switch {
+	case strings.HasSuffix(res.Identifier, zipSuffix):
+		return res.unpackZipArchive()
+	default:
+		return fmt.Errorf("unsupported file type for unpacking")
+	}
+}
+
+func (res *Resource) unpackZipArchive() error {
+	// Get file and directory paths.
+	archiveFile := res.SelectedVersion.storagePath()
+	destDir := strings.TrimSuffix(archiveFile, zipSuffix)
+	tmpDir := filepath.Join(
+		res.registry.tmpDir.Path,
+		filepath.FromSlash(strings.TrimSuffix(
+			path.Base(res.SelectedVersion.versionedPath()),
+			zipSuffix,
+		)),
+	)
+
+	// Check status of destination.
+	dstStat, err := os.Stat(destDir)
+	switch {
+	case errors.Is(err, fs.ErrNotExist):
+		// The destination does not exist, continue with unpacking.
+	case err != nil:
+		return fmt.Errorf("cannot access destination for unpacking: %w", err)
+	case !dstStat.IsDir():
+		return fmt.Errorf("destination for unpacking is blocked by file: %s", dstStat.Name())
+	default:
+		// Archive already seems to be unpacked.
+		return nil
+	}
+
+	// Create the tmp directory for unpacking.
+	err = res.registry.tmpDir.EnsureAbsPath(tmpDir)
+	if err != nil {
+		return fmt.Errorf("failed to create tmp dir for unpacking: %w", err)
+	}
+
+	// Defer clean up of directories.
+	defer func() {
+		// Always clean up the tmp dir.
+		_ = os.RemoveAll(tmpDir)
+		// Cleanup the destination in case of an error.
+		if err != nil {
+			_ = os.RemoveAll(destDir)
+		}
+	}()
+
+	// Open the archive for reading.
+	var archiveReader *zip.ReadCloser
+	archiveReader, err = zip.OpenReader(archiveFile)
+	if err != nil {
+		return fmt.Errorf("failed to open zip reader: %w", err)
+	}
+	defer func() {
+		_ = archiveReader.Close()
+	}()
+
+	// Save all files to the tmp dir.
+	for _, file := range archiveReader.File {
+		err = copyFromZipArchive(
+			file,
+			filepath.Join(tmpDir, filepath.FromSlash(file.Name)),
+		)
+		if err != nil {
+			return fmt.Errorf("failed to extract archive file %s: %w", file.Name, err)
+		}
+	}
+
+	// Make the final move.
+	err = os.Rename(tmpDir, destDir)
+	if err != nil {
+		return fmt.Errorf("failed to move the extracted archive from %s to %s: %w", tmpDir, destDir, err)
+	}
+
+	// Fix permissions on the destination dir.
+	err = res.registry.storageDir.EnsureAbsPath(destDir)
+	if err != nil {
+		return fmt.Errorf("failed to apply directory permissions on %s: %w", destDir, err)
+	}
+
+	log.Infof("%s: unpacked %s", res.registry.Name, res.SelectedVersion.versionedPath())
+	return nil
+}
+
+func copyFromZipArchive(archiveFile *zip.File, dstPath string) error {
+	// If file is a directory, create it and continue.
+	if archiveFile.FileInfo().IsDir() {
+		err := os.Mkdir(dstPath, archiveFile.Mode())
+		if err != nil {
+			return fmt.Errorf("failed to create directory %s: %w", dstPath, err)
+		}
+		return nil
+	}
+
+	// Open archived file for reading.
+	fileReader, err := archiveFile.Open()
+	if err != nil {
+		return fmt.Errorf("failed to open file in archive: %w", err)
+	}
+	defer func() {
+		_ = fileReader.Close()
+	}()
+
+	// Open destination file for writing.
+	dstFile, err := os.OpenFile(dstPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, archiveFile.Mode())
+	if err != nil {
+		return fmt.Errorf("failed to open destination file %s: %w", dstPath, err)
+	}
+	defer func() {
+		_ = dstFile.Close()
+	}()
+
+	// Copy full file from archive to dst.
+	if _, err := io.CopyN(dstFile, fileReader, MaxUnpackSize); err != nil {
+		// EOF is expected here as the archive is likely smaller
+		// thane MaxUnpackSize
+		if errors.Is(err, io.EOF) {
+			return nil
+		}
+		return err
+	}
+
+	return nil
+}
diff --git a/base/updater/updating.go b/base/updater/updating.go
new file mode 100644
index 00000000..cf87472e
--- /dev/null
+++ b/base/updater/updating.go
@@ -0,0 +1,359 @@
+package updater
+
+import (
+	"context"
+	"fmt"
+	"net/http"
+	"os"
+	"path"
+	"path/filepath"
+	"strings"
+
+	"golang.org/x/exp/slices"
+
+	"github.com/safing/jess/filesig"
+	"github.com/safing/jess/lhash"
+	"github.com/safing/portmaster/base/log"
+	"github.com/safing/portmaster/base/utils"
+)
+
+// UpdateIndexes downloads all indexes. An error is only returned when all
+// indexes fail to update.
+func (reg *ResourceRegistry) UpdateIndexes(ctx context.Context) error {
+	var lastErr error
+	var anySuccess bool
+
+	// Start registry operation.
+	reg.state.StartOperation(StateChecking)
+	defer reg.state.EndOperation()
+
+	client := &http.Client{}
+	for _, idx := range reg.getIndexes() {
+		if err := reg.downloadIndex(ctx, client, idx); err != nil {
+			lastErr = err
+			log.Warningf("%s: failed to update index %s: %s", reg.Name, idx.Path, err)
+		} else {
+			anySuccess = true
+		}
+	}
+
+	// If all indexes failed to update, fail.
+	if !anySuccess {
+		err := fmt.Errorf("failed to update all indexes, last error was: %w", lastErr)
+		reg.state.ReportUpdateCheck(nil, err)
+		return err
+	}
+
+	// Get pending resources and update status.
+	pendingResourceVersions, _ := reg.GetPendingDownloads(true, false)
+	reg.state.ReportUpdateCheck(
+		humanInfoFromResourceVersions(pendingResourceVersions),
+		nil,
+	)
+
+	return nil
+}
+
+func (reg *ResourceRegistry) downloadIndex(ctx context.Context, client *http.Client, idx *Index) error {
+	var (
+		// Index.
+		indexErr    error
+		indexData   []byte
+		downloadURL string
+
+		// Signature.
+		sigErr       error
+		verifiedHash *lhash.LabeledHash
+		sigFileData  []byte
+		verifOpts    = reg.GetVerificationOptions(idx.Path)
+	)
+
+	// Upgrade to v2 index if verification is enabled.
+	downloadIndexPath := idx.Path
+	if verifOpts != nil {
+		downloadIndexPath = strings.TrimSuffix(downloadIndexPath, baseIndexExtension) + v2IndexExtension
+	}
+
+	// Download new index and signature.
+	for tries := range 3 {
+		// Index and signature need to be fetched together, so that they are
+		// fetched from the same source. One source should always have a matching
+		// index and signature. Backup sources may be behind a little.
+		// If the signature verification fails, another source should be tried.
+
+		// Get index data.
+		indexData, downloadURL, indexErr = reg.fetchData(ctx, client, downloadIndexPath, tries)
+		if indexErr != nil {
+			log.Debugf("%s: failed to fetch index %s: %s", reg.Name, downloadURL, indexErr)
+			continue
+		}
+
+		// Get signature and verify it.
+		if verifOpts != nil {
+			verifiedHash, sigFileData, sigErr = reg.fetchAndVerifySigFile(
+				ctx, client,
+				verifOpts, downloadIndexPath+filesig.Extension, nil,
+				tries,
+			)
+			if sigErr != nil {
+				log.Debugf("%s: failed to verify signature of %s: %s", reg.Name, downloadURL, sigErr)
+				continue
+			}
+
+			// Check if the index matches the verified hash.
+			if verifiedHash.Matches(indexData) {
+				log.Infof("%s: verified signature of %s", reg.Name, downloadURL)
+			} else {
+				sigErr = ErrIndexChecksumMismatch
+				log.Debugf("%s: checksum does not match file from %s", reg.Name, downloadURL)
+				continue
+			}
+		}
+
+		break
+	}
+	if indexErr != nil {
+		return fmt.Errorf("failed to fetch index %s: %w", downloadIndexPath, indexErr)
+	}
+	if sigErr != nil {
+		return fmt.Errorf("failed to fetch or verify index %s signature: %w", downloadIndexPath, sigErr)
+	}
+
+	// Parse the index file.
+	indexFile, err := ParseIndexFile(indexData, idx.Channel, idx.LastRelease)
+	if err != nil {
+		return fmt.Errorf("failed to parse index %s: %w", idx.Path, err)
+	}
+
+	// Add index data to registry.
+	if len(indexFile.Releases) > 0 {
+		// Check if all resources are within the indexes' authority.
+		authoritativePath := path.Dir(idx.Path) + "/"
+		if authoritativePath == "./" {
+			// Fix path for indexes at the storage root.
+			authoritativePath = ""
+		}
+		cleanedData := make(map[string]string, len(indexFile.Releases))
+		for key, version := range indexFile.Releases {
+			if strings.HasPrefix(key, authoritativePath) {
+				cleanedData[key] = version
+			} else {
+				log.Warningf("%s: index %s oversteps it's authority by defining version for %s", reg.Name, idx.Path, key)
+			}
+		}
+
+		// add resources to registry
+		err = reg.AddResources(cleanedData, idx, false, true, idx.PreRelease)
+		if err != nil {
+			log.Warningf("%s: failed to add resources: %s", reg.Name, err)
+		}
+	} else {
+		log.Debugf("%s: index %s is empty", reg.Name, idx.Path)
+	}
+
+	// Check if dest dir exists.
+	indexDir := filepath.FromSlash(path.Dir(idx.Path))
+	err = reg.storageDir.EnsureRelPath(indexDir)
+	if err != nil {
+		log.Warningf("%s: failed to ensure directory for updated index %s: %s", reg.Name, idx.Path, err)
+	}
+
+	// Index files must be readable by portmaster-staert with user permissions in order to load the index.
+	err = os.WriteFile( //nolint:gosec
+		filepath.Join(reg.storageDir.Path, filepath.FromSlash(idx.Path)),
+		indexData, 0o0644,
+	)
+	if err != nil {
+		log.Warningf("%s: failed to save updated index %s: %s", reg.Name, idx.Path, err)
+	}
+
+	// Write signature file, if we have one.
+	if len(sigFileData) > 0 {
+		err = os.WriteFile( //nolint:gosec
+			filepath.Join(reg.storageDir.Path, filepath.FromSlash(idx.Path)+filesig.Extension),
+			sigFileData, 0o0644,
+		)
+		if err != nil {
+			log.Warningf("%s: failed to save updated index signature %s: %s", reg.Name, idx.Path+filesig.Extension, err)
+		}
+	}
+
+	log.Infof("%s: updated index %s with %d entries", reg.Name, idx.Path, len(indexFile.Releases))
+	return nil
+}
+
+// DownloadUpdates checks if updates are available and downloads updates of used components.
+func (reg *ResourceRegistry) DownloadUpdates(ctx context.Context, includeManual bool) error {
+	// Start registry operation.
+	reg.state.StartOperation(StateDownloading)
+	defer reg.state.EndOperation()
+
+	// Get pending updates.
+	toUpdate, missingSigs := reg.GetPendingDownloads(includeManual, true)
+	downloadDetailsResources := humanInfoFromResourceVersions(toUpdate)
+	reg.state.UpdateOperationDetails(&StateDownloadingDetails{
+		Resources: downloadDetailsResources,
+	})
+
+	// nothing to update
+	if len(toUpdate) == 0 && len(missingSigs) == 0 {
+		log.Infof("%s: everything up to date", reg.Name)
+		return nil
+	}
+
+	// check download dir
+	if err := reg.tmpDir.Ensure(); err != nil {
+		return fmt.Errorf("could not prepare tmp directory for download: %w", err)
+	}
+
+	// download updates
+	log.Infof("%s: starting to download %d updates", reg.Name, len(toUpdate))
+	client := &http.Client{}
+	var reportError error
+
+	for i, rv := range toUpdate {
+		log.Infof(
+			"%s: downloading update [%d/%d]: %s version %s",
+			reg.Name,
+			i+1, len(toUpdate),
+			rv.resource.Identifier, rv.VersionNumber,
+		)
+		var err error
+		for tries := range 3 {
+			err = reg.fetchFile(ctx, client, rv, tries)
+			if err == nil {
+				// Update resource version state.
+				rv.resource.Lock()
+				rv.Available = true
+				if rv.resource.VerificationOptions != nil {
+					rv.SigAvailable = true
+				}
+				rv.resource.Unlock()
+
+				break
+			}
+		}
+		if err != nil {
+			reportError := fmt.Errorf("failed to download %s version %s: %w", rv.resource.Identifier, rv.VersionNumber, err)
+			log.Warningf("%s: %s", reg.Name, reportError)
+		}
+
+		reg.state.UpdateOperationDetails(&StateDownloadingDetails{
+			Resources:    downloadDetailsResources,
+			FinishedUpTo: i + 1,
+		})
+	}
+
+	if len(missingSigs) > 0 {
+		log.Infof("%s: downloading %d missing signatures", reg.Name, len(missingSigs))
+
+		for _, rv := range missingSigs {
+			var err error
+			for tries := range 3 {
+				err = reg.fetchMissingSig(ctx, client, rv, tries)
+				if err == nil {
+					// Update resource version state.
+					rv.resource.Lock()
+					rv.SigAvailable = true
+					rv.resource.Unlock()
+
+					break
+				}
+			}
+			if err != nil {
+				reportError := fmt.Errorf("failed to download missing sig of %s version %s: %w", rv.resource.Identifier, rv.VersionNumber, err)
+				log.Warningf("%s: %s", reg.Name, reportError)
+			}
+		}
+	}
+
+	reg.state.ReportDownloads(
+		downloadDetailsResources,
+		reportError,
+	)
+	log.Infof("%s: finished downloading updates", reg.Name)
+
+	return nil
+}
+
+// DownloadUpdates checks if updates are available and downloads updates of used components.
+
+// GetPendingDownloads returns the list of pending downloads.
+// If manual is set, indexes with AutoDownload=false will be checked.
+// If auto is set, indexes with AutoDownload=true will be checked.
+func (reg *ResourceRegistry) GetPendingDownloads(manual, auto bool) (resources, sigs []*ResourceVersion) {
+	reg.RLock()
+	defer reg.RUnlock()
+
+	// create list of downloads
+	var toUpdate []*ResourceVersion
+	var missingSigs []*ResourceVersion
+
+	for _, res := range reg.resources {
+		func() {
+			res.Lock()
+			defer res.Unlock()
+
+			// Skip resources without index or indexes that should not be reported
+			// according to parameters.
+			switch {
+			case res.Index == nil:
+				// Cannot download if resource is not part of an index.
+				return
+			case manual && !res.Index.AutoDownload:
+				// Manual update report and index is not auto-download.
+			case auto && res.Index.AutoDownload:
+				// Auto update report and index is auto-download.
+			default:
+				// Resource should not be reported.
+				return
+			}
+
+			// Skip resources we don't need.
+			switch {
+			case res.inUse():
+				// Update if resource is in use.
+			case res.available():
+				// Update if resource is available locally, ie. was used in the past.
+			case utils.StringInSlice(reg.MandatoryUpdates, res.Identifier):
+				// Update is set as mandatory.
+			default:
+				// Resource does not need to be updated.
+				return
+			}
+
+			// Go through all versions until we find versions that need updating.
+			for _, rv := range res.Versions {
+				switch {
+				case !rv.CurrentRelease:
+					// We are not interested in older releases.
+				case !rv.Available:
+					// File not available locally, download!
+					toUpdate = append(toUpdate, rv)
+				case !rv.SigAvailable && res.VerificationOptions != nil:
+					// File signature is not available and verification is enabled, download signature!
+					missingSigs = append(missingSigs, rv)
+				}
+			}
+		}()
+	}
+
+	slices.SortFunc(toUpdate, func(a, b *ResourceVersion) int {
+		return strings.Compare(a.resource.Identifier, b.resource.Identifier)
+	})
+	slices.SortFunc(missingSigs, func(a, b *ResourceVersion) int {
+		return strings.Compare(a.resource.Identifier, b.resource.Identifier)
+	})
+
+	return toUpdate, missingSigs
+}
+
+func humanInfoFromResourceVersions(resourceVersions []*ResourceVersion) []string {
+	identifiers := make([]string, len(resourceVersions))
+
+	for i, rv := range resourceVersions {
+		identifiers[i] = fmt.Sprintf("%s v%s", rv.resource.Identifier, rv.VersionNumber)
+	}
+
+	return identifiers
+}
diff --git a/cmds/hub/main.go b/cmds/hub/main.go
index 1fdc8809..3db002b3 100644
--- a/cmds/hub/main.go
+++ b/cmds/hub/main.go
@@ -18,13 +18,13 @@ import (
 	"github.com/safing/portmaster/base/metrics"
 	"github.com/safing/portmaster/service/mgr"
 	"github.com/safing/portmaster/service/updates"
+	"github.com/safing/portmaster/service/updates/helper"
 	"github.com/safing/portmaster/spn"
 	"github.com/safing/portmaster/spn/conf"
 )
 
 func init() {
-	// flag.BoolVar(&updates.RebootOnRestart, "reboot-on-restart", false, "reboot server on auto-upgrade")
-	// FIXME
+	flag.BoolVar(&updates.RebootOnRestart, "reboot-on-restart", false, "reboot server on auto-upgrade")
 }
 
 var sigUSR1 = syscall.Signal(0xa)
@@ -40,15 +40,14 @@ func main() {
 
 	// Configure user agent and updates.
 	updates.UserAgent = fmt.Sprintf("SPN Hub (%s %s)", runtime.GOOS, runtime.GOARCH)
-	// helper.IntelOnly()
+	helper.IntelOnly()
 
 	// Set SPN public hub mode.
 	conf.EnablePublicHub(true)
 
-	// Start logger with default log level.
-	_ = log.Start(log.WarningLevel)
-
-	// FIXME: Use service?
+	// Set default log level.
+	log.SetLogLevel(log.WarningLevel)
+	_ = log.Start()
 
 	// Create instance.
 	var execCmdLine bool
@@ -111,7 +110,7 @@ func main() {
 			slog.Warn("program was interrupted, stopping")
 		}
 
-	case <-instance.ShutdownComplete():
+	case <-instance.Stopped():
 		log.Shutdown()
 		os.Exit(instance.ExitCode())
 	}
diff --git a/cmds/notifier/.gitignore b/cmds/notifier/.gitignore
new file mode 100644
index 00000000..602ad23c
--- /dev/null
+++ b/cmds/notifier/.gitignore
@@ -0,0 +1,34 @@
+# Compiled binaries
+notifier
+notifier.exe
+
+# Go vendor
+vendor
+
+# Compiled Object files, Static and Dynamic libs (Shared Objects)
+*.o
+*.a
+*.so
+
+# Folders
+_obj
+_test
+
+# Architecture specific extensions/prefixes
+*.[568vq]
+[568vq].out
+
+*.cgo1.go
+*.cgo2.c
+_cgo_defun.c
+_cgo_gotypes.go
+_cgo_export.*
+
+_testmain.go
+
+*.exe
+*.test
+*.prof
+
+# Output of the go coverage tool, specifically when used with LiteIDE
+*.out
diff --git a/cmds/notifier/README.md b/cmds/notifier/README.md
new file mode 100644
index 00000000..bdfcece8
--- /dev/null
+++ b/cmds/notifier/README.md
@@ -0,0 +1,5 @@
+### Development Dependencies
+
+sudo apt install libgtk-3-dev libayatana-appindicator3-dev libwebkitgtk-3.0-dev libgl1-mesa-dev libglu1-mesa-dev libnotify-dev libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev
+
+sudo pacman -S libappindicator-gtk3 
diff --git a/cmds/notifier/http_api.go b/cmds/notifier/http_api.go
new file mode 100644
index 00000000..7a68349a
--- /dev/null
+++ b/cmds/notifier/http_api.go
@@ -0,0 +1,63 @@
+package main
+
+import (
+	"fmt"
+	"io"
+	"net/http"
+	"net/http/cookiejar"
+	"strings"
+	"time"
+
+	"github.com/safing/portmaster/base/log"
+)
+
+const (
+	apiBaseURL          = "http://127.0.0.1:817/api/v1/"
+	apiShutdownEndpoint = "core/shutdown"
+)
+
+var httpAPIClient *http.Client
+
+func init() {
+	// Make cookie jar.
+	jar, err := cookiejar.New(nil)
+	if err != nil {
+		log.Warningf("http-api: failed to create cookie jar: %s", err)
+		jar = nil
+	}
+
+	// Create client.
+	httpAPIClient = &http.Client{
+		Jar:     jar,
+		Timeout: 3 * time.Second,
+	}
+}
+
+func httpAPIAction(endpoint string) (response string, err error) {
+	// Make action request.
+	resp, err := httpAPIClient.Post(apiBaseURL+endpoint, "", nil)
+	if err != nil {
+		return "", fmt.Errorf("request failed: %w", err)
+	}
+
+	// Read the response body.
+	defer func() { _ = resp.Body.Close() }()
+	respData, err := io.ReadAll(resp.Body)
+	if err != nil {
+		return "", fmt.Errorf("failed to read data: %w", err)
+	}
+	response = strings.TrimSpace(string(respData))
+
+	// Check if the request was successful on the server.
+	if resp.StatusCode >= 200 && resp.StatusCode < 300 {
+		return response, fmt.Errorf("server failed with %s: %s", resp.Status, response)
+	}
+
+	return response, nil
+}
+
+// TriggerShutdown triggers a shutdown via the APi.
+func TriggerShutdown() error {
+	_, err := httpAPIAction(apiShutdownEndpoint)
+	return err
+}
diff --git a/cmds/notifier/icons.go b/cmds/notifier/icons.go
new file mode 100644
index 00000000..b3690a3f
--- /dev/null
+++ b/cmds/notifier/icons.go
@@ -0,0 +1,25 @@
+package main
+
+import (
+	"os"
+	"path/filepath"
+	"sync"
+
+	icons "github.com/safing/portmaster/assets"
+)
+
+var (
+	appIconEnsureOnce sync.Once
+	appIconPath       string
+)
+
+func ensureAppIcon() (location string, err error) {
+	appIconEnsureOnce.Do(func() {
+		if appIconPath == "" {
+			appIconPath = filepath.Join(dataDir, "exec", "portmaster.png")
+		}
+		err = os.WriteFile(appIconPath, icons.PNG, 0o0644) // nolint:gosec
+	})
+
+	return appIconPath, err
+}
diff --git a/cmds/notifier/main.go b/cmds/notifier/main.go
new file mode 100644
index 00000000..e40487bb
--- /dev/null
+++ b/cmds/notifier/main.go
@@ -0,0 +1,287 @@
+package main
+
+import (
+	"context"
+	"errors"
+	"flag"
+	"fmt"
+	"os"
+	"os/signal"
+	"path/filepath"
+	"runtime"
+	"runtime/pprof"
+	"strings"
+	"sync"
+	"syscall"
+	"time"
+
+	"github.com/tevino/abool"
+
+	"github.com/safing/portmaster/base/api/client"
+	"github.com/safing/portmaster/base/dataroot"
+	"github.com/safing/portmaster/base/info"
+	"github.com/safing/portmaster/base/log"
+	"github.com/safing/portmaster/base/updater"
+	"github.com/safing/portmaster/base/utils"
+	"github.com/safing/portmaster/service/updates/helper"
+)
+
+var (
+	dataDir          string
+	printStackOnExit bool
+	showVersion      bool
+
+	apiClient    = client.NewClient("127.0.0.1:817")
+	connected    = abool.New()
+	shuttingDown = abool.New()
+	restarting   = abool.New()
+
+	mainCtx, cancelMainCtx = context.WithCancel(context.Background())
+	mainWg                 = &sync.WaitGroup{}
+
+	dataRoot *utils.DirStructure
+	// Create registry.
+	registry = &updater.ResourceRegistry{
+		Name: "updates",
+		UpdateURLs: []string{
+			"https://updates.safing.io",
+		},
+		DevMode: false,
+		Online:  false, // disable download of resources (this is job for the core).
+	}
+)
+
+const query = "query "
+
+func init() {
+	flag.StringVar(&dataDir, "data", "", "set data directory")
+	flag.BoolVar(&printStackOnExit, "print-stack-on-exit", false, "prints the stack before of shutting down")
+	flag.BoolVar(&showVersion, "version", false, "show version and exit")
+
+	runtime.GOMAXPROCS(2)
+}
+
+func main() {
+	// parse flags
+	flag.Parse()
+
+	// set meta info
+	info.Set("Portmaster Notifier", "0.3.6", "GPLv3")
+
+	// check if meta info is ok
+	err := info.CheckVersion()
+	if err != nil {
+		fmt.Println("compile error: please compile using the provided build script")
+		os.Exit(1)
+	}
+
+	// print help
+	// if modules.HelpFlag {
+	// 	flag.Usage()
+	// 	os.Exit(0)
+	// }
+
+	if showVersion {
+		fmt.Println(info.FullVersion())
+		os.Exit(0)
+	}
+
+	// auto detect
+	if dataDir == "" {
+		dataDir = detectDataDir()
+	}
+
+	// check data dir
+	if dataDir == "" {
+		fmt.Fprintln(os.Stderr, "please set the data directory using --data=/path/to/data/dir")
+		os.Exit(1)
+	}
+
+	// switch to safe exec dir
+	err = os.Chdir(filepath.Join(dataDir, "exec"))
+	if err != nil {
+		fmt.Fprintf(os.Stderr, "warning: failed to switch to safe exec dir: %s\n", err)
+	}
+
+	// start log writer
+	err = log.Start()
+	if err != nil {
+		fmt.Fprintf(os.Stderr, "failed to start logging: %s\n", err)
+		os.Exit(1)
+	}
+
+	// load registry
+	err = configureRegistry(true)
+	if err != nil {
+		fmt.Fprintf(os.Stderr, "failed to load registry: %s\n", err)
+		os.Exit(1)
+	}
+
+	// connect to API
+	go apiClient.StayConnected()
+	go apiStatusMonitor()
+
+	// start subsystems
+	go tray()
+	go subsystemsClient()
+	go spnStatusClient()
+	go notifClient()
+	go startShutdownEventListener()
+
+	// Shutdown
+	// catch interrupt for clean shutdown
+	signalCh := make(chan os.Signal, 1)
+	signal.Notify(
+		signalCh,
+		os.Interrupt,
+		syscall.SIGHUP,
+		syscall.SIGINT,
+		syscall.SIGTERM,
+		syscall.SIGQUIT,
+	)
+
+	// wait for shutdown
+	select {
+	case <-signalCh:
+		fmt.Println(" <INTERRUPT>")
+		log.Warning("program was interrupted, shutting down")
+	case <-mainCtx.Done():
+		log.Warning("program is shutting down")
+	}
+
+	if printStackOnExit {
+		fmt.Println("=== PRINTING STACK ===")
+		_ = pprof.Lookup("goroutine").WriteTo(os.Stdout, 2)
+		fmt.Println("=== END STACK ===")
+	}
+	go func() {
+		time.Sleep(10 * time.Second)
+		fmt.Println("===== TAKING TOO LONG FOR SHUTDOWN - PRINTING STACK TRACES =====")
+		_ = pprof.Lookup("goroutine").WriteTo(os.Stdout, 2)
+		os.Exit(1)
+	}()
+
+	// clear all notifications
+	clearNotifications()
+
+	// shutdown
+	cancelMainCtx()
+	mainWg.Wait()
+
+	apiClient.Shutdown()
+	exitTray()
+	log.Shutdown()
+
+	os.Exit(0)
+}
+
+func apiStatusMonitor() {
+	for {
+		// Wait for connection.
+		<-apiClient.Online()
+		connected.Set()
+		triggerTrayUpdate()
+
+		// Wait for lost connection.
+		<-apiClient.Offline()
+		connected.UnSet()
+		triggerTrayUpdate()
+	}
+}
+
+func detectDataDir() string {
+	// get path of executable
+	binPath, err := os.Executable()
+	if err != nil {
+		return ""
+	}
+	// get directory
+	binDir := filepath.Dir(binPath)
+	// check if we in the updates directory
+	identifierDir := filepath.Join("updates", runtime.GOOS+"_"+runtime.GOARCH, "notifier")
+	// check if there is a match and return data dir
+	if strings.HasSuffix(binDir, identifierDir) {
+		return filepath.Clean(strings.TrimSuffix(binDir, identifierDir))
+	}
+	return ""
+}
+
+func configureRegistry(mustLoadIndex bool) error {
+	// If dataDir is not set, check the environment variable.
+	if dataDir == "" {
+		dataDir = os.Getenv("PORTMASTER_DATA")
+	}
+
+	// If it's still empty, try to auto-detect it.
+	if dataDir == "" {
+		dataDir = detectInstallationDir()
+	}
+
+	// Finally, if it's still empty, the user must provide it.
+	if dataDir == "" {
+		return errors.New("please set the data directory using --data=/path/to/data/dir")
+	}
+
+	// Remove left over quotes.
+	dataDir = strings.Trim(dataDir, `\"`)
+	// Initialize data root.
+	err := dataroot.Initialize(dataDir, 0o0755)
+	if err != nil {
+		return fmt.Errorf("failed to initialize data root: %w", err)
+	}
+	dataRoot = dataroot.Root()
+
+	// Initialize registry.
+	err = registry.Initialize(dataRoot.ChildDir("updates", 0o0755))
+	if err != nil {
+		return err
+	}
+
+	return updateRegistryIndex(mustLoadIndex)
+}
+
+func detectInstallationDir() string {
+	exePath, err := filepath.Abs(os.Args[0])
+	if err != nil {
+		return ""
+	}
+
+	parent := filepath.Dir(exePath)                                    // parent should be "...\updates\windows_amd64\notifier"
+	stableJSONFile := filepath.Join(parent, "..", "..", "stable.json") // "...\updates\stable.json"
+	stat, err := os.Stat(stableJSONFile)
+	if err != nil {
+		return ""
+	}
+
+	if stat.IsDir() {
+		return ""
+	}
+
+	return parent
+}
+
+func updateRegistryIndex(mustLoadIndex bool) error {
+	// Set indexes based on the release channel.
+	warning := helper.SetIndexes(registry, "", false, false, false)
+	if warning != nil {
+		log.Warningf("%q", warning)
+	}
+
+	// Load indexes from disk or network, if needed and desired.
+	err := registry.LoadIndexes(context.Background())
+	if err != nil {
+		log.Warningf("error loading indexes %q", warning)
+		if mustLoadIndex {
+			return err
+		}
+	}
+
+	// Load versions from disk to know which others we have and which are available.
+	err = registry.ScanStorage("")
+	if err != nil {
+		log.Warningf("error during storage scan: %q\n", err)
+	}
+
+	registry.SelectVersions()
+	return nil
+}
diff --git a/cmds/notifier/notification.go b/cmds/notifier/notification.go
new file mode 100644
index 00000000..0f6ded8d
--- /dev/null
+++ b/cmds/notifier/notification.go
@@ -0,0 +1,35 @@
+package main
+
+import (
+	"fmt"
+
+	pbnotify "github.com/safing/portmaster/base/notifications"
+)
+
+// Notification represents a notification that is to be delivered to the user.
+type Notification struct {
+	pbnotify.Notification
+
+	// systemID holds the ID returned by the dbus interface on Linux or by WinToast library on Windows.
+	systemID NotificationID
+}
+
+// IsSupportedAction returns whether the action is supported on this system.
+func IsSupportedAction(a pbnotify.Action) bool {
+	switch a.Type {
+	case pbnotify.ActionTypeNone:
+		return true
+	default:
+		return false
+	}
+}
+
+// SelectAction sends an action back to the portmaster.
+func (n *Notification) SelectAction(action string) {
+	upd := &pbnotify.Notification{
+		EventID:          n.EventID,
+		SelectedActionID: action,
+	}
+
+	_ = apiClient.Update(fmt.Sprintf("%s%s", dbNotifBasePath, upd.EventID), upd, nil)
+}
diff --git a/cmds/notifier/notify.go b/cmds/notifier/notify.go
new file mode 100644
index 00000000..48e117c0
--- /dev/null
+++ b/cmds/notifier/notify.go
@@ -0,0 +1,102 @@
+package main
+
+import (
+	"fmt"
+	"strings"
+	"sync"
+	"time"
+
+	"github.com/safing/portmaster/base/api/client"
+	"github.com/safing/portmaster/base/log"
+	pbnotify "github.com/safing/portmaster/base/notifications"
+	"github.com/safing/structures/dsd"
+)
+
+const (
+	dbNotifBasePath = "notifications:all/"
+)
+
+var (
+	notifications     = make(map[string]*Notification)
+	notificationsLock sync.Mutex
+)
+
+func notifClient() {
+	notifOp := apiClient.Qsub(fmt.Sprintf("query %s where ShowOnSystem is true", dbNotifBasePath), handleNotification)
+	notifOp.EnableResuscitation()
+
+	// start the action listener and block
+	// until it's closed.
+	actionListener()
+}
+
+func handleNotification(m *client.Message) {
+	notificationsLock.Lock()
+	defer notificationsLock.Unlock()
+
+	log.Tracef("received %s msg: %s", m.Type, m.Key)
+
+	switch m.Type {
+	case client.MsgError:
+	case client.MsgDone:
+	case client.MsgSuccess:
+	case client.MsgOk, client.MsgUpdate, client.MsgNew:
+
+		n := &Notification{}
+		_, err := dsd.Load(m.RawValue, &n.Notification)
+		if err != nil {
+			log.Warningf("notify: failed to parse new notification: %s", err)
+			return
+		}
+
+		// copy existing system values
+		existing, ok := notifications[n.EventID]
+		if ok {
+			existing.Lock()
+			n.systemID = existing.systemID
+			existing.Unlock()
+		}
+
+		// save
+		notifications[n.EventID] = n
+
+		// Handle notification.
+		switch {
+		case existing != nil:
+			// Cancel existing notification if not active, else ignore.
+			if n.State != pbnotify.Active {
+				existing.Cancel()
+			}
+			return
+		case n.State == pbnotify.Active:
+			// Show new notifications that are active.
+			n.Show()
+		default:
+			// Ignore new notifications that are not active.
+		}
+
+	case client.MsgDelete:
+
+		n, ok := notifications[strings.TrimPrefix(m.Key, dbNotifBasePath)]
+		if ok {
+			n.Cancel()
+			delete(notifications, n.EventID)
+		}
+
+	case client.MsgWarning:
+	case client.MsgOffline:
+	}
+}
+
+func clearNotifications() {
+	notificationsLock.Lock()
+	defer notificationsLock.Unlock()
+
+	for _, n := range notifications {
+		n.Cancel()
+	}
+
+	// Wait for goroutines that cancel notifications.
+	// TODO: Revamp to use a waitgroup.
+	time.Sleep(1 * time.Second)
+}
diff --git a/cmds/notifier/notify_linux.go b/cmds/notifier/notify_linux.go
new file mode 100644
index 00000000..80cc8e15
--- /dev/null
+++ b/cmds/notifier/notify_linux.go
@@ -0,0 +1,160 @@
+package main
+
+import (
+	"context"
+	"errors"
+	"sync"
+
+	notify "github.com/dhaavi/go-notify"
+
+	"github.com/safing/portmaster/base/log"
+)
+
+type NotificationID uint32
+
+var (
+	capabilities notify.Capabilities
+	notifsByID   sync.Map
+)
+
+func init() {
+	var err error
+	capabilities, err = notify.GetCapabilities()
+	if err != nil {
+		log.Errorf("failed to get notification system capabilities: %s", err)
+	}
+}
+
+func handleActions(ctx context.Context, actions chan notify.Signal) {
+	mainWg.Add(1)
+	defer mainWg.Done()
+
+listenForNotifications:
+	for {
+		select {
+		case <-ctx.Done():
+			return
+		case sig := <-actions:
+			if sig.Name != "org.freedesktop.Notifications.ActionInvoked" {
+				// we don't care for anything else (dismissed, closed)
+				continue listenForNotifications
+			}
+
+			// get notification by system ID
+			n, ok := notifsByID.LoadAndDelete(NotificationID(sig.ID))
+
+			if !ok {
+				continue listenForNotifications
+			}
+
+			notification, ok := n.(*Notification)
+			if !ok {
+				log.Errorf("received invalid notification type %T", n)
+
+				continue listenForNotifications
+			}
+
+			log.Tracef("notify: received signal: %+v", sig)
+			if sig.ActionKey != "" {
+				// send action
+				if ok {
+					notification.Lock()
+					notification.SelectAction(sig.ActionKey)
+					notification.Unlock()
+				}
+			} else {
+				log.Tracef("notify: notification clicked: %+v", sig)
+				// Global action invoked, start the app
+				launchApp()
+			}
+		}
+	}
+}
+
+func actionListener() {
+	actions := make(chan notify.Signal, 100)
+
+	go handleActions(mainCtx, actions)
+
+	err := notify.SignalNotify(mainCtx, actions)
+	if err != nil && errors.Is(err, context.Canceled) {
+		log.Errorf("notify: signal listener failed: %s", err)
+	}
+}
+
+// Show shows the notification.
+func (n *Notification) Show() {
+	sysN := notify.NewNotification("Portmaster", n.Message)
+	// see https://developer.gnome.org/notification-spec/
+
+	// The optional name of the application sending the notification.
+	// Can be blank.
+	sysN.AppName = "Portmaster"
+
+	// The optional notification ID that this notification replaces.
+	sysN.ReplacesID = uint32(n.systemID)
+
+	// The optional program icon of the calling application.
+	// sysN.AppIcon string
+
+	// The summary text briefly describing the notification.
+	// Summary string (arg 1)
+
+	// The optional detailed body text.
+	// Body string (arg 2)
+
+	// The actions send a request message back to the notification client
+	// when invoked.
+	// sysN.Actions []string
+	if capabilities.Actions {
+		sysN.Actions = make([]string, 0, len(n.AvailableActions)*2)
+		for _, action := range n.AvailableActions {
+			if IsSupportedAction(*action) {
+				sysN.Actions = append(sysN.Actions, action.ID)
+				sysN.Actions = append(sysN.Actions, action.Text)
+			}
+		}
+	}
+
+	// Set Portmaster icon.
+	iconLocation, err := ensureAppIcon()
+	if err != nil {
+		log.Warningf("notify: failed to write icon: %s", err)
+	}
+	sysN.AppIcon = iconLocation
+
+	// TODO: Use hints to display icon of affected app.
+	// Hints are a way to provide extra data to a notification server.
+	// sysN.Hints = make(map[string]interface{})
+
+	// The timeout time in milliseconds since the display of the
+	// notification at which the notification should automatically close.
+	// sysN.Timeout int32
+
+	newID, err := sysN.Show()
+	if err != nil {
+		log.Warningf("notify: failed to show notification %s", n.EventID)
+		return
+	}
+
+	notifsByID.Store(NotificationID(newID), n)
+
+	n.Lock()
+	defer n.Unlock()
+	n.systemID = NotificationID(newID)
+}
+
+// Cancel cancels the notification.
+func (n *Notification) Cancel() {
+	n.Lock()
+	defer n.Unlock()
+
+	// TODO: could a ID of 0 be valid?
+	if n.systemID != 0 {
+		err := notify.CloseNotification(uint32(n.systemID))
+		if err != nil {
+			log.Warningf("notify: failed to close notification %s/%d", n.EventID, n.systemID)
+		}
+		notifsByID.Delete(n.systemID)
+	}
+}
diff --git a/cmds/notifier/notify_windows.go b/cmds/notifier/notify_windows.go
new file mode 100644
index 00000000..98cf987a
--- /dev/null
+++ b/cmds/notifier/notify_windows.go
@@ -0,0 +1,184 @@
+package main
+
+import (
+	"fmt"
+	"sync"
+
+	"github.com/safing/portmaster/base/log"
+	"github.com/safing/portmaster/cmds/notifier/wintoast"
+	"github.com/safing/portmaster/service/updates/helper"
+)
+
+type NotificationID int64
+
+const (
+	appName              = "Portmaster"
+	appUserModelID       = "io.safing.portmaster.2"
+	originalShortcutPath = "C:\\ProgramData\\Microsoft\\Windows\\Start Menu\\Programs\\Portmaster\\Portmaster.lnk"
+)
+
+const (
+	SoundDefault = 0
+	SoundSilent  = 1
+	SoundLoop    = 2
+)
+
+const (
+	SoundPathDefault = 0
+	// see notification_glue.h if you need more types
+)
+
+var (
+	initOnce           sync.Once
+	lib                *wintoast.WinToast
+	notificationsByIDs sync.Map
+)
+
+func getLib() *wintoast.WinToast {
+	initOnce.Do(func() {
+		dllPath, err := getDllPath()
+		if err != nil {
+			log.Errorf("notify: failed to get dll path: %s", err)
+			return
+		}
+		// Load dll and all the functions
+		newLib, err := wintoast.New(dllPath)
+		if err != nil {
+			log.Errorf("notify: failed to load library: %s", err)
+			return
+		}
+
+		// Initialize. This will create or update application shortcut. C:\Users\<user>\AppData\Roaming\Microsoft\Windows\Start Menu\Programs
+		// and it will be of the originalShortcutPath with no CLSID and different AUMI
+		err = newLib.Initialize(appName, appUserModelID, originalShortcutPath)
+		if err != nil {
+			log.Errorf("notify: failed to load library: %s", err)
+			return
+		}
+
+		// library was initialized successfully
+		lib = newLib
+
+		// Set callbacks
+
+		err = lib.SetCallbacks(notificationActivatedCallback, notificationDismissedCallback, notificationDismissedCallback)
+		if err != nil {
+			log.Warningf("notify: failed to set callbacks: %s", err)
+			return
+		}
+	})
+
+	return lib
+}
+
+// Show shows the notification.
+func (n *Notification) Show() {
+	// Lock notification
+	n.Lock()
+	defer n.Unlock()
+
+	// Create new notification object
+	builder, err := getLib().NewNotification(n.Title, n.Message)
+	if err != nil {
+		log.Errorf("notify: failed to create notification: %s", err)
+		return
+	}
+	// Make sure memory is freed when done
+	defer builder.Delete()
+
+	// if needed set notification icon
+	// _ = builder.SetImage(iconLocation)
+
+	// Leaving the default value for the sound
+	// _ = builder.SetSound(SoundDefault, SoundPathDefault)
+
+	// Set all the required actions.
+	for _, action := range n.AvailableActions {
+		err = builder.AddButton(action.Text)
+		if err != nil {
+			log.Warningf("notify: failed to add button: %s", err)
+		}
+	}
+
+	// Show notification.
+	id, err := builder.Show()
+	if err != nil {
+		log.Errorf("notify: failed to show notification: %s", err)
+		return
+	}
+	n.systemID = NotificationID(id)
+
+	// Link system id to the notification object
+	notificationsByIDs.Store(NotificationID(id), n)
+
+	log.Debugf("notify: showing notification %q: %d", n.Title, n.systemID)
+}
+
+// Cancel cancels the notification.
+func (n *Notification) Cancel() {
+	// Lock notification
+	n.Lock()
+	defer n.Unlock()
+
+	// No need to check for errors. If it fails it is probably already dismissed
+	_ = getLib().HideNotification(int64(n.systemID))
+
+	notificationsByIDs.Delete(n.systemID)
+	log.Debugf("notify: notification canceled %q: %d", n.Title, n.systemID)
+}
+
+func notificationActivatedCallback(id int64, actionIndex int32) {
+	if actionIndex == -1 {
+		// The user clicked on the notification (not a button), open the portmaster and delete
+		launchApp()
+		notificationsByIDs.Delete(NotificationID(id))
+		log.Debugf("notify: notification clicked %d", id)
+		return
+	}
+
+	// The user click one of the buttons
+
+	// Get notified object
+	n, ok := notificationsByIDs.LoadAndDelete(NotificationID(id))
+	if !ok {
+		return
+	}
+
+	notification := n.(*Notification)
+
+	notification.Lock()
+	defer notification.Unlock()
+
+	// Set selected action
+	actionID := notification.AvailableActions[actionIndex].ID
+	notification.SelectAction(actionID)
+
+	log.Debugf("notify: notification button cliecked %d button id: %d", id, actionIndex)
+}
+
+func notificationDismissedCallback(id int64, reason int32) {
+	// Failure or user dismissed the notification
+	if reason == 0 {
+		notificationsByIDs.Delete(NotificationID(id))
+		log.Debugf("notify: notification dissmissed %d", id)
+	}
+}
+
+func getDllPath() (string, error) {
+	if dataDir == "" {
+		return "", fmt.Errorf("dataDir is empty")
+	}
+
+	// Aks the registry for the dll path
+	identifier := helper.PlatformIdentifier("notifier/portmaster-wintoast.dll")
+	file, err := registry.GetFile(identifier)
+	if err != nil {
+		return "", err
+	}
+	return file.Path(), nil
+}
+
+func actionListener() {
+	// initialize the library
+	_ = getLib()
+}
diff --git a/cmds/notifier/shutdown.go b/cmds/notifier/shutdown.go
new file mode 100644
index 00000000..70b2e6d8
--- /dev/null
+++ b/cmds/notifier/shutdown.go
@@ -0,0 +1,50 @@
+package main
+
+import (
+	"github.com/safing/portmaster/base/api/client"
+	"github.com/safing/portmaster/base/log"
+)
+
+func startShutdownEventListener() {
+	shutdownNotifOp := apiClient.Sub("query runtime:modules/core/event/shutdown", handleShutdownEvent)
+	shutdownNotifOp.EnableResuscitation()
+
+	restartNotifOp := apiClient.Sub("query runtime:modules/core/event/restart", handleRestartEvent)
+	restartNotifOp.EnableResuscitation()
+}
+
+func handleShutdownEvent(m *client.Message) {
+	switch m.Type {
+	case client.MsgOk, client.MsgUpdate, client.MsgNew:
+		shuttingDown.Set()
+		triggerTrayUpdate()
+
+		log.Warningf("shutdown: received shutdown event, shutting down now")
+
+		// wait for the API client connection to die
+		<-apiClient.Offline()
+		shuttingDown.UnSet()
+
+		cancelMainCtx()
+
+	case client.MsgWarning, client.MsgError:
+		log.Errorf("shutdown: event subscription error: %s", string(m.RawValue))
+	}
+}
+
+func handleRestartEvent(m *client.Message) {
+	switch m.Type {
+	case client.MsgOk, client.MsgUpdate, client.MsgNew:
+		restarting.Set()
+		triggerTrayUpdate()
+
+		log.Warningf("restart: received restart event")
+
+		// wait for the API client connection to die
+		<-apiClient.Offline()
+		restarting.UnSet()
+		triggerTrayUpdate()
+	case client.MsgWarning, client.MsgError:
+		log.Errorf("shutdown: event subscription error: %s", string(m.RawValue))
+	}
+}
diff --git a/cmds/notifier/snoretoast-guid.patch b/cmds/notifier/snoretoast-guid.patch
new file mode 100644
index 00000000..1a050e5f
--- /dev/null
+++ b/cmds/notifier/snoretoast-guid.patch
@@ -0,0 +1,15 @@
+diff --git a/CMakeLists.txt b/CMakeLists.txt
+index 498226a..446ba5e 100644
+--- a/CMakeLists.txt
++++ b/CMakeLists.txt
+@@ -2,7 +2,9 @@ cmake_minimum_required(VERSION 3.4)
+ 
+ project(snoretoast VERSION 0.6.0)
+ # Always change the guid when the version is changed SNORETOAST_CALLBACK_GUID
+-set(SNORETOAST_CALLBACK_GUID eb1fdd5b-8f70-4b5a-b230-998a2dc19303)
++#We keep it fixed!
++set(SNORETOAST_CALLBACK_GUID 7F00FB48-65D5-4BA8-A35B-F194DA7E1A51)
++
+ 
+ set(CMAKE_MODULE_PATH ${CMAKE_CURRENT_SOURCE_DIR}/cmake/)
+ 
diff --git a/cmds/notifier/spn.go b/cmds/notifier/spn.go
new file mode 100644
index 00000000..0b49d63c
--- /dev/null
+++ b/cmds/notifier/spn.go
@@ -0,0 +1,104 @@
+package main
+
+import (
+	"sync"
+	"time"
+
+	"github.com/tevino/abool"
+
+	"github.com/safing/portmaster/base/api/client"
+	"github.com/safing/portmaster/base/log"
+	"github.com/safing/structures/dsd"
+)
+
+const (
+	spnModuleKey = "config:spn/enable"
+	spnStatusKey = "runtime:spn/status"
+)
+
+var (
+	spnEnabled = abool.New()
+
+	spnStatusCache     *SPNStatus
+	spnStatusCacheLock sync.Mutex
+)
+
+// SPNStatus holds SPN status information.
+type SPNStatus struct {
+	Status             string
+	HomeHubID          string
+	HomeHubName        string
+	ConnectedIP        string
+	ConnectedTransport string
+	ConnectedSince     *time.Time
+}
+
+// GetSPNStatus returns the SPN status.
+func GetSPNStatus() *SPNStatus {
+	spnStatusCacheLock.Lock()
+	defer spnStatusCacheLock.Unlock()
+
+	return spnStatusCache
+}
+
+func updateSPNStatus(s *SPNStatus) {
+	spnStatusCacheLock.Lock()
+	defer spnStatusCacheLock.Unlock()
+
+	spnStatusCache = s
+}
+
+func spnStatusClient() {
+	moduleQueryOp := apiClient.Qsub(query+spnModuleKey, handleSPNModuleUpdate)
+	moduleQueryOp.EnableResuscitation()
+
+	statusQueryOp := apiClient.Qsub(query+spnStatusKey, handleSPNStatusUpdate)
+	statusQueryOp.EnableResuscitation()
+}
+
+func handleSPNModuleUpdate(m *client.Message) {
+	switch m.Type {
+	case client.MsgOk, client.MsgUpdate, client.MsgNew:
+		var cfg struct {
+			Value bool `json:"Value"`
+		}
+		_, err := dsd.Load(m.RawValue, &cfg)
+		if err != nil {
+			log.Warningf("config: failed to parse config: %s", err)
+			return
+		}
+		log.Infof("config: received update to SPN module: enabled=%v", cfg.Value)
+
+		spnEnabled.SetTo(cfg.Value)
+		triggerTrayUpdate()
+
+	default:
+	}
+}
+
+func handleSPNStatusUpdate(m *client.Message) {
+	switch m.Type {
+	case client.MsgOk, client.MsgUpdate, client.MsgNew:
+		newStatus := &SPNStatus{}
+		_, err := dsd.Load(m.RawValue, newStatus)
+		if err != nil {
+			log.Warningf("config: failed to parse config: %s", err)
+			return
+		}
+		log.Infof("config: received update to SPN status: %+v", newStatus)
+
+		updateSPNStatus(newStatus)
+		triggerTrayUpdate()
+
+	default:
+	}
+}
+
+func ToggleSPN() {
+	var cfg struct {
+		Value bool `json:"Value"`
+	}
+	cfg.Value = !spnEnabled.IsSet()
+
+	apiClient.Update(spnModuleKey, &cfg, nil)
+}
diff --git a/cmds/notifier/subsystems.go b/cmds/notifier/subsystems.go
new file mode 100644
index 00000000..587d6d84
--- /dev/null
+++ b/cmds/notifier/subsystems.go
@@ -0,0 +1,121 @@
+package main
+
+import (
+	"sync"
+
+	"github.com/safing/portmaster/base/api/client"
+	"github.com/safing/portmaster/base/log"
+	"github.com/safing/structures/dsd"
+)
+
+const (
+	subsystemsKeySpace = "runtime:subsystems/"
+
+	// Module Failure Status Values
+	// FailureNone    = 0 // unused
+	// FailureHint    = 1 // unused.
+	FailureWarning = 2
+	FailureError   = 3
+)
+
+var (
+	subsystems     = make(map[string]*Subsystem)
+	subsystemsLock sync.Mutex
+)
+
+// Subsystem describes a subset of modules that represent a part of a
+// service or program to the user. Subsystems can be (de-)activated causing
+// all related modules to be brought down or up.
+type Subsystem struct { //nolint:maligned // not worth the effort
+	// ID is a unique identifier for the subsystem.
+	ID string
+
+	// Name holds a human readable name of the subsystem.
+	Name string
+
+	// Description may holds an optional description of
+	// the subsystem's purpose.
+	Description string
+
+	// Modules contains all modules that are related to the subsystem.
+	// Note that this slice also contains a reference to the subsystem
+	// module itself.
+	Modules []*ModuleStatus
+
+	// FailureStatus is the worst failure status that is currently
+	// set in one of the subsystem's dependencies.
+	FailureStatus uint8
+}
+
+// ModuleStatus describes the status of a module.
+type ModuleStatus struct {
+	Name          string
+	Enabled       bool
+	Status        uint8
+	FailureStatus uint8
+	FailureID     string
+	FailureMsg    string
+}
+
+// GetFailure returns the worst of all subsystem failures.
+func GetFailure() (failureStatus uint8, failureMsg string) {
+	subsystemsLock.Lock()
+	defer subsystemsLock.Unlock()
+
+	for _, subsystem := range subsystems {
+		for _, module := range subsystem.Modules {
+			if failureStatus < module.FailureStatus {
+				failureStatus = module.FailureStatus
+				failureMsg = module.FailureMsg
+			}
+		}
+	}
+
+	return
+}
+
+func updateSubsystem(s *Subsystem) {
+	subsystemsLock.Lock()
+	defer subsystemsLock.Unlock()
+
+	subsystems[s.ID] = s
+}
+
+func clearSubsystems() {
+	subsystemsLock.Lock()
+	defer subsystemsLock.Unlock()
+
+	for key := range subsystems {
+		delete(subsystems, key)
+	}
+}
+
+func subsystemsClient() {
+	subsystemsOp := apiClient.Qsub("query "+subsystemsKeySpace, handleSubsystem)
+	subsystemsOp.EnableResuscitation()
+}
+
+func handleSubsystem(m *client.Message) {
+	switch m.Type {
+	case client.MsgError:
+	case client.MsgDone:
+	case client.MsgSuccess:
+	case client.MsgOk, client.MsgUpdate, client.MsgNew:
+
+		newSubsystem := &Subsystem{}
+		_, err := dsd.Load(m.RawValue, newSubsystem)
+		if err != nil {
+			log.Warningf("subsystems: failed to parse new subsystem: %s", err)
+			return
+		}
+		updateSubsystem(newSubsystem)
+		triggerTrayUpdate()
+
+	case client.MsgDelete:
+	case client.MsgWarning:
+	case client.MsgOffline:
+
+		clearSubsystems()
+
+	}
+}
diff --git a/cmds/notifier/tray.go b/cmds/notifier/tray.go
new file mode 100644
index 00000000..abdf48d5
--- /dev/null
+++ b/cmds/notifier/tray.go
@@ -0,0 +1,217 @@
+package main
+
+import (
+	"flag"
+	"os/exec"
+	"path/filepath"
+	"runtime"
+	"strings"
+	"sync"
+	"time"
+
+	"fyne.io/systray"
+
+	icons "github.com/safing/portmaster/assets"
+	"github.com/safing/portmaster/base/log"
+)
+
+const (
+	shortenStatusMsgTo = 40
+)
+
+var (
+	trayLock sync.Mutex
+
+	scaleColoredIconsTo int
+
+	activeIconID    int = -1
+	activeStatusMsg     = ""
+	activeSPNStatus     = ""
+	activeSPNSwitch     = ""
+
+	menuItemStatusMsg *systray.MenuItem
+	menuItemSPNStatus *systray.MenuItem
+	menuItemSPNSwitch *systray.MenuItem
+)
+
+func init() {
+	flag.IntVar(&scaleColoredIconsTo, "scale-icons", 32, "scale colored icons to given size in pixels")
+
+	// lock until ready
+	trayLock.Lock()
+}
+
+func tray() {
+	if scaleColoredIconsTo > 0 {
+		icons.ScaleColoredIconsTo(scaleColoredIconsTo)
+	}
+
+	systray.Run(onReady, onExit)
+}
+
+func exitTray() {
+	systray.Quit()
+}
+
+func onReady() {
+	// unlock when ready
+	defer trayLock.Unlock()
+
+	// icon
+	systray.SetIcon(icons.ColoredIcons[icons.RedID])
+	if runtime.GOOS == "windows" {
+		// systray.SetTitle("Portmaster Notifier") // Don't set title, as it may be displayed in full in the menu/tray bar. (Ubuntu)
+		systray.SetTooltip("Portmaster Notifier")
+	}
+
+	// menu: open app
+	if dataDir != "" {
+		menuItemOpenApp := systray.AddMenuItem("Open App", "")
+		go clickListener(menuItemOpenApp, launchApp)
+		systray.AddSeparator()
+	}
+
+	// menu: status
+
+	menuItemStatusMsg = systray.AddMenuItem("Loading...", "")
+	menuItemStatusMsg.Disable()
+	systray.AddSeparator()
+
+	// menu: SPN
+
+	menuItemSPNStatus = systray.AddMenuItem("Loading...", "")
+	menuItemSPNStatus.Disable()
+	menuItemSPNSwitch = systray.AddMenuItem("Loading...", "")
+	go clickListener(menuItemSPNSwitch, func() {
+		ToggleSPN()
+	})
+	systray.AddSeparator()
+
+	// menu: quit
+	systray.AddSeparator()
+	closeTray := systray.AddMenuItem("Close Tray Notifier", "")
+	go clickListener(closeTray, func() {
+		cancelMainCtx()
+	})
+	shutdownPortmaster := systray.AddMenuItem("Shut Down Portmaster", "")
+	go clickListener(shutdownPortmaster, func() {
+		_ = TriggerShutdown()
+		time.Sleep(1 * time.Second)
+		cancelMainCtx()
+	})
+}
+
+func onExit() {
+}
+
+func triggerTrayUpdate() {
+	// TODO: Deduplicate triggers.
+	go updateTray()
+}
+
+// updateTray update the state of the tray depending on the currently available information.
+func updateTray() {
+	// Get current information.
+	spnStatus := GetSPNStatus()
+	failureID, failureMsg := GetFailure()
+
+	trayLock.Lock()
+	defer trayLock.Unlock()
+
+	// Select icon and status message to show.
+	newIconID := icons.GreenID
+	newStatusMsg := "Secure"
+	switch {
+	case shuttingDown.IsSet():
+		newIconID = icons.RedID
+		newStatusMsg = "Shutting Down Portmaster"
+
+	case restarting.IsSet():
+		newIconID = icons.YellowID
+		newStatusMsg = "Restarting Portmaster"
+
+	case !connected.IsSet():
+		newIconID = icons.RedID
+		newStatusMsg = "Waiting for Portmaster Core Service"
+
+	case failureID == FailureError:
+		newIconID = icons.RedID
+		newStatusMsg = failureMsg
+
+	case failureID == FailureWarning:
+		newIconID = icons.YellowID
+		newStatusMsg = failureMsg
+
+	case spnEnabled.IsSet():
+		newIconID = icons.BlueID
+	}
+
+	// Set icon if changed.
+	if newIconID != activeIconID {
+		activeIconID = newIconID
+		systray.SetIcon(icons.ColoredIcons[activeIconID])
+	}
+
+	// Set message if changed.
+	if newStatusMsg != activeStatusMsg {
+		activeStatusMsg = newStatusMsg
+
+		// Shorten message if too long.
+		shortenedMsg := activeStatusMsg
+		if len(shortenedMsg) > shortenStatusMsgTo && strings.Contains(shortenedMsg, ". ") {
+			shortenedMsg = strings.SplitN(shortenedMsg, ". ", 2)[0]
+		}
+		if len(shortenedMsg) > shortenStatusMsgTo {
+			shortenedMsg = shortenedMsg[:shortenStatusMsgTo] + "..."
+		}
+
+		menuItemStatusMsg.SetTitle("Status: " + shortenedMsg)
+	}
+
+	// Set SPN status if changed.
+	if spnStatus != nil && activeSPNStatus != spnStatus.Status {
+		activeSPNStatus = spnStatus.Status
+		menuItemSPNStatus.SetTitle("SPN: " + strings.Title(activeSPNStatus)) // nolint:staticcheck
+	}
+
+	// Set SPN switch if changed.
+	newSPNSwitch := "Enable SPN"
+	if spnEnabled.IsSet() {
+		newSPNSwitch = "Disable SPN"
+	}
+	if activeSPNSwitch != newSPNSwitch {
+		activeSPNSwitch = newSPNSwitch
+		menuItemSPNSwitch.SetTitle(activeSPNSwitch)
+	}
+}
+
+func clickListener(item *systray.MenuItem, fn func()) {
+	for range item.ClickedCh {
+		fn()
+	}
+}
+
+func launchApp() {
+	// build path to app
+	pmStartPath := filepath.Join(dataDir, "portmaster-start")
+	if runtime.GOOS == "windows" {
+		pmStartPath += ".exe"
+	}
+
+	// start app
+	cmd := exec.Command(pmStartPath, "app", "--data", dataDir)
+	err := cmd.Start()
+	if err != nil {
+		log.Warningf("failed to start app: %s", err)
+		return
+	}
+
+	// Use cmd.Wait() instead of cmd.Process.Release() to properly release its resources.
+	// See https://github.com/golang/go/issues/36534
+	go func() {
+		err := cmd.Wait()
+		if err != nil {
+			log.Warningf("failed to wait/release app process: %s", err)
+		}
+	}()
+}
diff --git a/cmds/notifier/wintoast/notification_builder.go b/cmds/notifier/wintoast/notification_builder.go
new file mode 100644
index 00000000..89eca798
--- /dev/null
+++ b/cmds/notifier/wintoast/notification_builder.go
@@ -0,0 +1,90 @@
+//go:build windows
+
+package wintoast
+
+import (
+	"unsafe"
+
+	"golang.org/x/sys/windows"
+)
+
+type NotificationBuilder struct {
+	templatePointer uintptr
+	lib             *WinToast
+}
+
+func newNotification(lib *WinToast, title string, message string) (*NotificationBuilder, error) {
+	lib.Lock()
+	defer lib.Unlock()
+
+	titleUTF, _ := windows.UTF16PtrFromString(title)
+	messageUTF, _ := windows.UTF16PtrFromString(message)
+	titleP := unsafe.Pointer(titleUTF)
+	messageP := unsafe.Pointer(messageUTF)
+
+	ptr, _, err := lib.createNotification.Call(uintptr(titleP), uintptr(messageP))
+	if ptr == 0 {
+		return nil, err
+	}
+
+	return &NotificationBuilder{ptr, lib}, nil
+}
+
+func (n *NotificationBuilder) Delete() {
+	if n == nil {
+		return
+	}
+
+	n.lib.Lock()
+	defer n.lib.Unlock()
+
+	_, _, _ = n.lib.deleteNotification.Call(n.templatePointer)
+}
+
+func (n *NotificationBuilder) AddButton(text string) error {
+	n.lib.Lock()
+	defer n.lib.Unlock()
+	textUTF, _ := windows.UTF16PtrFromString(text)
+	textP := unsafe.Pointer(textUTF)
+
+	rc, _, err := n.lib.addButton.Call(n.templatePointer, uintptr(textP))
+	if rc != 1 {
+		return err
+	}
+	return nil
+}
+
+func (n *NotificationBuilder) SetImage(iconPath string) error {
+	n.lib.Lock()
+	defer n.lib.Unlock()
+	pathUTF, _ := windows.UTF16PtrFromString(iconPath)
+	pathP := unsafe.Pointer(pathUTF)
+
+	rc, _, err := n.lib.setImage.Call(n.templatePointer, uintptr(pathP))
+	if rc != 1 {
+		return err
+	}
+	return nil
+}
+
+func (n *NotificationBuilder) SetSound(option int, path int) error {
+	n.lib.Lock()
+	defer n.lib.Unlock()
+
+	rc, _, err := n.lib.setSound.Call(n.templatePointer, uintptr(option), uintptr(path))
+	if rc != 1 {
+		return err
+	}
+	return nil
+}
+
+func (n *NotificationBuilder) Show() (int64, error) {
+	n.lib.Lock()
+	defer n.lib.Unlock()
+
+	id, _, err := n.lib.showNotification.Call(n.templatePointer)
+	if int64(id) == -1 {
+		return -1, err
+	}
+	return int64(id), nil
+}
diff --git a/cmds/notifier/wintoast/wintoast.go b/cmds/notifier/wintoast/wintoast.go
new file mode 100644
index 00000000..5d9a3380
--- /dev/null
+++ b/cmds/notifier/wintoast/wintoast.go
@@ -0,0 +1,217 @@
+//go:build windows
+
+package wintoast
+
+import (
+	"fmt"
+	"sync"
+	"unsafe"
+
+	"github.com/tevino/abool"
+
+	"golang.org/x/sys/windows"
+)
+
+// WinNotify holds the DLL handle.
+type WinToast struct {
+	sync.RWMutex
+
+	dll *windows.DLL
+
+	initialized *abool.AtomicBool
+
+	initialize           *windows.Proc
+	isInitialized        *windows.Proc
+	createNotification   *windows.Proc
+	deleteNotification   *windows.Proc
+	addButton            *windows.Proc
+	setImage             *windows.Proc
+	setSound             *windows.Proc
+	showNotification     *windows.Proc
+	hideNotification     *windows.Proc
+	setActivatedCallback *windows.Proc
+	setDismissedCallback *windows.Proc
+	setFailedCallback    *windows.Proc
+}
+
+func New(dllPath string) (*WinToast, error) {
+	if dllPath == "" {
+		return nil, fmt.Errorf("winnotifiy: path to dll not specified")
+	}
+
+	libraryObject := &WinToast{}
+	libraryObject.initialized = abool.New()
+
+	// load dll
+	var err error
+	libraryObject.dll, err = windows.LoadDLL(dllPath)
+	if err != nil {
+		return nil, fmt.Errorf("winnotifiy: failed to load notifier dll %w", err)
+	}
+
+	// load functions
+	libraryObject.initialize, err = libraryObject.dll.FindProc("PortmasterToastInitialize")
+	if err != nil {
+		return nil, fmt.Errorf("winnotifiy: PortmasterToastInitialize not found %w", err)
+	}
+
+	libraryObject.isInitialized, err = libraryObject.dll.FindProc("PortmasterToastIsInitialized")
+	if err != nil {
+		return nil, fmt.Errorf("winnotifiy: PortmasterToastIsInitialized not found %w", err)
+	}
+
+	libraryObject.createNotification, err = libraryObject.dll.FindProc("PortmasterToastCreateNotification")
+	if err != nil {
+		return nil, fmt.Errorf("winnotifiy: PortmasterToastCreateNotification not found %w", err)
+	}
+
+	libraryObject.deleteNotification, err = libraryObject.dll.FindProc("PortmasterToastDeleteNotification")
+	if err != nil {
+		return nil, fmt.Errorf("winnotifiy: PortmasterToastDeleteNotification not found %w", err)
+	}
+
+	libraryObject.addButton, err = libraryObject.dll.FindProc("PortmasterToastAddButton")
+	if err != nil {
+		return nil, fmt.Errorf("winnotifiy: PortmasterToastAddButton not found %w", err)
+	}
+
+	libraryObject.setImage, err = libraryObject.dll.FindProc("PortmasterToastSetImage")
+	if err != nil {
+		return nil, fmt.Errorf("winnotifiy: PortmasterToastSetImage not found %w", err)
+	}
+
+	libraryObject.setSound, err = libraryObject.dll.FindProc("PortmasterToastSetSound")
+	if err != nil {
+		return nil, fmt.Errorf("winnotifiy: PortmasterToastSetSound not found %w", err)
+	}
+
+	libraryObject.showNotification, err = libraryObject.dll.FindProc("PortmasterToastShow")
+	if err != nil {
+		return nil, fmt.Errorf("winnotifiy: PortmasterToastShow not found %w", err)
+	}
+
+	libraryObject.setActivatedCallback, err = libraryObject.dll.FindProc("PortmasterToastActivatedCallback")
+	if err != nil {
+		return nil, fmt.Errorf("winnotifiy: PortmasterActivatedCallback not found %w", err)
+	}
+
+	libraryObject.setDismissedCallback, err = libraryObject.dll.FindProc("PortmasterToastDismissedCallback")
+	if err != nil {
+		return nil, fmt.Errorf("winnotifiy: PortmasterToastDismissedCallback not found %w", err)
+	}
+
+	libraryObject.setFailedCallback, err = libraryObject.dll.FindProc("PortmasterToastFailedCallback")
+	if err != nil {
+		return nil, fmt.Errorf("winnotifiy: PortmasterToastFailedCallback not found %w", err)
+	}
+
+	libraryObject.hideNotification, err = libraryObject.dll.FindProc("PortmasterToastHide")
+	if err != nil {
+		return nil, fmt.Errorf("winnotifiy: PortmasterToastHide not found %w", err)
+	}
+
+	return libraryObject, nil
+}
+
+func (lib *WinToast) Initialize(appName, aumi, originalShortcutPath string) error {
+	if lib == nil {
+		return fmt.Errorf("wintoast: lib object was nil")
+	}
+
+	lib.Lock()
+	defer lib.Unlock()
+
+	// Initialize all necessary string for the notification meta data
+	appNameUTF, _ := windows.UTF16PtrFromString(appName)
+	aumiUTF, _ := windows.UTF16PtrFromString(aumi)
+	linkUTF, _ := windows.UTF16PtrFromString(originalShortcutPath)
+
+	// They are needed as unsafe pointers
+	appNameP := unsafe.Pointer(appNameUTF)
+	aumiP := unsafe.Pointer(aumiUTF)
+	linkP := unsafe.Pointer(linkUTF)
+
+	// Initialize notifications
+	rc, _, err := lib.initialize.Call(uintptr(appNameP), uintptr(aumiP), uintptr(linkP))
+	if rc != 0 {
+		return fmt.Errorf("wintoast: failed to initialize library rc = %d, %w", rc, err)
+	}
+
+	// Check if if the initialization was successfully
+	rc, _, _ = lib.isInitialized.Call()
+	if rc == 1 {
+		lib.initialized.Set()
+	} else {
+		return fmt.Errorf("wintoast: initialized flag was not set: rc = %d", rc)
+	}
+
+	return nil
+}
+
+func (lib *WinToast) SetCallbacks(activated func(id int64, actionIndex int32), dismissed func(id int64, reason int32), failed func(id int64, reason int32)) error {
+	if lib == nil {
+		return fmt.Errorf("wintoast: lib object was nil")
+	}
+
+	if lib.initialized.IsNotSet() {
+		return fmt.Errorf("winnotifiy: library not initialized")
+	}
+
+	// Initialize notification activated callback
+	callback := windows.NewCallback(func(id int64, actionIndex int32) uint64 {
+		activated(id, actionIndex)
+		return 0
+	})
+	rc, _, err := lib.setActivatedCallback.Call(callback)
+	if rc != 1 {
+		return fmt.Errorf("winnotifiy: failed to initialize activated callback %w", err)
+	}
+
+	// Initialize notification dismissed callback
+	callback = windows.NewCallback(func(id int64, actionIndex int32) uint64 {
+		dismissed(id, actionIndex)
+		return 0
+	})
+	rc, _, err = lib.setDismissedCallback.Call(callback)
+	if rc != 1 {
+		return fmt.Errorf("winnotifiy: failed to initialize dismissed callback %w", err)
+	}
+
+	// Initialize notification failed callback
+	callback = windows.NewCallback(func(id int64, actionIndex int32) uint64 {
+		failed(id, actionIndex)
+		return 0
+	})
+	rc, _, err = lib.setFailedCallback.Call(callback)
+	if rc != 1 {
+		return fmt.Errorf("winnotifiy: failed to initialize failed callback %s", err)
+	}
+
+	return nil
+}
+
+// NewNotification starts a creation of new notification. NotificationBuilder.Delete should allays be called when done using the object or there will be memory leeks
+func (lib *WinToast) NewNotification(title string, content string) (*NotificationBuilder, error) {
+	if lib == nil {
+		return nil, fmt.Errorf("wintoast: lib object was nil")
+	}
+	return newNotification(lib, title, content)
+}
+
+// HideNotification hides notification
+func (lib *WinToast) HideNotification(id int64) error {
+	if lib == nil {
+		return fmt.Errorf("wintoast: lib object was nil")
+	}
+
+	lib.Lock()
+	defer lib.Unlock()
+
+	rc, _, _ := lib.hideNotification.Call(uintptr(id))
+
+	if rc != 1 {
+		return fmt.Errorf("wintoast: failed to hide notification %d", id)
+	}
+
+	return nil
+}
diff --git a/cmds/observation-hub/main.go b/cmds/observation-hub/main.go
index 598680f0..0a96df6e 100644
--- a/cmds/observation-hub/main.go
+++ b/cmds/observation-hub/main.go
@@ -19,6 +19,7 @@ import (
 	"github.com/safing/portmaster/base/metrics"
 	"github.com/safing/portmaster/service/mgr"
 	"github.com/safing/portmaster/service/updates"
+	"github.com/safing/portmaster/service/updates/helper"
 	"github.com/safing/portmaster/spn"
 	"github.com/safing/portmaster/spn/captain"
 	"github.com/safing/portmaster/spn/conf"
@@ -37,6 +38,7 @@ func main() {
 
 	// Configure user agent and updates.
 	updates.UserAgent = fmt.Sprintf("SPN Observation Hub (%s %s)", runtime.GOOS, runtime.GOARCH)
+	helper.IntelOnly()
 
 	// Configure SPN mode.
 	conf.EnableClient(true)
@@ -46,8 +48,9 @@ func main() {
 	sluice.EnableListener = false
 	api.EnableServer = false
 
-	// Start logger with default log level.
-	_ = log.Start(log.WarningLevel)
+	// Set default log level.
+	log.SetLogLevel(log.WarningLevel)
+	_ = log.Start()
 
 	// Create instance.
 	var execCmdLine bool
@@ -77,8 +80,6 @@ func main() {
 	}
 	instance.AddModule(observer)
 
-	// FIXME: Use service?
-
 	// Execute command line operation, if requested or available.
 	switch {
 	case !execCmdLine:
@@ -127,7 +128,7 @@ func main() {
 			slog.Warn("program was interrupted, stopping")
 		}
 
-	case <-instance.ShuttingDown():
+	case <-instance.Stopped():
 		log.Shutdown()
 		os.Exit(instance.ExitCode())
 	}
diff --git a/cmds/portmaster-core/main.go b/cmds/portmaster-core/main.go
index 8cd80864..bc9f4f28 100644
--- a/cmds/portmaster-core/main.go
+++ b/cmds/portmaster-core/main.go
@@ -1,59 +1,43 @@
 package main
 
 import (
+	"bufio"
+	"errors"
 	"flag"
 	"fmt"
+	"io"
+	"log/slog"
 	"os"
+	"os/signal"
 	"runtime"
-
-	"github.com/spf13/cobra"
+	"runtime/pprof"
+	"syscall"
+	"time"
 
 	"github.com/safing/portmaster/base/info"
+	"github.com/safing/portmaster/base/log"
 	"github.com/safing/portmaster/base/metrics"
 	"github.com/safing/portmaster/service"
+	"github.com/safing/portmaster/service/mgr"
 	"github.com/safing/portmaster/service/updates"
+	"github.com/safing/portmaster/spn/conf"
 )
 
 var (
-	rootCmd = &cobra.Command{
-		Use:              "portmaster-core",
-		PersistentPreRun: initializeGlobals,
-		Run:              cmdRun,
-	}
+	printStackOnExit   bool
+	enableInputSignals bool
 
-	binDir  string
-	dataDir string
-
-	logToStdout bool
-	logDir      string
-	logLevel    string
-
-	svcCfg *service.ServiceConfig
+	sigUSR1 = syscall.Signal(0xa) // dummy for windows
 )
 
 func init() {
-	// Add Go's default flag set.
-	// TODO: Move flags throughout Portmaster to here and add their values to the service config.
-	rootCmd.Flags().AddGoFlagSet(flag.CommandLine)
-
-	// Add persisent flags for all commands.
-	rootCmd.PersistentFlags().StringVar(&binDir, "bin-dir", "", "set directory for executable binaries (rw/ro)")
-	rootCmd.PersistentFlags().StringVar(&dataDir, "data-dir", "", "set directory for variable data (rw)")
-
-	// Add flags for service only.
-	rootCmd.Flags().BoolVar(&logToStdout, "log-stdout", false, "log to stdout instead of file")
-	rootCmd.Flags().StringVar(&logDir, "log-dir", "", "set directory for logs")
-	rootCmd.Flags().StringVar(&logLevel, "log", "", "set log level to [trace|debug|info|warning|error|critical]")
+	flag.BoolVar(&printStackOnExit, "print-stack-on-exit", false, "prints the stack before of shutting down")
+	flag.BoolVar(&enableInputSignals, "input-signals", false, "emulate signals using stdin")
 }
 
 func main() {
-	if err := rootCmd.Execute(); err != nil {
-		fmt.Println(err)
-		os.Exit(1)
-	}
-}
+	flag.Parse()
 
-func initializeGlobals(cmd *cobra.Command, args []string) {
 	// set information
 	info.Set("Portmaster", "", "GPLv3")
 
@@ -63,18 +47,150 @@ func initializeGlobals(cmd *cobra.Command, args []string) {
 	// Configure user agent.
 	updates.UserAgent = fmt.Sprintf("Portmaster Core (%s %s)", runtime.GOOS, runtime.GOARCH)
 
-	// Create service config.
-	svcCfg = &service.ServiceConfig{
-		BinDir:  binDir,
-		DataDir: dataDir,
+	// enable SPN client mode
+	conf.EnableClient(true)
+	conf.EnableIntegration(true)
 
-		LogToStdout: logToStdout,
-		LogDir:      logDir,
-		LogLevel:    logLevel,
+	// Create instance.
+	var execCmdLine bool
+	instance, err := service.New(&service.ServiceConfig{})
+	switch {
+	case err == nil:
+		// Continue
+	case errors.Is(err, mgr.ErrExecuteCmdLineOp):
+		execCmdLine = true
+	default:
+		fmt.Printf("error creating an instance: %s\n", err)
+		os.Exit(2)
+	}
 
-		BinariesIndexURLs:   service.DefaultStableBinaryIndexURLs,
-		IntelIndexURLs:      service.DefaultIntelIndexURLs,
-		VerifyBinaryUpdates: service.BinarySigningTrustStore,
-		VerifyIntelUpdates:  service.BinarySigningTrustStore,
+	// Execute command line operation, if requested or available.
+	switch {
+	case !execCmdLine:
+		// Run service.
+	case instance.CommandLineOperation == nil:
+		fmt.Println("command line operation execution requested, but not set")
+		os.Exit(3)
+	default:
+		// Run the function and exit.
+		err = instance.CommandLineOperation()
+		if err != nil {
+			fmt.Fprintf(os.Stderr, "command line operation failed: %s\n", err)
+			os.Exit(3)
+		}
+		os.Exit(0)
+	}
+
+	// Set default log level.
+	log.SetLogLevel(log.WarningLevel)
+	_ = log.Start()
+
+	// Start
+	go func() {
+		err = instance.Start()
+		if err != nil {
+			fmt.Printf("instance start failed: %s\n", err)
+
+			// Print stack on start failure, if enabled.
+			if printStackOnExit {
+				printStackTo(os.Stdout, "PRINTING STACK ON START FAILURE")
+			}
+
+			os.Exit(1)
+		}
+	}()
+
+	// Wait for signal.
+	signalCh := make(chan os.Signal, 1)
+	if enableInputSignals {
+		go inputSignals(signalCh)
+	}
+	signal.Notify(
+		signalCh,
+		os.Interrupt,
+		syscall.SIGHUP,
+		syscall.SIGINT,
+		syscall.SIGTERM,
+		syscall.SIGQUIT,
+		sigUSR1,
+	)
+
+	select {
+	case sig := <-signalCh:
+		// Only print and continue to wait if SIGUSR1
+		if sig == sigUSR1 {
+			printStackTo(os.Stderr, "PRINTING STACK ON REQUEST")
+		} else {
+			fmt.Println(" <INTERRUPT>") // CLI output.
+			slog.Warn("program was interrupted, stopping")
+		}
+
+	case <-instance.Stopped():
+		log.Shutdown()
+		os.Exit(instance.ExitCode())
+	}
+
+	// Catch signals during shutdown.
+	// Rapid unplanned disassembly after 5 interrupts.
+	go func() {
+		forceCnt := 5
+		for {
+			<-signalCh
+			forceCnt--
+			if forceCnt > 0 {
+				fmt.Printf(" <INTERRUPT> again, but already shutting down - %d more to force\n", forceCnt)
+			} else {
+				printStackTo(os.Stderr, "PRINTING STACK ON FORCED EXIT")
+				os.Exit(1)
+			}
+		}
+	}()
+
+	// Rapid unplanned disassembly after 3 minutes.
+	go func() {
+		time.Sleep(3 * time.Minute)
+		printStackTo(os.Stderr, "PRINTING STACK - TAKING TOO LONG FOR SHUTDOWN")
+		os.Exit(1)
+	}()
+
+	// Stop instance.
+	if err := instance.Stop(); err != nil {
+		slog.Error("failed to stop", "err", err)
+	}
+	log.Shutdown()
+
+	// Print stack on shutdown, if enabled.
+	if printStackOnExit {
+		printStackTo(os.Stdout, "PRINTING STACK ON EXIT")
+	}
+
+	os.Exit(instance.ExitCode())
+}
+
+func printStackTo(writer io.Writer, msg string) {
+	_, err := fmt.Fprintf(writer, "===== %s =====\n", msg)
+	if err == nil {
+		err = pprof.Lookup("goroutine").WriteTo(writer, 1)
+	}
+	if err != nil {
+		slog.Error("failed to write stack trace", "err", err)
+	}
+}
+
+func inputSignals(signalCh chan os.Signal) {
+	scanner := bufio.NewScanner(os.Stdin)
+	for scanner.Scan() {
+		switch scanner.Text() {
+		case "SIGHUP":
+			signalCh <- syscall.SIGHUP
+		case "SIGINT":
+			signalCh <- syscall.SIGINT
+		case "SIGQUIT":
+			signalCh <- syscall.SIGQUIT
+		case "SIGTERM":
+			signalCh <- syscall.SIGTERM
+		case "SIGUSR1":
+			signalCh <- sigUSR1
+		}
 	}
 }
diff --git a/cmds/portmaster-core/recover_linux.go b/cmds/portmaster-core/recover_linux.go
deleted file mode 100644
index 6f5532c2..00000000
--- a/cmds/portmaster-core/recover_linux.go
+++ /dev/null
@@ -1,90 +0,0 @@
-package main
-
-import (
-	"errors"
-	"flag"
-	"fmt"
-	"os"
-	"strings"
-
-	"github.com/hashicorp/go-multierror"
-	"github.com/spf13/cobra"
-
-	"github.com/safing/portmaster/service/firewall/interception"
-)
-
-var (
-	recoverCmd = &cobra.Command{
-		Use:   "recover-iptables",
-		Short: "Force an update of all components.",
-		RunE:  update,
-	}
-
-	recoverIPTables bool
-)
-
-func init() {
-	rootCmd.AddCommand(recoverCmd)
-
-	flag.BoolVar(&recoverIPTables, "recover-iptables", false, "recovers ip table rules (backward compatibility; use command instead)")
-}
-
-func recover(cmd *cobra.Command, args []string) error {
-	// interception.DeactiveNfqueueFirewall uses coreos/go-iptables
-	// which shells out to the /sbin/iptables binary. As a result,
-	// we don't get the errno of the actual error and need to parse the
-	// output instead. Make sure it's always english by setting LC_ALL=C
-	currentLocale := os.Getenv("LC_ALL")
-	_ = os.Setenv("LC_ALL", "C")
-	defer func() {
-		_ = os.Setenv("LC_ALL", currentLocale)
-	}()
-
-	err := interception.DeactivateNfqueueFirewall()
-	if err == nil {
-		return nil
-	}
-
-	// we don't want to show ErrNotExists to the user
-	// as that only means portmaster did the cleanup itself.
-	var mr *multierror.Error
-	if !errors.As(err, &mr) {
-		return err
-	}
-
-	var filteredErrors *multierror.Error
-	for _, err := range mr.Errors {
-		// if we have a permission denied error, all errors will be the same
-		if strings.Contains(err.Error(), "Permission denied") {
-			return fmt.Errorf("failed to cleanup iptables: %w", os.ErrPermission)
-		}
-
-		if !strings.Contains(err.Error(), "No such file or directory") {
-			filteredErrors = multierror.Append(filteredErrors, err)
-		}
-	}
-
-	if filteredErrors != nil {
-		filteredErrors.ErrorFormat = formatNfqErrors
-		return filteredErrors.ErrorOrNil()
-	}
-
-	return nil
-}
-
-func formatNfqErrors(es []error) string {
-	if len(es) == 1 {
-		return fmt.Sprintf("1 error occurred:\n\t* %s\n\n", es[0])
-	}
-
-	points := make([]string, len(es))
-	for i, err := range es {
-		// only display the very first line of each error
-		first := strings.Split(err.Error(), "\n")[0]
-		points[i] = fmt.Sprintf("* %s", first)
-	}
-
-	return fmt.Sprintf(
-		"%d errors occurred:\n\t%s\n\n",
-		len(es), strings.Join(points, "\n\t"))
-}
diff --git a/cmds/portmaster-core/run.go b/cmds/portmaster-core/run.go
deleted file mode 100644
index 6c41bd56..00000000
--- a/cmds/portmaster-core/run.go
+++ /dev/null
@@ -1,140 +0,0 @@
-package main
-
-import (
-	"errors"
-	"flag"
-	"fmt"
-	"io"
-	"log/slog"
-	"os"
-	"runtime/pprof"
-	"time"
-
-	"github.com/spf13/cobra"
-
-	"github.com/safing/portmaster/base/log"
-	"github.com/safing/portmaster/service"
-	"github.com/safing/portmaster/service/mgr"
-	"github.com/safing/portmaster/spn/conf"
-)
-
-var printStackOnExit bool
-
-func init() {
-	flag.BoolVar(&printStackOnExit, "print-stack-on-exit", false, "prints the stack before of shutting down")
-}
-
-type SystemService interface {
-	Run()
-	IsService() bool
-	RestartService() error
-}
-
-func cmdRun(cmd *cobra.Command, args []string) {
-	// Run platform specific setup or switches.
-	runPlatformSpecifics(cmd, args)
-
-	// SETUP
-
-	// Enable SPN client mode.
-	// TODO: Move this to service config.
-	conf.EnableClient(true)
-	conf.EnableIntegration(true)
-
-	// Create instance.
-	// Instance modules might request a cmdline execution of a function.
-	var execCmdLine bool
-	instance, err := service.New(svcCfg)
-	switch {
-	case err == nil:
-		// Continue
-	case errors.Is(err, mgr.ErrExecuteCmdLineOp):
-		execCmdLine = true
-	default:
-		fmt.Printf("error creating an instance: %s\n", err)
-		os.Exit(2)
-	}
-
-	// Execute module command line operation, if requested or available.
-	switch {
-	case !execCmdLine:
-		// Run service.
-	case instance.CommandLineOperation == nil:
-		fmt.Println("command line operation execution requested, but not set")
-		os.Exit(3)
-	default:
-		// Run the function and exit.
-		fmt.Println("executing cmdline op")
-		err = instance.CommandLineOperation()
-		if err != nil {
-			fmt.Fprintf(os.Stderr, "command line operation failed: %s\n", err)
-			os.Exit(3)
-		}
-		os.Exit(0)
-	}
-
-	// START
-
-	// FIXME: fix color and duplicate level when logging with slog
-	// FIXME: check for tty for color enabling
-
-	// Start logging.
-	err = log.Start(svcCfg.LogLevel, svcCfg.LogToStdout, svcCfg.LogDir)
-	if err != nil {
-		fmt.Fprintln(os.Stderr, err.Error())
-		os.Exit(4)
-	}
-
-	// Create system service.
-	service := NewSystemService(instance)
-
-	// Start instance via system service manager.
-	go func() {
-		service.Run()
-	}()
-
-	// SHUTDOWN
-
-	// Wait for shutdown to be started.
-	<-instance.ShuttingDown()
-
-	// Wait for shutdown to be finished.
-	select {
-	case <-instance.ShutdownComplete():
-		// Print stack on shutdown, if enabled.
-		if printStackOnExit {
-			printStackTo(log.GlobalWriter, "PRINTING STACK ON EXIT")
-		}
-	case <-time.After(3 * time.Minute):
-		printStackTo(log.GlobalWriter, "PRINTING STACK - TAKING TOO LONG FOR SHUTDOWN")
-	}
-
-	// Check if restart was triggered and send start service command if true.
-	if instance.ShouldRestart && service.IsService() {
-		if err := service.RestartService(); err != nil {
-			slog.Error("failed to restart service", "err", err)
-		}
-	}
-
-	// Stop logging.
-	log.Shutdown()
-
-	// Give a small amount of time for everything to settle:
-	// - All logs written.
-	// - Restart command started, if needed.
-	// - Windows service manager notified.
-	time.Sleep(100 * time.Millisecond)
-
-	// Exit
-	os.Exit(instance.ExitCode())
-}
-
-func printStackTo(writer io.Writer, msg string) {
-	_, err := fmt.Fprintf(writer, "===== %s =====\n", msg)
-	if err == nil {
-		err = pprof.Lookup("goroutine").WriteTo(writer, 1)
-	}
-	if err != nil {
-		slog.Error("failed to write stack trace", "err", err)
-	}
-}
diff --git a/cmds/portmaster-core/run_linux.go b/cmds/portmaster-core/run_linux.go
deleted file mode 100644
index 858ed022..00000000
--- a/cmds/portmaster-core/run_linux.go
+++ /dev/null
@@ -1,145 +0,0 @@
-package main
-
-import (
-	"fmt"
-	"log/slog"
-	"os"
-	"os/exec"
-	"os/signal"
-	"syscall"
-
-	processInfo "github.com/shirou/gopsutil/process"
-	"github.com/spf13/cobra"
-
-	"github.com/safing/portmaster/base/log"
-	"github.com/safing/portmaster/service"
-)
-
-type LinuxSystemService struct {
-	instance *service.Instance
-}
-
-func NewSystemService(instance *service.Instance) *LinuxSystemService {
-	return &LinuxSystemService{instance: instance}
-}
-
-func (s *LinuxSystemService) Run() {
-	// Start instance.
-	err := s.instance.Start()
-	if err != nil {
-		slog.Error("failed to start", "err", err)
-
-		// Print stack on start failure, if enabled.
-		if printStackOnExit {
-			printStackTo(log.GlobalWriter, "PRINTING STACK ON START FAILURE")
-		}
-
-		os.Exit(1)
-	}
-
-	// Subscribe to signals.
-	signalCh := make(chan os.Signal, 1)
-	signal.Notify(
-		signalCh,
-		os.Interrupt,
-		syscall.SIGHUP,
-		syscall.SIGINT,
-		syscall.SIGTERM,
-		syscall.SIGQUIT,
-		syscall.SIGUSR1,
-	)
-
-	// Wait for shutdown signal.
-wait:
-	for {
-		select {
-		case <-s.instance.ShuttingDown():
-			break wait
-		case sig := <-signalCh:
-			// Only print and continue to wait if SIGUSR1
-			if sig == syscall.SIGUSR1 {
-				printStackTo(log.GlobalWriter, "PRINTING STACK ON REQUEST")
-				continue wait
-			} else {
-				// Trigger shutdown.
-				fmt.Printf(" <SIGNAL: %v>", sig) // CLI output.
-				slog.Warn("received stop signal", "signal", sig)
-				s.instance.Shutdown()
-				break wait
-			}
-		}
-	}
-
-	// Wait for shutdown to finish.
-
-	// Catch signals during shutdown.
-	// Force exit after 5 interrupts.
-	forceCnt := 5
-	for {
-		select {
-		case <-s.instance.ShutdownComplete():
-			return
-		case sig := <-signalCh:
-			if sig != syscall.SIGUSR1 {
-				forceCnt--
-				if forceCnt > 0 {
-					fmt.Printf(" <SIGNAL: %s> again, but already shutting down - %d more to force\n", sig, forceCnt)
-				} else {
-					printStackTo(log.GlobalWriter, "PRINTING STACK ON FORCED EXIT")
-					os.Exit(1)
-				}
-			}
-		}
-	}
-}
-
-func (s *LinuxSystemService) RestartService() error {
-	// Check if user defined custom command for restarting the service.
-	restartCommand, exists := os.LookupEnv("PORTMASTER_RESTART_COMMAND")
-
-	// Run the service restart
-	var cmd *exec.Cmd
-	if exists && restartCommand != "" {
-		slog.Debug("running custom restart command", "command", restartCommand)
-		cmd = exec.Command("sh", "-c", restartCommand)
-	} else {
-		cmd = exec.Command("systemctl", "restart", "portmaster")
-	}
-	if err := cmd.Start(); err != nil {
-		return fmt.Errorf("failed run restart command: %w", err)
-	}
-	return nil
-}
-
-func (s *LinuxSystemService) IsService() bool {
-	// Get own process ID
-	pid := os.Getpid()
-
-	// Get parent process ID.
-	currentProcess, err := processInfo.NewProcess(int32(pid)) //nolint:gosec
-	if err != nil {
-		return false
-	}
-	ppid, err := currentProcess.Ppid()
-	if err != nil {
-		return false
-	}
-
-	// Check if the parent process ID is 1 == init system
-	return ppid == 1
-}
-
-func runPlatformSpecifics(cmd *cobra.Command, args []string) {
-	// If recover-iptables flag is set, run the recover-iptables command.
-	// This is for backwards compatibility
-	if recoverIPTables {
-		exitCode := 0
-		err := recover(cmd, args)
-		if err != nil {
-			fmt.Printf("failed: %s", err)
-			exitCode = 1
-		}
-
-		os.Exit(exitCode)
-	}
-}
diff --git a/cmds/portmaster-core/run_windows.go b/cmds/portmaster-core/run_windows.go
deleted file mode 100644
index 09593630..00000000
--- a/cmds/portmaster-core/run_windows.go
+++ /dev/null
@@ -1,241 +0,0 @@
-package main
-
-// Based on the official Go examples from
-// https://github.com/golang/sys/blob/master/windows/svc/example
-// by The Go Authors.
-// Original LICENSE (sha256sum: 2d36597f7117c38b006835ae7f537487207d8ec407aa9d9980794b2030cbc067) can be found in vendor/pkg cache directory.
-
-import (
-	"fmt"
-	"log/slog"
-	"os"
-	"os/exec"
-	"os/signal"
-	"syscall"
-
-	"github.com/spf13/cobra"
-	"golang.org/x/sys/windows/svc"
-	"golang.org/x/sys/windows/svc/debug"
-
-	"github.com/safing/portmaster/base/log"
-	"github.com/safing/portmaster/service"
-)
-
-const serviceName = "PortmasterCore"
-
-type WindowsSystemService struct {
-	instance *service.Instance
-}
-
-func NewSystemService(instance *service.Instance) *WindowsSystemService {
-	return &WindowsSystemService{instance: instance}
-}
-
-func (s *WindowsSystemService) Run() {
-	svcRun := svc.Run
-
-	// Check if we are running interactively.
-	isService, err := svc.IsWindowsService()
-	switch {
-	case err != nil:
-		slog.Warn("failed to determine if running interactively", "err", err)
-		slog.Warn("continuing without service integration (no real service)")
-		svcRun = debug.Run
-
-	case !isService:
-		slog.Warn("running interactively, switching to debug execution (no real service)")
-		svcRun = debug.Run
-	}
-
-	// Run service client.
-	err = svcRun(serviceName, s)
-	if err != nil {
-		slog.Error("service execution failed", "err", err)
-		os.Exit(1)
-	}
-
-	// Execution continues in s.Execute().
-}
-
-func (s *WindowsSystemService) Execute(args []string, changeRequests <-chan svc.ChangeRequest, changes chan<- svc.Status) (svcSpecificEC bool, exitCode uint32) {
-	// Tell service manager we are starting.
-	changes <- svc.Status{State: svc.StartPending}
-
-	// Start instance.
-	err := s.instance.Start()
-	if err != nil {
-		fmt.Printf("failed to start: %s\n", err)
-
-		// Print stack on start failure, if enabled.
-		if printStackOnExit {
-			printStackTo(log.GlobalWriter, "PRINTING STACK ON START FAILURE")
-		}
-
-		// Notify service manager we stopped again.
-		changes <- svc.Status{State: svc.Stopped}
-
-		// Relay exit code to service manager.
-		return false, 1
-	}
-
-	// Tell service manager we are up and running!
-	changes <- svc.Status{State: svc.Running, Accepts: svc.AcceptStop | svc.AcceptShutdown}
-
-	// Subscribe to signals.
-	// Docs: https://pkg.go.dev/os/signal?GOOS=windows
-	signalCh := make(chan os.Signal, 4)
-	signal.Notify(
-		signalCh,
-
-		// Windows ^C (Control-C) or ^BREAK (Control-Break).
-		// Completely prevents kill.
-		os.Interrupt,
-
-		// Windows CTRL_CLOSE_EVENT, CTRL_LOGOFF_EVENT or CTRL_SHUTDOWN_EVENT.
-		// Does not prevent kill, but gives a little time to stop service.
-		syscall.SIGTERM,
-	)
-
-	// Wait for shutdown signal.
-waitSignal:
-	for {
-		select {
-		case sig := <-signalCh:
-			// Trigger shutdown.
-			fmt.Printf(" <SIGNAL: %v>", sig) // CLI output.
-			slog.Warn("received stop signal", "signal", sig)
-			break waitSignal
-
-		case c := <-changeRequests:
-			switch c.Cmd {
-			case svc.Interrogate:
-				changes <- c.CurrentStatus
-
-			case svc.Stop, svc.Shutdown:
-				fmt.Printf(" <SERVICE CMD: %v>", serviceCmdName(c.Cmd)) // CLI output.
-				slog.Warn("received service shutdown command", "cmd", c.Cmd)
-				break waitSignal
-
-			default:
-				slog.Error("unexpected service control request", "cmd", serviceCmdName(c.Cmd))
-			}
-
-		case <-s.instance.ShuttingDown():
-			break waitSignal
-		}
-	}
-
-	// Wait for shutdown to finish.
-	changes <- svc.Status{State: svc.StopPending}
-
-	// Catch signals during shutdown.
-	// Force exit after 5 interrupts.
-	forceCnt := 5
-waitShutdown:
-	for {
-		select {
-		case <-s.instance.ShutdownComplete():
-			break waitShutdown
-
-		case sig := <-signalCh:
-			forceCnt--
-			if forceCnt > 0 {
-				fmt.Printf(" <SIGNAL: %s> but already shutting down - %d more to force\n", sig, forceCnt)
-			} else {
-				printStackTo(log.GlobalWriter, "PRINTING STACK ON FORCED EXIT")
-				os.Exit(1)
-			}
-
-		case c := <-changeRequests:
-			switch c.Cmd {
-			case svc.Interrogate:
-				changes <- c.CurrentStatus
-
-			case svc.Stop, svc.Shutdown:
-				forceCnt--
-				if forceCnt > 0 {
-					fmt.Printf(" <SERVICE CMD: %v> but already shutting down - %d more to force\n", serviceCmdName(c.Cmd), forceCnt)
-				} else {
-					printStackTo(log.GlobalWriter, "PRINTING STACK ON FORCED EXIT")
-					os.Exit(1)
-				}
-
-			default:
-				slog.Error("unexpected service control request", "cmd", serviceCmdName(c.Cmd))
-			}
-		}
-	}
-
-	// Notify service manager.
-	changes <- svc.Status{State: svc.Stopped}
-
-	return false, 0
-}
-
-func (s *WindowsSystemService) IsService() bool {
-	isService, err := svc.IsWindowsService()
-	if err != nil {
-		return false
-	}
-	return isService
-}
-
-func (s *WindowsSystemService) RestartService() error {
-	// Script that wait for portmaster service status to change to stop
-	// and then sends a start command for the same service.
-	command := `
-$serviceName = "PortmasterCore"
-while ((Get-Service -Name $serviceName).Status -ne 'Stopped') {
-    Start-Sleep -Seconds 1
-}
-sc.exe start $serviceName`
-
-	// Create the command to execute the PowerShell script
-	cmd := exec.Command("powershell.exe", "-Command", command)
-	// Start the command. The script will continue even after the parent process exits.
-	err := cmd.Start()
-	if err != nil {
-		return err
-	}
-
-	return nil
-}
-
-func runPlatformSpecifics(cmd *cobra.Command, args []string)
-
-func serviceCmdName(cmd svc.Cmd) string {
-	switch cmd {
-	case svc.Stop:
-		return "Stop"
-	case svc.Pause:
-		return "Pause"
-	case svc.Continue:
-		return "Continue"
-	case svc.Interrogate:
-		return "Interrogate"
-	case svc.Shutdown:
-		return "Shutdown"
-	case svc.ParamChange:
-		return "ParamChange"
-	case svc.NetBindAdd:
-		return "NetBindAdd"
-	case svc.NetBindRemove:
-		return "NetBindRemove"
-	case svc.NetBindEnable:
-		return "NetBindEnable"
-	case svc.NetBindDisable:
-		return "NetBindDisable"
-	case svc.DeviceEvent:
-		return "DeviceEvent"
-	case svc.HardwareProfileChange:
-		return "HardwareProfileChange"
-	case svc.PowerEvent:
-		return "PowerEvent"
-	case svc.SessionChange:
-		return "SessionChange"
-	case svc.PreShutdown:
-		return "PreShutdown"
-	default:
-		return "Unknown Command"
-	}
-}
diff --git a/cmds/portmaster-core/update.go b/cmds/portmaster-core/update.go
deleted file mode 100644
index 866aab61..00000000
--- a/cmds/portmaster-core/update.go
+++ /dev/null
@@ -1,77 +0,0 @@
-package main
-
-import (
-	"fmt"
-	"log/slog"
-
-	"github.com/spf13/cobra"
-
-	"github.com/safing/portmaster/base/log"
-	"github.com/safing/portmaster/base/notifications"
-	"github.com/safing/portmaster/service"
-	"github.com/safing/portmaster/service/updates"
-)
-
-var updateCmd = &cobra.Command{
-	Use:   "update",
-	Short: "Force an update of all components.",
-	RunE:  update,
-}
-
-func init() {
-	rootCmd.AddCommand(updateCmd)
-}
-
-func update(cmd *cobra.Command, args []string) error {
-	// Finalize config.
-	err := svcCfg.Init()
-	if err != nil {
-		return fmt.Errorf("internal configuration error: %w", err)
-	}
-	// Force logging to stdout.
-	svcCfg.LogToStdout = true
-
-	// Start logging.
-	_ = log.Start(svcCfg.LogLevel, svcCfg.LogToStdout, svcCfg.LogDir)
-	defer log.Shutdown()
-
-	// Create updaters.
-	instance := &updateDummyInstance{}
-	binaryUpdateConfig, intelUpdateConfig, err := service.MakeUpdateConfigs(svcCfg)
-	if err != nil {
-		return fmt.Errorf("init updater config: %w", err)
-	}
-	binaryUpdates, err := updates.New(instance, "Binary Updater", *binaryUpdateConfig)
-	if err != nil {
-		return fmt.Errorf("configure binary updates: %w", err)
-	}
-	intelUpdates, err := updates.New(instance, "Intel Updater", *intelUpdateConfig)
-	if err != nil {
-		return fmt.Errorf("configure intel updates: %w", err)
-	}
-
-	// Force update all.
-	binErr := binaryUpdates.ForceUpdate()
-	if binErr != nil {
-		slog.Error("binary update failed", "err", binErr)
-	}
-	intelErr := intelUpdates.ForceUpdate()
-	if intelErr != nil {
-		slog.Error("intel update failed", "err", intelErr)
-	}
-
-	// Return error.
-	if binErr != nil {
-		return fmt.Errorf("binary update failed: %w", binErr)
-	}
-	if intelErr != nil {
-		return fmt.Errorf("intel update failed: %w", intelErr)
-	}
-	return nil
-}
-
-type updateDummyInstance struct{}
-
-func (udi *updateDummyInstance) Restart()                                    {}
-func (udi *updateDummyInstance) Shutdown()                                   {}
-func (udi *updateDummyInstance) Notifications() *notifications.Notifications { return nil }
diff --git a/cmds/portmaster-start/.gitignore b/cmds/portmaster-start/.gitignore
new file mode 100644
index 00000000..e3db7ed6
--- /dev/null
+++ b/cmds/portmaster-start/.gitignore
@@ -0,0 +1,6 @@
+# binaries
+portmaster-start
+portmaster-start.exe
+
+# test dir
+test
diff --git a/cmds/portmaster-start/build b/cmds/portmaster-start/build
new file mode 100755
index 00000000..38c552f5
--- /dev/null
+++ b/cmds/portmaster-start/build
@@ -0,0 +1,77 @@
+#!/bin/bash
+
+# get build data
+if [[ "$BUILD_COMMIT" == "" ]]; then
+  BUILD_COMMIT=$(git describe --all --long --abbrev=99 --dirty 2>/dev/null)
+fi
+if [[ "$BUILD_USER" == "" ]]; then
+  BUILD_USER=$(id -un)
+fi
+if [[ "$BUILD_HOST" == "" ]]; then
+  BUILD_HOST=$(hostname -f)
+fi
+if [[ "$BUILD_DATE" == "" ]]; then
+  BUILD_DATE=$(date +%d.%m.%Y)
+fi
+if [[ "$BUILD_SOURCE" == "" ]]; then
+  BUILD_SOURCE=$(git remote -v | grep origin | cut -f2 | cut -d" " -f1 | head -n 1)
+fi
+if [[ "$BUILD_SOURCE" == "" ]]; then
+  BUILD_SOURCE=$(git remote -v | cut -f2 | cut -d" " -f1 | head -n 1)
+fi
+BUILD_BUILDOPTIONS=$(echo $* | sed "s/ /§/g")
+
+# check
+if [[ "$BUILD_COMMIT" == "" ]]; then
+  echo "could not automatically determine BUILD_COMMIT, please supply manually as environment variable."
+  exit 1
+fi
+if [[ "$BUILD_USER" == "" ]]; then
+  echo "could not automatically determine BUILD_USER, please supply manually as environment variable."
+  exit 1
+fi
+if [[ "$BUILD_HOST" == "" ]]; then
+  echo "could not automatically determine BUILD_HOST, please supply manually as environment variable."
+  exit 1
+fi
+if [[ "$BUILD_DATE" == "" ]]; then
+  echo "could not automatically determine BUILD_DATE, please supply manually as environment variable."
+  exit 1
+fi
+if [[ "$BUILD_SOURCE" == "" ]]; then
+  echo "could not automatically determine BUILD_SOURCE, please supply manually as environment variable."
+  exit 1
+fi
+
+# set build options
+export CGO_ENABLED=0
+
+# special handling for Windows
+EXTRA_LD_FLAGS=""
+if [[ $GOOS == "windows" ]]; then
+  # checks
+  if [[ $CC_FOR_windows_amd64 == "" ]]; then
+    echo "ENV variable CC_FOR_windows_amd64 (c compiler) is not set. Please set it to the cross compiler you want to use for compiling for windows_amd64"
+    exit 1
+  fi
+  if [[ $CXX_FOR_windows_amd64 == "" ]]; then
+    echo "ENV variable CXX_FOR_windows_amd64 (c++ compiler) is not set. Please set it to the cross compiler you want to use for compiling for windows_amd64"
+    exit 1
+  fi
+  # compilers
+  export CC=$CC_FOR_windows_amd64
+  export CXX=$CXX_FOR_windows_amd64
+  # custom
+  export CGO_ENABLED=1
+  EXTRA_LD_FLAGS='-H windowsgui' # Hide console window by default (but we attach to parent console if available)
+  # generate resource.syso for windows metadata / icon
+  go generate
+fi
+
+echo "Please notice, that this build script includes metadata into the build."
+echo "This information is useful for debugging and license compliance."
+echo "Run the compiled binary with the -version flag to see the information included."
+
+# build
+BUILD_PATH="github.com/safing/portmaster/base/info"
+go build -ldflags "$EXTRA_LD_FLAGS -X ${BUILD_PATH}.commit=${BUILD_COMMIT} -X ${BUILD_PATH}.buildOptions=${BUILD_BUILDOPTIONS} -X ${BUILD_PATH}.buildUser=${BUILD_USER} -X ${BUILD_PATH}.buildHost=${BUILD_HOST} -X ${BUILD_PATH}.buildDate=${BUILD_DATE} -X ${BUILD_PATH}.buildSource=${BUILD_SOURCE}" "$@"
diff --git a/cmds/portmaster-start/console_default.go b/cmds/portmaster-start/console_default.go
new file mode 100644
index 00000000..f11a9fae
--- /dev/null
+++ b/cmds/portmaster-start/console_default.go
@@ -0,0 +1,11 @@
+//go:build !windows
+
+package main
+
+import "os/exec"
+
+func attachToParentConsole() (attached bool, err error) {
+	return true, nil
+}
+
+func hideWindow(cmd *exec.Cmd) {}
diff --git a/cmds/portmaster-start/console_windows.go b/cmds/portmaster-start/console_windows.go
new file mode 100644
index 00000000..1148d90e
--- /dev/null
+++ b/cmds/portmaster-start/console_windows.go
@@ -0,0 +1,150 @@
+package main
+
+// Parts of this file are FORKED
+// from https://github.com/apenwarr/fixconsole/blob/35b2e7d921eb80a71a5f04f166ff0a1405bddf79/fixconsole_windows.go
+// on 16.07.2019
+// with Apache-2.0 license
+// authored by https://github.com/apenwarr
+
+// docs/sources:
+// Stackoverflow Question: https://stackoverflow.com/questions/23743217/printing-output-to-a-command-window-when-golang-application-is-compiled-with-ld
+// MS AttachConsole: https://docs.microsoft.com/en-us/windows/console/attachconsole
+
+import (
+	"log"
+	"os"
+	"os/exec"
+	"syscall"
+
+	"golang.org/x/sys/windows"
+)
+
+const (
+	windowsAttachParentProcess = ^uintptr(0) // (DWORD)-1
+)
+
+var (
+	kernel32          = syscall.NewLazyDLL("kernel32.dll")
+	procAttachConsole = kernel32.NewProc("AttachConsole")
+)
+
+// Windows console output is a mess.
+//
+// If you compile as "-H windows", then if you launch your program without
+// a console, Windows forcibly creates one to use as your stdin/stdout, which
+// is silly for a GUI app, so we can't do that.
+//
+// If you compile as "-H windowsgui", then it doesn't create a console for
+// your app... but also doesn't provide a working stdin/stdout/stderr even if
+// you *did* launch from the console.  However, you can use AttachConsole()
+// to get a handle to your parent process's console, if any, and then
+// os.NewFile() to turn that handle into a fd usable as stdout/stderr.
+//
+// However, then you have the problem that if you redirect stdout or stderr
+// from the shell, you end up ignoring the redirection by forcing it to the
+// console.
+//
+// To fix *that*, we have to detect whether there was a pre-existing stdout
+// or not. We can check GetStdHandle(), which returns 0 for "should be
+// console" and nonzero for "already pointing at a file."
+//
+// Be careful though!  As soon as you run AttachConsole(), it resets *all*
+// the GetStdHandle() handles to point them at the console instead, thus
+// throwing away the original file redirects.  So we have to GetStdHandle()
+// *before* AttachConsole().
+//
+// For some reason, powershell redirections provide a valid file handle, but
+// writing to that handle doesn't write to the file.  I haven't found a way
+// to work around that.  (Windows 10.0.17763.379)
+//
+// Net result is as follows.
+// Before:
+//    SHELL            NON-REDIRECTED     REDIRECTED
+//    explorer.exe     no console         n/a
+//    cmd.exe          broken             works
+//    powershell       broken             broken
+//    WSL bash         broken             works
+// After
+//    SHELL            NON-REDIRECTED     REDIRECTED
+//    explorer.exe     no console         n/a
+//    cmd.exe          works              works
+//    powershell       works              broken
+//    WSL bash         works              works
+//
+// We don't seem to make anything worse, at least.
+func attachToParentConsole() (attached bool, err error) {
+	// get std handles before we attempt to attach to parent console
+	stdin, _ := syscall.GetStdHandle(syscall.STD_INPUT_HANDLE)
+	stdout, _ := syscall.GetStdHandle(syscall.STD_OUTPUT_HANDLE)
+	stderr, _ := syscall.GetStdHandle(syscall.STD_ERROR_HANDLE)
+
+	// attempt to attach to parent console
+	err = procAttachConsole.Find()
+	if err != nil {
+		return false, err
+	}
+	r1, _, _ := procAttachConsole.Call(windowsAttachParentProcess)
+	if r1 == 0 {
+		// possible errors:
+		// ERROR_ACCESS_DENIED: already attached to console
+		// ERROR_INVALID_HANDLE: process does not have console
+		// ERROR_INVALID_PARAMETER: process does not exist
+		return false, nil
+	}
+
+	// get std handles after we attached to console
+	var invalid syscall.Handle
+	con := invalid
+
+	if stdin == invalid {
+		stdin, _ = syscall.GetStdHandle(syscall.STD_INPUT_HANDLE)
+	}
+	if stdout == invalid {
+		stdout, _ = syscall.GetStdHandle(syscall.STD_OUTPUT_HANDLE)
+		con = stdout
+	}
+	if stderr == invalid {
+		stderr, _ = syscall.GetStdHandle(syscall.STD_ERROR_HANDLE)
+		con = stderr
+	}
+
+	// correct output mode
+	if con != invalid {
+		// Make sure the console is configured to convert
+		// \n to \r\n, like Go programs expect.
+		h := windows.Handle(con)
+		var st uint32
+		err := windows.GetConsoleMode(h, &st)
+		if err != nil {
+			log.Printf("failed to get console mode: %s\n", err)
+		} else {
+			err = windows.SetConsoleMode(h, st&^windows.DISABLE_NEWLINE_AUTO_RETURN)
+			if err != nil {
+				log.Printf("failed to set console mode: %s\n", err)
+			}
+		}
+	}
+
+	// fix std handles to correct values (ie. redirects)
+	if stdin != invalid {
+		os.Stdin = os.NewFile(uintptr(stdin), "stdin")
+		log.Printf("fixed os.Stdin after attaching to parent console\n")
+	}
+	if stdout != invalid {
+		os.Stdout = os.NewFile(uintptr(stdout), "stdout")
+		log.Printf("fixed os.Stdout after attaching to parent console\n")
+	}
+	if stderr != invalid {
+		os.Stderr = os.NewFile(uintptr(stderr), "stderr")
+		log.Printf("fixed os.Stderr after attaching to parent console\n")
+	}
+
+	log.Println("attached to parent console")
+	return true, nil
+}
+
+func hideWindow(cmd *exec.Cmd) {
+	cmd.SysProcAttr = &syscall.SysProcAttr{
+		HideWindow: true,
+	}
+}
diff --git a/cmds/portmaster-start/dirs.go b/cmds/portmaster-start/dirs.go
new file mode 100644
index 00000000..e327963f
--- /dev/null
+++ b/cmds/portmaster-start/dirs.go
@@ -0,0 +1,42 @@
+package main
+
+import (
+	"fmt"
+	"log"
+	"os"
+
+	"github.com/spf13/cobra"
+)
+
+func init() {
+	rootCmd.AddCommand(cleanStructureCmd)
+}
+
+var cleanStructureCmd = &cobra.Command{
+	Use:   "clean-structure",
+	Short: "Create and clean the required directory structure",
+	RunE: func(cmd *cobra.Command, args []string) error {
+		if err := ensureLoggingDir(); err != nil {
+			return err
+		}
+		return cleanAndEnsureExecDir()
+	},
+}
+
+func cleanAndEnsureExecDir() error {
+	execDir := dataRoot.ChildDir("exec", 0o777)
+
+	// Clean up and remove exec dir.
+	err := os.RemoveAll(execDir.Path)
+	if err != nil {
+		log.Printf("WARNING: failed to fully remove exec dir (%q) for cleaning: %s", execDir.Path, err)
+	}
+
+	// Re-create exec dir.
+	err = execDir.Ensure()
+	if err != nil {
+		return fmt.Errorf("failed to initialize exec dir (%q): %w", execDir.Path, err)
+	}
+
+	return nil
+}
diff --git a/cmds/portmaster-start/install_windows.go b/cmds/portmaster-start/install_windows.go
new file mode 100644
index 00000000..5f1d5bb2
--- /dev/null
+++ b/cmds/portmaster-start/install_windows.go
@@ -0,0 +1,180 @@
+package main
+
+// Based on the official Go examples from
+// https://github.com/golang/sys/blob/master/windows/svc/example
+// by The Go Authors.
+// Original LICENSE (sha256sum: 2d36597f7117c38b006835ae7f537487207d8ec407aa9d9980794b2030cbc067) can be found in vendor/pkg cache directory.
+
+import (
+	"fmt"
+	"log"
+	"os"
+	"path/filepath"
+	"strings"
+	"time"
+
+	"github.com/spf13/cobra"
+	"golang.org/x/sys/windows"
+	"golang.org/x/sys/windows/svc"
+	"golang.org/x/sys/windows/svc/mgr"
+)
+
+func init() {
+	rootCmd.AddCommand(installCmd)
+	installCmd.AddCommand(installService)
+
+	rootCmd.AddCommand(uninstallCmd)
+	uninstallCmd.AddCommand(uninstallService)
+}
+
+var installCmd = &cobra.Command{
+	Use:   "install",
+	Short: "Install system integrations",
+}
+
+var uninstallCmd = &cobra.Command{
+	Use:   "uninstall",
+	Short: "Uninstall system integrations",
+}
+
+var installService = &cobra.Command{
+	Use:   "core-service",
+	Short: "Install Portmaster Core Windows Service",
+	RunE:  installWindowsService,
+}
+
+var uninstallService = &cobra.Command{
+	Use:   "core-service",
+	Short: "Uninstall Portmaster Core Windows Service",
+	PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
+		// non-nil dummy to override db flag requirement
+		return nil
+	},
+	RunE: uninstallWindowsService,
+}
+
+func getAbsBinaryPath() (string, error) {
+	p, err := filepath.Abs(os.Args[0])
+	if err != nil {
+		return "", err
+	}
+
+	return p, nil
+}
+
+func getServiceExecCommand(exePath string, escape bool) []string {
+	return []string{
+		maybeEscape(exePath, escape),
+		"core-service",
+		"--data",
+		maybeEscape(dataRoot.Path, escape),
+		"--input-signals",
+	}
+}
+
+func maybeEscape(s string, escape bool) string {
+	if escape {
+		return windows.EscapeArg(s)
+	}
+	return s
+}
+
+func getServiceConfig(exePath string) mgr.Config {
+	return mgr.Config{
+		ServiceType:    windows.SERVICE_WIN32_OWN_PROCESS,
+		StartType:      mgr.StartAutomatic,
+		ErrorControl:   mgr.ErrorNormal,
+		BinaryPathName: strings.Join(getServiceExecCommand(exePath, true), " "),
+		DisplayName:    "Portmaster Core",
+		Description:    "Portmaster Application Firewall - Core Service",
+	}
+}
+
+func getRecoveryActions() (recoveryActions []mgr.RecoveryAction, resetPeriod uint32) {
+	return []mgr.RecoveryAction{
+		{
+			Type:  mgr.ServiceRestart, // one of NoAction, ComputerReboot, ServiceRestart or RunCommand
+			Delay: 1 * time.Minute,    // the time to wait before performing the specified action
+		},
+	}, 86400
+}
+
+func installWindowsService(cmd *cobra.Command, args []string) error {
+	// get exe path
+	exePath, err := getAbsBinaryPath()
+	if err != nil {
+		return fmt.Errorf("failed to get exe path: %s", err)
+	}
+
+	// connect to Windows service manager
+	m, err := mgr.Connect()
+	if err != nil {
+		return fmt.Errorf("failed to connect to service manager: %s", err)
+	}
+	defer m.Disconnect() //nolint:errcheck // TODO
+
+	// open service
+	created := false
+	s, err := m.OpenService(serviceName)
+	if err != nil {
+		// create service
+		cmd := getServiceExecCommand(exePath, false)
+		s, err = m.CreateService(serviceName, cmd[0], getServiceConfig(exePath), cmd[1:]...)
+		if err != nil {
+			return fmt.Errorf("failed to create service: %s", err)
+		}
+		defer s.Close()
+		created = true
+	} else {
+		// update service
+		err = s.UpdateConfig(getServiceConfig(exePath))
+		if err != nil {
+			return fmt.Errorf("failed to update service: %s", err)
+		}
+		defer s.Close()
+	}
+
+	// update recovery actions
+	err = s.SetRecoveryActions(getRecoveryActions())
+	if err != nil {
+		return fmt.Errorf("failed to update recovery actions: %s", err)
+	}
+
+	if created {
+		log.Printf("created service %s\n", serviceName)
+	} else {
+		log.Printf("updated service %s\n", serviceName)
+	}
+
+	return nil
+}
+
+func uninstallWindowsService(cmd *cobra.Command, args []string) error {
+	// connect to Windows service manager
+	m, err := mgr.Connect()
+	if err != nil {
+		return err
+	}
+	defer m.Disconnect() //nolint:errcheck // we don't care if we failed to disconnect from the service manager, we're quitting anyway.
+
+	// open service
+	s, err := m.OpenService(serviceName)
+	if err != nil {
+		return fmt.Errorf("service %s is not installed", serviceName)
+	}
+	defer s.Close()
+
+	_, err = s.Control(svc.Stop)
+	if err != nil {
+		log.Printf("failed to stop service: %s\n", err)
+	}
+
+	// delete service
+	err = s.Delete()
+	if err != nil {
+		return fmt.Errorf("failed to delete service: %s", err)
+	}
+
+	log.Printf("uninstalled service %s\n", serviceName)
+	return nil
+}
diff --git a/cmds/portmaster-start/lock.go b/cmds/portmaster-start/lock.go
new file mode 100644
index 00000000..0526084c
--- /dev/null
+++ b/cmds/portmaster-start/lock.go
@@ -0,0 +1,109 @@
+package main
+
+import (
+	"errors"
+	"fmt"
+	"io/fs"
+	"log"
+	"os"
+	"os/user"
+	"path/filepath"
+	"strconv"
+	"strings"
+
+	processInfo "github.com/shirou/gopsutil/process"
+)
+
+func checkAndCreateInstanceLock(path, name string, perUser bool) (pid int32, err error) {
+	lockFilePath := getLockFilePath(path, name, perUser)
+
+	// read current pid file
+	data, err := os.ReadFile(lockFilePath)
+	if err != nil {
+		if errors.Is(err, fs.ErrNotExist) {
+			// create new lock
+			return 0, createInstanceLock(lockFilePath)
+		}
+		return 0, err
+	}
+
+	// file exists!
+	parsedPid, err := strconv.ParseInt(strings.TrimSpace(string(data)), 10, 64)
+	if err != nil {
+		log.Printf("failed to parse existing lock pid file (ignoring): %s\n", err)
+		return 0, createInstanceLock(lockFilePath)
+	}
+
+	// Check if process exists.
+	p, err := processInfo.NewProcess(int32(parsedPid))
+	switch {
+	case err == nil:
+		// Process exists, continue.
+	case errors.Is(err, processInfo.ErrorProcessNotRunning):
+		// A process with the locked PID does not exist.
+		// This is expected, so we can continue normally.
+		return 0, createInstanceLock(lockFilePath)
+	default:
+		// There was an internal error getting the process.
+		return 0, err
+	}
+
+	// Get the process paths and evaluate and clean them.
+	executingBinaryPath, err := p.Exe()
+	if err != nil {
+		return 0, fmt.Errorf("failed to get path of existing process: %w", err)
+	}
+	cleanedExecutingBinaryPath, err := filepath.EvalSymlinks(executingBinaryPath)
+	if err != nil {
+		return 0, fmt.Errorf("failed to evaluate path of existing process: %w", err)
+	}
+
+	// Check if the binary is portmaster-start with high probability.
+	if !strings.Contains(filepath.Base(cleanedExecutingBinaryPath), "portmaster-start") {
+		// The process with the locked PID belongs to another binary.
+		// As the Portmaster usually starts very early, it will have a low PID,
+		// which could be assigned to another process on next boot.
+		return 0, createInstanceLock(lockFilePath)
+	}
+
+	// Return PID of already running instance.
+	return p.Pid, nil
+}
+
+func createInstanceLock(lockFilePath string) error {
+	// check data root dir
+	err := dataRoot.Ensure()
+	if err != nil {
+		log.Printf("failed to check data root dir: %s\n", err)
+	}
+
+	// create lock file
+	// TODO: Investigate required permissions.
+	err = os.WriteFile(lockFilePath, []byte(strconv.Itoa(os.Getpid())), 0o0666) //nolint:gosec
+	if err != nil {
+		return err
+	}
+
+	return nil
+}
+
+func deleteInstanceLock(path, name string, perUser bool) error {
+	return os.Remove(getLockFilePath(path, name, perUser))
+}
+
+func getLockFilePath(path, name string, perUser bool) string {
+	if !perUser {
+		return filepath.Join(dataRoot.Path, path, fmt.Sprintf("%s-lock.pid", name))
+	}
+
+	// Get user ID for per-user lock file.
+	var userID string
+	usr, err := user.Current()
+	if err != nil {
+		log.Printf("failed to get current user: %s\n", err)
+		userID = "no-user"
+	} else {
+		userID = usr.Uid
+	}
+	return filepath.Join(dataRoot.Path, path, fmt.Sprintf("%s-%s-lock.pid", name, userID))
+}
diff --git a/cmds/portmaster-start/logs.go b/cmds/portmaster-start/logs.go
new file mode 100644
index 00000000..f2f514b9
--- /dev/null
+++ b/cmds/portmaster-start/logs.go
@@ -0,0 +1,127 @@
+package main
+
+import (
+	"fmt"
+	"log"
+	"os"
+	"path/filepath"
+	"runtime"
+	"time"
+
+	"github.com/spf13/cobra"
+
+	"github.com/safing/portmaster/base/database/record"
+	"github.com/safing/portmaster/base/info"
+	"github.com/safing/structures/container"
+	"github.com/safing/structures/dsd"
+)
+
+func initializeLogFile(logFilePath string, identifier string, version string) *os.File {
+	logFile, err := os.OpenFile(logFilePath, os.O_RDWR|os.O_CREATE, 0o0440) //nolint:gosec // As desired.
+	if err != nil {
+		log.Printf("failed to create log file %s: %s\n", logFilePath, err)
+		return nil
+	}
+
+	// create header, so that the portmaster can view log files as a database
+	meta := record.Meta{}
+	meta.Update()
+	meta.SetAbsoluteExpiry(time.Now().Add(720 * time.Hour).Unix()) // one month
+
+	// manually marshal
+	// version
+	c := container.New([]byte{1})
+	// meta
+	metaSection, err := dsd.Dump(meta, dsd.JSON)
+	if err != nil {
+		log.Printf("failed to serialize header for log file %s: %s\n", logFilePath, err)
+		finalizeLogFile(logFile)
+		return nil
+	}
+	c.AppendAsBlock(metaSection)
+	// log file data type (string) and newline for better manual viewing
+	c.Append([]byte("S\n"))
+	c.Append([]byte(fmt.Sprintf("executing %s version %s on %s %s\n", identifier, version, runtime.GOOS, runtime.GOARCH)))
+
+	_, err = logFile.Write(c.CompileData())
+	if err != nil {
+		log.Printf("failed to write header for log file %s: %s\n", logFilePath, err)
+		finalizeLogFile(logFile)
+		return nil
+	}
+
+	return logFile
+}
+
+func finalizeLogFile(logFile *os.File) {
+	logFilePath := logFile.Name()
+
+	err := logFile.Close()
+	if err != nil {
+		log.Printf("failed to close log file %s: %s\n", logFilePath, err)
+	}
+
+	// check file size
+	stat, err := os.Stat(logFilePath)
+	if err != nil {
+		return
+	}
+
+	// delete if file is smaller than
+	if stat.Size() >= 200 { // header + info is about 150 bytes
+		return
+	}
+
+	if err := os.Remove(logFilePath); err != nil {
+		log.Printf("failed to delete empty log file %s: %s\n", logFilePath, err)
+	}
+}
+
+func getLogFile(options *Options, version, ext string) *os.File {
+	// check logging dir
+	logFileBasePath := filepath.Join(logsRoot.Path, options.ShortIdentifier)
+	err := logsRoot.EnsureAbsPath(logFileBasePath)
+	if err != nil {
+		log.Printf("failed to check/create log file folder %s: %s\n", logFileBasePath, err)
+	}
+
+	// open log file
+	logFilePath := filepath.Join(logFileBasePath, fmt.Sprintf("%s%s", time.Now().UTC().Format("2006-01-02-15-04-05"), ext))
+	return initializeLogFile(logFilePath, options.Identifier, version)
+}
+
+func getPmStartLogFile(ext string) *os.File {
+	return getLogFile(&Options{
+		ShortIdentifier: "start",
+		Identifier:      "start/portmaster-start",
+	}, info.Version(), ext)
+}
+
+//nolint:unused // false positive on linux, currently used by windows only. TODO: move to a _windows file.
+func logControlError(cErr error) {
+	// check if error present
+	if cErr == nil {
+		return
+	}
+
+	errorFile := getPmStartLogFile(".error.log")
+	if errorFile == nil {
+		return
+	}
+	defer func() {
+		_ = errorFile.Close()
+	}()
+
+	fmt.Fprintln(errorFile, cErr.Error())
+}
+
+//nolint:deadcode,unused // false positive on linux, currently used by windows only. TODO: move to a _windows file.
+func runAndLogControlError(wrappedFunc func(cmd *cobra.Command, args []string) error) func(cmd *cobra.Command, args []string) error {
+	return func(cmd *cobra.Command, args []string) error {
+		err := wrappedFunc(cmd, args)
+		if err != nil {
+			logControlError(err)
+		}
+		return err
+	}
+}
diff --git a/cmds/portmaster-start/main.go b/cmds/portmaster-start/main.go
new file mode 100644
index 00000000..f764dfbf
--- /dev/null
+++ b/cmds/portmaster-start/main.go
@@ -0,0 +1,257 @@
+package main
+
+import (
+	"context"
+	"errors"
+	"fmt"
+	"log"
+	"net/url"
+	"os"
+	"os/signal"
+	"path/filepath"
+	"runtime"
+	"strings"
+	"syscall"
+
+	"github.com/spf13/cobra"
+
+	"github.com/safing/portmaster/base/dataroot"
+	"github.com/safing/portmaster/base/info"
+	portlog "github.com/safing/portmaster/base/log"
+	"github.com/safing/portmaster/base/updater"
+	"github.com/safing/portmaster/base/utils"
+	"github.com/safing/portmaster/service/updates/helper"
+)
+
+var (
+	dataDir    string
+	maxRetries int
+	dataRoot   *utils.DirStructure
+	logsRoot   *utils.DirStructure
+	forceOldUI bool
+
+	updateURLFlag string
+	userAgentFlag string
+
+	// Create registry.
+	registry = &updater.ResourceRegistry{
+		Name: "updates",
+		UpdateURLs: []string{
+			"https://updates.safing.io",
+		},
+		UserAgent:    fmt.Sprintf("Portmaster Start (%s %s)", runtime.GOOS, runtime.GOARCH),
+		Verification: helper.VerificationConfig,
+		DevMode:      false,
+		Online:       true, // is disabled later based on command
+	}
+
+	rootCmd = &cobra.Command{
+		Use:   "portmaster-start",
+		Short: "Start Portmaster components",
+		PersistentPreRunE: func(cmd *cobra.Command, args []string) (err error) {
+			mustLoadIndex := indexRequired(cmd)
+			if err := configureRegistry(mustLoadIndex); err != nil {
+				return err
+			}
+
+			if err := ensureLoggingDir(); err != nil {
+				return err
+			}
+
+			return nil
+		},
+		SilenceUsage: true,
+	}
+)
+
+func init() {
+	// Let cobra ignore if we are running as "GUI" or not
+	cobra.MousetrapHelpText = ""
+
+	flags := rootCmd.PersistentFlags()
+	{
+		flags.StringVar(&dataDir, "data", "", "Configures the data directory. Alternatively, this can also be set via the environment variable PORTMASTER_DATA.")
+		flags.StringVar(&updateURLFlag, "update-server", "", "Set an alternative update server (full URL)")
+		flags.StringVar(&userAgentFlag, "update-agent", "", "Set an alternative user agent for requests to the update server")
+		flags.IntVar(&maxRetries, "max-retries", 5, "Maximum number of retries when starting a Portmaster component")
+		flags.BoolVar(&stdinSignals, "input-signals", false, "Emulate signals using stdin.")
+		flags.BoolVar(&forceOldUI, "old-ui", false, "Use the old ui. (Beta)")
+		_ = rootCmd.MarkPersistentFlagDirname("data")
+		_ = flags.MarkHidden("input-signals")
+	}
+}
+
+func main() {
+	cobra.OnInitialize(initCobra)
+
+	// set meta info
+	info.Set("Portmaster Start", "", "GPLv3")
+
+	// catch interrupt for clean shutdown
+	signalCh := make(chan os.Signal, 2)
+	signal.Notify(
+		signalCh,
+		os.Interrupt,
+		syscall.SIGHUP,
+		syscall.SIGINT,
+		syscall.SIGTERM,
+		syscall.SIGQUIT,
+	)
+
+	// start root command
+	go func() {
+		if err := rootCmd.Execute(); err != nil {
+			os.Exit(1)
+		}
+		os.Exit(0)
+	}()
+
+	// wait for signals
+	for sig := range signalCh {
+		if childIsRunning.IsSet() {
+			log.Printf("got %s signal (ignoring), waiting for child to exit...\n", sig)
+			continue
+		}
+
+		log.Printf("got %s signal, exiting... (not executing anything)\n", sig)
+		os.Exit(0)
+	}
+}
+
+func initCobra() {
+	// check if we are running in a console (try to attach to parent console if available)
+	var err error
+	runningInConsole, err = attachToParentConsole()
+	if err != nil {
+		log.Fatalf("failed to attach to parent console: %s\n", err)
+	}
+
+	// check if meta info is ok
+	err = info.CheckVersion()
+	if err != nil {
+		log.Fatalf("compile error: please compile using the provided build script")
+	}
+
+	// set up logging
+	log.SetFlags(log.Ldate | log.Ltime | log.LUTC)
+	log.SetPrefix("[pmstart] ")
+	log.SetOutput(os.Stdout)
+
+	// not using portbase logger
+	portlog.SetLogLevel(portlog.CriticalLevel)
+}
+
+func configureRegistry(mustLoadIndex bool) error {
+	// Check if update server URL supplied via flag is a valid URL.
+	if updateURLFlag != "" {
+		u, err := url.Parse(updateURLFlag)
+		if err != nil {
+			return fmt.Errorf("supplied update server URL is invalid: %w", err)
+		}
+		if u.Scheme != "https" {
+			return errors.New("supplied update server URL must use HTTPS")
+		}
+	}
+
+	// Override values from flags.
+	if userAgentFlag != "" {
+		registry.UserAgent = userAgentFlag
+	}
+	if updateURLFlag != "" {
+		registry.UpdateURLs = []string{updateURLFlag}
+	}
+
+	// If dataDir is not set, check the environment variable.
+	if dataDir == "" {
+		dataDir = os.Getenv("PORTMASTER_DATA")
+	}
+
+	// If it's still empty, try to auto-detect it.
+	if dataDir == "" {
+		dataDir = detectInstallationDir()
+	}
+
+	// Finally, if it's still empty, the user must provide it.
+	if dataDir == "" {
+		return errors.New("please set the data directory using --data=/path/to/data/dir")
+	}
+
+	// Remove left over quotes.
+	dataDir = strings.Trim(dataDir, `\"`)
+	// Initialize data root.
+	err := dataroot.Initialize(dataDir, 0o0755)
+	if err != nil {
+		return fmt.Errorf("failed to initialize data root: %w", err)
+	}
+	dataRoot = dataroot.Root()
+
+	// Initialize registry.
+	err = registry.Initialize(dataRoot.ChildDir("updates", 0o0755))
+	if err != nil {
+		return err
+	}
+
+	return updateRegistryIndex(mustLoadIndex)
+}
+
+func ensureLoggingDir() error {
+	// set up logs root
+	logsRoot = dataRoot.ChildDir("logs", 0o0777)
+	err := logsRoot.Ensure()
+	if err != nil {
+		return fmt.Errorf("failed to initialize logs root (%q): %w", logsRoot.Path, err)
+	}
+
+	// warn about CTRL-C on windows
+	if runningInConsole && onWindows {
+		log.Println("WARNING: portmaster-start is marked as a GUI application in order to get rid of the console window.")
+		log.Println("WARNING: CTRL-C will immediately kill without clean shutdown.")
+	}
+	return nil
+}
+
+func updateRegistryIndex(mustLoadIndex bool) error {
+	// Set indexes based on the release channel.
+	warning := helper.SetIndexes(registry, "", false, false, false)
+	if warning != nil {
+		log.Printf("WARNING: %s\n", warning)
+	}
+
+	// Load indexes from disk or network, if needed and desired.
+	err := registry.LoadIndexes(context.Background())
+	if err != nil {
+		log.Printf("WARNING: error loading indexes: %s\n", err)
+		if mustLoadIndex {
+			return err
+		}
+	}
+
+	// Load versions from disk to know which others we have and which are available.
+	err = registry.ScanStorage("")
+	if err != nil {
+		log.Printf("WARNING: error during storage scan: %s\n", err)
+	}
+
+	registry.SelectVersions()
+	return nil
+}
+
+func detectInstallationDir() string {
+	exePath, err := filepath.Abs(os.Args[0])
+	if err != nil {
+		return ""
+	}
+
+	parent := filepath.Dir(exePath)
+	stableJSONFile := filepath.Join(parent, "updates", "stable.json")
+	stat, err := os.Stat(stableJSONFile)
+	if err != nil {
+		return ""
+	}
+
+	if stat.IsDir() {
+		return ""
+	}
+
+	return parent
+}
diff --git a/cmds/portmaster-start/pack b/cmds/portmaster-start/pack
new file mode 100755
index 00000000..1cf6ca2a
--- /dev/null
+++ b/cmds/portmaster-start/pack
@@ -0,0 +1,123 @@
+#!/bin/bash
+
+baseDir="$( cd "$(dirname "$0")" && pwd )"
+cd "$baseDir"
+
+COL_OFF="\033[0m"
+COL_BOLD="\033[01;01m"
+COL_RED="\033[31m"
+COL_GREEN="\033[32m"
+COL_YELLOW="\033[33m"
+
+destDirPart1="../../dist"
+destDirPart2="start"
+
+function prep {
+  # output
+  output="portmaster-start"
+  # get version
+  version=$(grep "info.Set" main.go | cut -d'"' -f4)
+  # build versioned file name
+  filename="portmaster-start_v${version//./-}"
+  # platform
+  platform="${GOOS}_${GOARCH}"
+  if [[ $GOOS == "windows" ]]; then
+    filename="${filename}.exe"
+    output="${output}.exe"
+  fi
+  # build destination path
+  destPath=${destDirPart1}/${platform}/${destDirPart2}/$filename
+}
+
+function check {
+  prep
+
+  # check if file exists
+  if [[ -f $destPath ]]; then
+    echo "[start] $platform $version already built"
+  else
+    echo -e "${COL_BOLD}[start] $platform v$version${COL_OFF}"
+  fi
+}
+
+function build {
+  prep
+
+  # check if file exists
+  if [[ -f $destPath ]]; then
+    echo "[start] $platform already built in v$version, skipping..."
+    return
+  fi
+
+  # build
+  ./build
+  if [[ $? -ne 0 ]]; then
+    echo -e "\n${COL_BOLD}[start] $platform v$version: ${COL_RED}BUILD FAILED.${COL_OFF}"
+    exit 1
+  fi
+  mkdir -p $(dirname $destPath)
+  cp $output $destPath
+  echo -e "\n${COL_BOLD}[start] $platform v$version: ${COL_GREEN}successfully built.${COL_OFF}"
+}
+
+function reset {
+  prep
+
+  # delete if file exists
+  if [[ -f $destPath ]]; then
+    rm $destPath
+    echo "[start] $platform v$version deleted."
+  fi
+}
+
+function check_all {
+  GOOS=linux GOARCH=amd64 check
+  GOOS=windows GOARCH=amd64 check
+  GOOS=darwin GOARCH=amd64 check
+  GOOS=linux GOARCH=arm64 check
+  GOOS=windows GOARCH=arm64 check
+  GOOS=darwin GOARCH=arm64 check
+}
+
+function build_all {
+  GOOS=linux GOARCH=amd64 build
+  GOOS=windows GOARCH=amd64 build
+  GOOS=darwin GOARCH=amd64 build
+  GOOS=linux GOARCH=arm64 build
+  GOOS=windows GOARCH=arm64 build
+  GOOS=darwin GOARCH=arm64 build
+}
+
+function reset_all {
+  GOOS=linux GOARCH=amd64 reset
+  GOOS=windows GOARCH=amd64 reset
+  GOOS=darwin GOARCH=amd64 reset
+  GOOS=linux GOARCH=arm64 reset
+  GOOS=windows GOARCH=arm64 reset
+  GOOS=darwin GOARCH=arm64 reset
+}
+
+case $1 in
+  "check" )
+    check_all
+    ;;
+  "build" )
+    build_all
+    ;;
+  "reset" )
+    reset_all
+    ;;
+  * )
+    echo ""
+    echo "build list:"
+    echo ""
+    check_all
+    echo ""
+    read -p "press [Enter] to start building" x
+    echo ""
+    build_all
+    echo ""
+    echo "finished building."
+    echo ""
+    ;;
+esac
diff --git a/cmds/portmaster-start/recover_linux.go b/cmds/portmaster-start/recover_linux.go
new file mode 100644
index 00000000..96719bd8
--- /dev/null
+++ b/cmds/portmaster-start/recover_linux.go
@@ -0,0 +1,82 @@
+package main
+
+import (
+	"errors"
+	"fmt"
+	"os"
+	"strings"
+
+	"github.com/hashicorp/go-multierror"
+	"github.com/spf13/cobra"
+
+	"github.com/safing/portmaster/service/firewall/interception"
+)
+
+var recoverIPTablesCmd = &cobra.Command{
+	Use:   "recover-iptables",
+	Short: "Removes obsolete IP tables rules in case of an unclean shutdown",
+	RunE: func(*cobra.Command, []string) error {
+		// interception.DeactiveNfqueueFirewall uses coreos/go-iptables
+		// which shells out to the /sbin/iptables binary. As a result,
+		// we don't get the errno of the actual error and need to parse the
+		// output instead. Make sure it's always english by setting LC_ALL=C
+		currentLocale := os.Getenv("LC_ALL")
+		_ = os.Setenv("LC_ALL", "C")
+		defer func() {
+			_ = os.Setenv("LC_ALL", currentLocale)
+		}()
+
+		err := interception.DeactivateNfqueueFirewall()
+		if err == nil {
+			return nil
+		}
+
+		// we don't want to show ErrNotExists to the user
+		// as that only means portmaster did the cleanup itself.
+		var mr *multierror.Error
+		if !errors.As(err, &mr) {
+			return err
+		}
+
+		var filteredErrors *multierror.Error
+		for _, err := range mr.Errors {
+			// if we have a permission denied error, all errors will be the same
+			if strings.Contains(err.Error(), "Permission denied") {
+				return fmt.Errorf("failed to cleanup iptables: %w", os.ErrPermission)
+			}
+
+			if !strings.Contains(err.Error(), "No such file or directory") {
+				filteredErrors = multierror.Append(filteredErrors, err)
+			}
+		}
+
+		if filteredErrors != nil {
+			filteredErrors.ErrorFormat = formatNfqErrors
+			return filteredErrors.ErrorOrNil()
+		}
+
+		return nil
+	},
+	SilenceUsage: true,
+}
+
+func init() {
+	rootCmd.AddCommand(recoverIPTablesCmd)
+}
+
+func formatNfqErrors(es []error) string {
+	if len(es) == 1 {
+		return fmt.Sprintf("1 error occurred:\n\t* %s\n\n", es[0])
+	}
+
+	points := make([]string, len(es))
+	for i, err := range es {
+		// only display the very first line of each error
+		first := strings.Split(err.Error(), "\n")[0]
+		points[i] = fmt.Sprintf("* %s", first)
+	}
+
+	return fmt.Sprintf(
+		"%d errors occurred:\n\t%s\n\n",
+		len(es), strings.Join(points, "\n\t"))
+}
diff --git a/cmds/portmaster-start/run.go b/cmds/portmaster-start/run.go
new file mode 100644
index 00000000..f807fd69
--- /dev/null
+++ b/cmds/portmaster-start/run.go
@@ -0,0 +1,486 @@
+package main
+
+import (
+	"errors"
+	"fmt"
+	"io"
+	"log"
+	"os"
+	"os/exec"
+	"path"
+	"path/filepath"
+	"runtime"
+	"strings"
+	"time"
+
+	"github.com/spf13/cobra"
+	"github.com/tevino/abool"
+
+	"github.com/safing/portmaster/service/updates/helper"
+)
+
+const (
+	// RestartExitCode is the exit code that any service started by portmaster-start
+	// can return in order to trigger a restart after a clean shutdown.
+	RestartExitCode = 23
+
+	// ControlledFailureExitCode is the exit code that any service started by
+	// portmaster-start can return in order to signify a controlled failure.
+	// This disables retrying and exits with an error code.
+	ControlledFailureExitCode = 24
+
+	// StartOldUIExitCode is an exit code that is returned by the UI when there. This is manfully triaged by the user, if the new UI does not work for them.
+	StartOldUIExitCode        = 77
+	MissingDependencyExitCode = 0xc0000135 // Windows STATUS_DLL_NOT_FOUND
+
+	exeSuffix = ".exe"
+	zipSuffix = ".zip"
+)
+
+var (
+	runningInConsole bool
+	onWindows        = runtime.GOOS == "windows"
+	stdinSignals     bool
+	childIsRunning   = abool.NewBool(false)
+
+	fallBackToOldUI bool = false
+)
+
+// Options for starting component.
+type Options struct {
+	Name              string
+	Identifier        string // component identifier
+	ShortIdentifier   string // populated automatically
+	LockPathPrefix    string
+	LockPerUser       bool
+	PIDFile           bool
+	SuppressArgs      bool // do not use any args
+	AllowDownload     bool // allow download of component if it is not yet available
+	AllowHidingWindow bool // allow hiding the window of the subprocess
+	NoOutput          bool // do not use stdout/err if logging to file is available (did not fail to open log file)
+	RestartOnFail     bool // Try restarting automatically, if the started component fails.
+}
+
+// This is a temp value that will be used to test the new UI in beta.
+var app2Options = Options{
+	Name:              "Portmaster App2",
+	Identifier:        "app2/portmaster-app",
+	AllowDownload:     false,
+	AllowHidingWindow: false,
+	RestartOnFail:     true,
+}
+
+func init() {
+	// Make sure the new UI has a proper extension.
+	if onWindows {
+		app2Options.Identifier += ".zip"
+	}
+
+	registerComponent([]Options{
+		{
+			Name:              "Portmaster Core",
+			Identifier:        "core/portmaster-core",
+			AllowDownload:     true,
+			AllowHidingWindow: true,
+			PIDFile:           true,
+			RestartOnFail:     true,
+		},
+		{
+			Name:              "Portmaster App",
+			Identifier:        "app/portmaster-app.zip",
+			AllowDownload:     false,
+			AllowHidingWindow: false,
+			RestartOnFail:     true,
+		},
+		{
+			Name:              "Portmaster Notifier",
+			Identifier:        "notifier/portmaster-notifier",
+			LockPerUser:       true,
+			AllowDownload:     false,
+			AllowHidingWindow: true,
+			PIDFile:           true,
+			LockPathPrefix:    "exec",
+		},
+		{
+			Name:              "Safing Privacy Network",
+			Identifier:        "hub/spn-hub",
+			AllowDownload:     true,
+			AllowHidingWindow: true,
+			PIDFile:           true,
+			RestartOnFail:     true,
+		},
+		app2Options,
+	})
+}
+
+func registerComponent(opts []Options) {
+	for idx := range opts {
+		opt := &opts[idx] // we need a copy
+		if opt.ShortIdentifier == "" {
+			opt.ShortIdentifier = path.Dir(opt.Identifier)
+		}
+
+		rootCmd.AddCommand(
+			&cobra.Command{
+				Use:   opt.ShortIdentifier,
+				Short: "Run the " + opt.Name,
+				RunE: func(cmd *cobra.Command, args []string) error {
+					err := run(opt, args)
+					initiateShutdown(err)
+					return err
+				},
+			},
+		)
+
+		showCmd.AddCommand(
+			&cobra.Command{
+				Use:   opt.ShortIdentifier,
+				Short: "Show command to execute the " + opt.Name,
+				RunE: func(cmd *cobra.Command, args []string) error {
+					return show(opt, args)
+				},
+			},
+		)
+	}
+}
+
+func getExecArgs(opts *Options, cmdArgs []string) []string {
+	if opts.SuppressArgs {
+		return nil
+	}
+
+	args := []string{"--data", dataDir}
+	if stdinSignals {
+		args = append(args, "--input-signals")
+	}
+
+	if runtime.GOOS == "linux" && opts.ShortIdentifier == "app" {
+		// see https://www.freedesktop.org/software/systemd/man/pam_systemd.html#type=
+		if xdgSessionType := os.Getenv("XDG_SESSION_TYPE"); xdgSessionType == "wayland" {
+			// we're running the Portmaster UI App under Wayland so make sure we add some arguments
+			// required by Electron.
+			args = append(args,
+				[]string{
+					"--enable-features=UseOzonePlatform,WaylandWindowDecorations",
+					"--ozone-platform=wayland",
+				}...,
+			)
+		}
+	}
+
+	args = append(args, cmdArgs...)
+	return args
+}
+
+func run(opts *Options, cmdArgs []string) (err error) {
+	// set download option
+	registry.Online = opts.AllowDownload
+
+	if isShuttingDown() {
+		return nil
+	}
+
+	// check for duplicate instances
+	if opts.PIDFile {
+		pid, err := checkAndCreateInstanceLock(opts.LockPathPrefix, opts.ShortIdentifier, opts.LockPerUser)
+		if err != nil {
+			return fmt.Errorf("failed to exec lock: %w", err)
+		}
+		if pid != 0 {
+			return fmt.Errorf("another instance of %s is already running: PID %d", opts.Name, pid)
+		}
+		defer func() {
+			err := deleteInstanceLock(opts.LockPathPrefix, opts.ShortIdentifier, opts.LockPerUser)
+			if err != nil {
+				log.Printf("failed to delete instance lock: %s\n", err)
+			}
+		}()
+	}
+
+	// notify service after some time
+	go func() {
+		// assume that after 3 seconds service has finished starting
+		time.Sleep(3 * time.Second)
+		startupComplete <- struct{}{}
+	}()
+
+	// adapt identifier
+	if onWindows && !strings.HasSuffix(opts.Identifier, zipSuffix) {
+		opts.Identifier += exeSuffix
+	}
+
+	// setup logging
+	// init log file
+	logFile := getPmStartLogFile(".log")
+	if logFile != nil {
+		// don't close logFile, will be closed by system
+		if opts.NoOutput {
+			log.Println("disabling log output to stdout... bye!")
+			log.SetOutput(logFile)
+		} else {
+			log.SetOutput(io.MultiWriter(os.Stdout, logFile))
+		}
+	}
+
+	return runAndRestart(opts, cmdArgs)
+}
+
+func runAndRestart(opts *Options, args []string) error {
+	tries := 0
+	for {
+		tryAgain, err := execute(opts, args)
+		if err != nil {
+			log.Printf("%s failed with: %s\n", opts.Identifier, err)
+			tries++
+			if tries >= maxRetries {
+				log.Printf("encountered %d consecutive errors, giving up ...", tries)
+				return err
+			}
+		} else {
+			tries = 0
+			log.Printf("%s exited without error", opts.Identifier)
+		}
+
+		if !opts.RestartOnFail || !tryAgain {
+			return err
+		}
+
+		// if a restart was requested `tries` is set to 0 so
+		// this becomes a no-op.
+		time.Sleep(time.Duration(2*tries) * time.Second)
+
+		if tries >= 2 || err == nil {
+			// if we are constantly failing or a restart was requested
+			// try to update the resources.
+			log.Printf("updating registry index")
+			_ = updateRegistryIndex(false) // will always return nil
+		}
+	}
+}
+
+func fixExecPerm(path string) error {
+	if onWindows {
+		return nil
+	}
+
+	info, err := os.Stat(path)
+	if err != nil {
+		return fmt.Errorf("failed to stat %s: %w", path, err)
+	}
+
+	if info.Mode() == 0o0755 {
+		return nil
+	}
+
+	if err := os.Chmod(path, 0o0755); err != nil { //nolint:gosec // Set execution rights.
+		return fmt.Errorf("failed to chmod %s: %w", path, err)
+	}
+
+	return nil
+}
+
+func copyLogs(opts *Options, consoleSink io.Writer, version, ext string, logSource io.Reader, notifier chan<- struct{}) {
+	defer func() { notifier <- struct{}{} }()
+
+	sink := consoleSink
+
+	fileSink := getLogFile(opts, version, ext)
+	if fileSink != nil {
+		defer finalizeLogFile(fileSink)
+		if opts.NoOutput {
+			sink = fileSink
+		} else {
+			sink = io.MultiWriter(consoleSink, fileSink)
+		}
+	}
+
+	if bytes, err := io.Copy(sink, logSource); err != nil {
+		log.Printf("%s: writing logs failed after %d bytes: %s", fileSink.Name(), bytes, err)
+	}
+}
+
+func persistOutputStreams(opts *Options, version string, cmd *exec.Cmd) (chan struct{}, error) {
+	var (
+		done         = make(chan struct{})
+		copyNotifier = make(chan struct{}, 2)
+	)
+
+	stdout, err := cmd.StdoutPipe()
+	if err != nil {
+		return nil, fmt.Errorf("failed to connect stdout: %w", err)
+	}
+
+	stderr, err := cmd.StderrPipe()
+	if err != nil {
+		return nil, fmt.Errorf("failed to connect stderr: %w", err)
+	}
+
+	go copyLogs(opts, os.Stdout, version, ".log", stdout, copyNotifier)
+	go copyLogs(opts, os.Stderr, version, ".error.log", stderr, copyNotifier)
+
+	go func() {
+		<-copyNotifier
+		<-copyNotifier
+		close(copyNotifier)
+		close(done)
+	}()
+
+	return done, nil
+}
+
+func execute(opts *Options, args []string) (cont bool, err error) {
+	// Auto-upgrade to new UI if in beta and new UI is not disabled or failed.
+	if opts.ShortIdentifier == "app" &&
+		registry.UsePreReleases &&
+		!forceOldUI &&
+		!fallBackToOldUI {
+		log.Println("auto-upgraded to new UI")
+		opts = &app2Options
+	}
+
+	// Compile arguments and add additional arguments based on system configuration.
+	// Extra parameters can be specified using "-- --some-parameter".
+	args = getExecArgs(opts, args)
+
+	file, err := registry.GetFile(
+		helper.PlatformIdentifier(opts.Identifier),
+	)
+	if err != nil {
+		return true, fmt.Errorf("could not get component: %w", err)
+	}
+	binPath := file.Path()
+
+	// Adapt path for packaged software.
+	if strings.HasSuffix(binPath, zipSuffix) {
+		// Remove suffix from binary path.
+		binPath = strings.TrimSuffix(binPath, zipSuffix)
+		// Add binary with the same name to access the unpacked binary.
+		binPath = filepath.Join(binPath, filepath.Base(binPath))
+
+		// Adapt binary path on Windows.
+		if onWindows {
+			binPath += exeSuffix
+		}
+	}
+
+	// check permission
+	if err := fixExecPerm(binPath); err != nil {
+		return true, err
+	}
+
+	log.Printf("starting %s %s\n", binPath, strings.Join(args, " "))
+
+	// create command
+	exc := exec.Command(binPath, args...)
+
+	if !runningInConsole && opts.AllowHidingWindow {
+		// Windows only:
+		// only hide (all) windows of program if we are not running in console and windows may be hidden
+		hideWindow(exc)
+	}
+
+	outputsWritten, err := persistOutputStreams(opts, file.Version(), exc)
+	if err != nil {
+		return true, err
+	}
+
+	interrupt, err := getProcessSignalFunc(exc)
+	if err != nil {
+		return true, err
+	}
+
+	err = exc.Start()
+	if err != nil {
+		return true, fmt.Errorf("failed to start %s: %w", opts.Identifier, err)
+	}
+	childIsRunning.Set()
+
+	// wait for completion
+	finished := make(chan error, 1)
+	go func() {
+		defer close(finished)
+
+		<-outputsWritten
+		// wait for process to return
+		finished <- exc.Wait()
+		// update status
+		childIsRunning.UnSet()
+	}()
+
+	// state change listeners
+	select {
+	case <-shuttingDown:
+		if err := interrupt(); err != nil {
+			log.Printf("failed to signal %s to shutdown: %s\n", opts.Identifier, err)
+			err = exc.Process.Kill()
+			if err != nil {
+				return false, fmt.Errorf("failed to kill %s: %w", opts.Identifier, err)
+			}
+			return false, fmt.Errorf("killed %s", opts.Identifier)
+		}
+
+		// wait until shut down
+		select {
+		case <-finished:
+		case <-time.After(3 * time.Minute): // portmaster core prints stack if not able to shutdown in 3 minutes, give it one more ...
+			err = exc.Process.Kill()
+			if err != nil {
+				return false, fmt.Errorf("failed to kill %s: %w", opts.Identifier, err)
+			}
+			return false, fmt.Errorf("killed %s", opts.Identifier)
+		}
+		return false, nil
+
+	case err := <-finished:
+		return parseExitError(err)
+	}
+}
+
+func getProcessSignalFunc(cmd *exec.Cmd) (func() error, error) {
+	if stdinSignals {
+		stdin, err := cmd.StdinPipe()
+		if err != nil {
+			return nil, fmt.Errorf("failed to connect stdin: %w", err)
+		}
+
+		return func() error {
+			_, err := fmt.Fprintln(stdin, "SIGINT")
+			return err
+		}, nil
+	}
+
+	return func() error {
+		return cmd.Process.Signal(os.Interrupt)
+	}, nil
+}
+
+func parseExitError(err error) (restart bool, errWithCtx error) {
+	if err == nil {
+		// clean and coordinated exit
+		return false, nil
+	}
+
+	var exErr *exec.ExitError
+	if errors.As(err, &exErr) {
+		switch exErr.ProcessState.ExitCode() {
+		case 0:
+			return false, fmt.Errorf("clean exit with error: %w", err)
+		case 1:
+			return true, fmt.Errorf("error during execution: %w", err)
+		case RestartExitCode:
+			return true, nil
+		case ControlledFailureExitCode:
+			return false, errors.New("controlled failure, check logs")
+		case StartOldUIExitCode:
+			fallBackToOldUI = true
+			return true, errors.New("user requested old UI")
+		case MissingDependencyExitCode:
+			fallBackToOldUI = true
+			return true, errors.New("new UI failed with missing dependency")
+		default:
+			return true, fmt.Errorf("unknown exit code %w", exErr)
+		}
+	}
+
+	return true, fmt.Errorf("unexpected error type: %w", err)
+}
diff --git a/cmds/portmaster-start/service_windows.go b/cmds/portmaster-start/service_windows.go
new file mode 100644
index 00000000..bd47c5b2
--- /dev/null
+++ b/cmds/portmaster-start/service_windows.go
@@ -0,0 +1,134 @@
+package main
+
+// Based on the official Go examples from
+// https://github.com/golang/sys/blob/master/windows/svc/example
+// by The Go Authors.
+// Original LICENSE (sha256sum: 2d36597f7117c38b006835ae7f537487207d8ec407aa9d9980794b2030cbc067) can be found in vendor/pkg cache directory.
+
+import (
+	"fmt"
+	"log"
+	"sync"
+	"time"
+
+	"github.com/spf13/cobra"
+	"golang.org/x/sys/windows/svc"
+	"golang.org/x/sys/windows/svc/debug"
+)
+
+var (
+	runCoreService = &cobra.Command{
+		Use:   "core-service",
+		Short: "Run the Portmaster Core as a Windows Service",
+		RunE: runAndLogControlError(func(cmd *cobra.Command, args []string) error {
+			return runService(cmd, &Options{
+				Name:              "Portmaster Core Service",
+				Identifier:        "core/portmaster-core",
+				ShortIdentifier:   "core",
+				AllowDownload:     true,
+				AllowHidingWindow: false,
+				NoOutput:          true,
+				RestartOnFail:     true,
+			}, args)
+		}),
+		FParseErrWhitelist: cobra.FParseErrWhitelist{
+			// UnknownFlags will ignore unknown flags errors and continue parsing rest of the flags
+			UnknownFlags: true,
+		},
+	}
+
+	// wait groups
+	runWg    sync.WaitGroup
+	finishWg sync.WaitGroup
+)
+
+func init() {
+	rootCmd.AddCommand(runCoreService)
+}
+
+const serviceName = "PortmasterCore"
+
+type windowsService struct{}
+
+func (ws *windowsService) Execute(args []string, changeRequests <-chan svc.ChangeRequest, changes chan<- svc.Status) (ssec bool, errno uint32) {
+	const cmdsAccepted = svc.AcceptStop | svc.AcceptShutdown
+	changes <- svc.Status{State: svc.StartPending}
+
+service:
+	for {
+		select {
+		case <-startupComplete:
+			changes <- svc.Status{State: svc.Running, Accepts: cmdsAccepted}
+		case <-shuttingDown:
+			changes <- svc.Status{State: svc.StopPending}
+			break service
+		case c := <-changeRequests:
+			switch c.Cmd {
+			case svc.Interrogate:
+				changes <- c.CurrentStatus
+			case svc.Stop, svc.Shutdown:
+				initiateShutdown(nil)
+			default:
+				log.Printf("unexpected control request: #%d\n", c)
+			}
+		}
+	}
+
+	// define return values
+	if getShutdownError() != nil {
+		ssec = true // this error is specific to this service (ie. custom)
+		errno = 1   // generic error, check logs / windows events
+	}
+
+	// wait until everything else is finished
+	finishWg.Wait()
+	// send stopped status
+	changes <- svc.Status{State: svc.Stopped}
+	// wait a little for the status to reach Windows
+	time.Sleep(100 * time.Millisecond)
+
+	return ssec, errno
+}
+
+func runService(_ *cobra.Command, opts *Options, cmdArgs []string) error {
+	// check if we are running interactively
+	isDebug, err := svc.IsAnInteractiveSession()
+	if err != nil {
+		return fmt.Errorf("could not determine if running interactively: %s", err)
+	}
+	// select service run type
+	svcRun := svc.Run
+	if isDebug {
+		log.Printf("WARNING: running interactively, switching to debug execution (no real service).\n")
+		svcRun = debug.Run
+	}
+
+	runWg.Add(2)
+	finishWg.Add(1)
+
+	// run service client
+	go func() {
+		sErr := svcRun(serviceName, &windowsService{})
+		initiateShutdown(sErr)
+		runWg.Done()
+	}()
+
+	// run service
+	go func() {
+		// run slightly delayed
+		time.Sleep(250 * time.Millisecond)
+		err := run(opts, getExecArgs(opts, cmdArgs))
+		initiateShutdown(err)
+		finishWg.Done()
+		runWg.Done()
+	}()
+
+	runWg.Wait()
+
+	err = getShutdownError()
+	if err != nil {
+		log.Printf("%s service experienced an error: %s\n", serviceName, err)
+	}
+
+	return err
+}
diff --git a/cmds/portmaster-start/show.go b/cmds/portmaster-start/show.go
new file mode 100644
index 00000000..7ae6fc85
--- /dev/null
+++ b/cmds/portmaster-start/show.go
@@ -0,0 +1,45 @@
+package main
+
+import (
+	"fmt"
+	"strings"
+
+	"github.com/spf13/cobra"
+
+	"github.com/safing/portmaster/service/updates/helper"
+)
+
+func init() {
+	rootCmd.AddCommand(showCmd)
+	// sub-commands of show are registered using registerComponent
+}
+
+var showCmd = &cobra.Command{
+	Use: "show",
+	PersistentPreRunE: func(*cobra.Command, []string) error {
+		// All show sub-commands need the registry but no logging.
+		return configureRegistry(false)
+	},
+	Short: "Show the command to run a Portmaster component yourself",
+}
+
+func show(opts *Options, cmdArgs []string) error {
+	// get original arguments
+	args := getExecArgs(opts, cmdArgs)
+
+	// adapt identifier
+	if onWindows {
+		opts.Identifier += exeSuffix
+	}
+
+	file, err := registry.GetFile(
+		helper.PlatformIdentifier(opts.Identifier),
+	)
+	if err != nil {
+		return fmt.Errorf("could not get component: %w", err)
+	}
+
+	fmt.Printf("%s %s\n", file.Path(), strings.Join(args, " "))
+
+	return nil
+}
diff --git a/cmds/portmaster-start/shutdown.go b/cmds/portmaster-start/shutdown.go
new file mode 100644
index 00000000..31d34f96
--- /dev/null
+++ b/cmds/portmaster-start/shutdown.go
@@ -0,0 +1,49 @@
+package main
+
+import (
+	"sync"
+)
+
+var (
+	// startupComplete signals that the start procedure completed.
+	// The channel is not closed, just signaled once.
+	startupComplete = make(chan struct{})
+
+	// shuttingDown signals that we are shutting down.
+	// The channel will be closed, but may not be closed directly - only via initiateShutdown.
+	shuttingDown = make(chan struct{})
+
+	// shutdownError is protected by shutdownLock.
+	shutdownError error //nolint:unused,errname // Not what the linter thinks it is. Currently used on windows only.
+	shutdownLock  sync.Mutex
+)
+
+func initiateShutdown(err error) {
+	shutdownLock.Lock()
+	defer shutdownLock.Unlock()
+
+	select {
+	case <-shuttingDown:
+		return
+	default:
+		shutdownError = err
+		close(shuttingDown)
+	}
+}
+
+func isShuttingDown() bool {
+	select {
+	case <-shuttingDown:
+		return true
+	default:
+		return false
+	}
+}
+
+//nolint:deadcode,unused // false positive on linux, currently used by windows only
+func getShutdownError() error {
+	shutdownLock.Lock()
+	defer shutdownLock.Unlock()
+
+	return shutdownError
+}
diff --git a/cmds/portmaster-start/update.go b/cmds/portmaster-start/update.go
new file mode 100644
index 00000000..544047e1
--- /dev/null
+++ b/cmds/portmaster-start/update.go
@@ -0,0 +1,158 @@
+package main
+
+import (
+	"context"
+	"fmt"
+	"log"
+	"os"
+
+	"github.com/spf13/cobra"
+
+	portlog "github.com/safing/portmaster/base/log"
+	"github.com/safing/portmaster/base/updater"
+	"github.com/safing/portmaster/service/updates/helper"
+)
+
+var (
+	reset     bool
+	intelOnly bool
+)
+
+func init() {
+	rootCmd.AddCommand(updateCmd)
+	rootCmd.AddCommand(purgeCmd)
+
+	flags := updateCmd.Flags()
+	flags.BoolVar(&reset, "reset", false, "Delete all resources and re-download the basic set")
+	flags.BoolVar(&intelOnly, "intel-only", false, "Only make downloading intel updates mandatory")
+}
+
+var (
+	updateCmd = &cobra.Command{
+		Use:   "update",
+		Short: "Run a manual update process",
+		RunE: func(cmd *cobra.Command, args []string) error {
+			return downloadUpdates()
+		},
+	}
+
+	purgeCmd = &cobra.Command{
+		Use:   "purge",
+		Short: "Remove old resource versions that are superseded by at least three versions",
+		RunE: func(cmd *cobra.Command, args []string) error {
+			return purge()
+		},
+	}
+)
+
+func indexRequired(cmd *cobra.Command) bool {
+	switch cmd {
+	case updateCmd, purgeCmd:
+		return true
+	default:
+		return false
+	}
+}
+
+func downloadUpdates() error {
+	// Check if only intel data is mandatory.
+	if intelOnly {
+		helper.IntelOnly()
+	}
+
+	// Set registry state notify callback.
+	registry.StateNotifyFunc = logProgress
+
+	// Set required updates.
+	registry.MandatoryUpdates = helper.MandatoryUpdates()
+	registry.AutoUnpack = helper.AutoUnpackUpdates()
+
+	if reset {
+		// Delete storage.
+		err := os.RemoveAll(registry.StorageDir().Path)
+		if err != nil {
+			return fmt.Errorf("failed to reset update dir: %w", err)
+		}
+		err = registry.StorageDir().Ensure()
+		if err != nil {
+			return fmt.Errorf("failed to create update dir: %w", err)
+		}
+
+		// Reset registry resources.
+		registry.ResetResources()
+	}
+
+	// Update all indexes.
+	err := registry.UpdateIndexes(context.TODO())
+	if err != nil {
+		return err
+	}
+
+	// Check if updates are available.
+	if len(registry.GetState().Updates.PendingDownload) == 0 {
+		log.Println("all resources are up to date")
+		return nil
+	}
+
+	// Download all required updates.
+	err = registry.DownloadUpdates(context.TODO(), true)
+	if err != nil {
+		return err
+	}
+
+	// Select versions and unpack the selected.
+	registry.SelectVersions()
+	err = registry.UnpackResources()
+	if err != nil {
+		return fmt.Errorf("failed to unpack resources: %w", err)
+	}
+
+	if !intelOnly {
+		// Fix chrome-sandbox permissions
+		if err := helper.EnsureChromeSandboxPermissions(registry); err != nil {
+			return fmt.Errorf("failed to fix electron permissions: %w", err)
+		}
+	}
+
+	return nil
+}
+
+func logProgress(state *updater.RegistryState) {
+	switch state.ID {
+	case updater.StateChecking:
+		if state.Updates.LastCheckAt == nil {
+			log.Println("checking for new versions")
+		}
+	case updater.StateDownloading:
+		if state.Details == nil {
+			log.Printf("downloading %d updates\n", len(state.Updates.PendingDownload))
+		} else if downloadDetails, ok := state.Details.(*updater.StateDownloadingDetails); ok {
+			if downloadDetails.FinishedUpTo < len(downloadDetails.Resources) {
+				log.Printf(
+					"[%d/%d] downloading %s",
+					downloadDetails.FinishedUpTo+1,
+					len(downloadDetails.Resources),
+					downloadDetails.Resources[downloadDetails.FinishedUpTo],
+				)
+			} else if state.Updates.LastDownloadAt == nil {
+				log.Println("finalizing downloads")
+			}
+		}
+	}
+}
+
+func purge() error {
+	portlog.SetLogLevel(portlog.TraceLevel)
+
+	// logging is configured as a persistent pre-run method inherited from
+	// the root command but since we don't use run.Run() we need to start
+	// logging ourself.
+	err := portlog.Start()
+	if err != nil {
+		fmt.Printf("failed to start logging: %s\n", err)
+	}
+	defer portlog.Shutdown()
+
+	registry.Purge(3)
+	return nil
+}
diff --git a/cmds/portmaster-start/verify.go b/cmds/portmaster-start/verify.go
new file mode 100644
index 00000000..9d63c51a
--- /dev/null
+++ b/cmds/portmaster-start/verify.go
@@ -0,0 +1,179 @@
+package main
+
+import (
+	"context"
+	"errors"
+	"fmt"
+	"io/fs"
+	"log"
+	"os"
+	"strings"
+
+	"github.com/spf13/cobra"
+
+	"github.com/safing/jess"
+	"github.com/safing/jess/filesig"
+	portlog "github.com/safing/portmaster/base/log"
+	"github.com/safing/portmaster/base/updater"
+	"github.com/safing/portmaster/service/updates/helper"
+)
+
+var (
+	verifyVerbose bool
+	verifyFix     bool
+
+	verifyCmd = &cobra.Command{
+		Use:   "verify",
+		Short: "Check integrity of updates / components",
+		RunE: func(cmd *cobra.Command, args []string) error {
+			return verifyUpdates(cmd.Context())
+		},
+	}
+)
+
+func init() {
+	rootCmd.AddCommand(verifyCmd)
+
+	flags := verifyCmd.Flags()
+	flags.BoolVarP(&verifyVerbose, "verbose", "v", false, "Enable verbose output")
+	flags.BoolVar(&verifyFix, "fix", false, "Delete and re-download broken components")
+}
+
+func verifyUpdates(ctx context.Context) error {
+	// Force registry to require signatures for all enabled scopes.
+	for _, opts := range registry.Verification {
+		if opts != nil {
+			opts.DownloadPolicy = updater.SignaturePolicyRequire
+			opts.DiskLoadPolicy = updater.SignaturePolicyRequire
+		}
+	}
+
+	// Load indexes again to ensure they are correctly signed.
+	err := registry.LoadIndexes(ctx)
+	if err != nil {
+		if verifyFix {
+			log.Println("[WARN] loading indexes failed, re-downloading...")
+			err = registry.UpdateIndexes(ctx)
+			if err != nil {
+				return fmt.Errorf("failed to download indexes: %w", err)
+			}
+			log.Println("[ OK ] indexes re-downloaded and verified")
+		} else {
+			return fmt.Errorf("failed to verify indexes: %w", err)
+		}
+	} else {
+		log.Println("[ OK ] indexes verified")
+	}
+
+	// Verify all resources.
+	export := registry.Export()
+	var verified, fails, skipped int
+	for _, rv := range export {
+		for _, version := range rv.Versions {
+			// Don't verify files we don't have.
+			if !version.Available {
+				continue
+			}
+
+			// Verify file signature.
+			file := version.GetFile()
+			fileData, err := file.Verify()
+			switch {
+			case err == nil:
+				verified++
+				if verifyVerbose {
+					verifOpts := registry.GetVerificationOptions(file.Identifier())
+					if verifOpts != nil {
+						log.Printf(
+							"[ OK ] valid signature for %s: signed by %s",
+							file.Path(), getSignedByMany(fileData, verifOpts.TrustStore),
+						)
+					} else {
+						log.Printf("[ OK ] valid signature for %s", file.Path())
+					}
+				}
+
+			case errors.Is(err, updater.ErrVerificationNotConfigured):
+				skipped++
+				if verifyVerbose {
+					log.Printf("[SKIP] no verification configured for %s", file.Path())
+				}
+
+			default:
+				log.Printf("[FAIL] failed to verify %s: %s", file.Path(), err)
+				fails++
+				if verifyFix {
+					// Delete file.
+					err = os.Remove(file.Path())
+					if err != nil && !errors.Is(err, fs.ErrNotExist) {
+						log.Printf("[FAIL] failed to delete %s to prepare re-download: %s", file.Path(), err)
+					} else {
+						// We should not be changing the version, but we are in a cmd-like
+						// scenario here without goroutines.
+						version.Available = false
+					}
+					// Delete file sig.
+					err = os.Remove(file.Path() + filesig.Extension)
+					if err != nil && !errors.Is(err, fs.ErrNotExist) {
+						log.Printf("[FAIL] failed to delete %s to prepare re-download: %s", file.Path()+filesig.Extension, err)
+					} else {
+						// We should not be changing the version, but we are in a cmd-like
+						// scenario here without goroutines.
+						version.SigAvailable = false
+					}
+				}
+			}
+		}
+	}
+
+	if verified > 0 {
+		log.Printf("[STAT] verified %d files", verified)
+	}
+	if skipped > 0 && verifyVerbose {
+		log.Printf("[STAT] skipped %d files (no verification configured)", skipped)
+	}
+	if fails > 0 {
+		if verifyFix {
+			log.Printf("[WARN] verification failed on %d files, re-downloading...", fails)
+		} else {
+			return fmt.Errorf("failed to verify %d files", fails)
+		}
+	} else {
+		// Everything was verified!
+		return nil
+	}
+
+	// Start logging system for update process.
+	portlog.SetLogLevel(portlog.InfoLevel)
+	err = portlog.Start()
+	if err != nil {
+		log.Printf("[WARN] failed to start logging for monitoring update process: %s\n", err)
+	}
+	defer portlog.Shutdown()
+
+	// Re-download broken files.
+	registry.MandatoryUpdates = helper.MandatoryUpdates()
+	registry.AutoUnpack = helper.AutoUnpackUpdates()
+	err = registry.DownloadUpdates(ctx, true)
+	if err != nil {
+		return fmt.Errorf("failed to re-download files: %w", err)
+	}
+
+	return nil
+}
+
+func getSignedByMany(fds []*filesig.FileData, trustStore jess.TrustStore) string {
+	signedBy := make([]string, 0, len(fds))
+	for _, fd := range fds {
+		if sig := fd.Signature(); sig != nil {
+			for _, seal := range sig.Signatures {
+				if signet, err := trustStore.GetSignet(seal.ID, true); err == nil {
+					signedBy = append(signedBy, fmt.Sprintf("%s (%s)", signet.Info.Name, seal.ID))
+				} else {
+					signedBy = append(signedBy, seal.ID)
+				}
+			}
+		}
+	}
+	return strings.Join(signedBy, " and ")
+}
diff --git a/cmds/portmaster-start/version.go b/cmds/portmaster-start/version.go
new file mode 100644
index 00000000..6c28362a
--- /dev/null
+++ b/cmds/portmaster-start/version.go
@@ -0,0 +1,81 @@
+package main
+
+import (
+	"fmt"
+	"os"
+	"runtime"
+	"sort"
+	"strings"
+	"text/tabwriter"
+
+	"github.com/spf13/cobra"
+
+	"github.com/safing/portmaster/base/info"
+)
+
+var (
+	showShortVersion bool
+	showAllVersions  bool
+	versionCmd       = &cobra.Command{
+		Use:   "version",
+		Short: "Display various portmaster versions",
+		Args:  cobra.NoArgs,
+		PersistentPreRunE: func(*cobra.Command, []string) error {
+			if showAllVersions {
+				// If we are going to show all component versions,
+				// we need the registry to be configured.
+				if err := configureRegistry(false); err != nil {
+					return err
+				}
+			}
+
+			return nil
+		},
+		RunE: func(*cobra.Command, []string) error {
+			if !showAllVersions {
+				if showShortVersion {
+					fmt.Println(info.Version())
+					return nil
+				}
+
+				fmt.Println(info.FullVersion())
+				return nil
+			}
+
+			fmt.Printf("portmaster-start %s\n\n", info.Version())
+			fmt.Printf("Assets:\n")
+
+			all := registry.Export()
+			keys := make([]string, 0, len(all))
+			for identifier := range all {
+				keys = append(keys, identifier)
+			}
+			sort.Strings(keys)
+
+			tw := tabwriter.NewWriter(os.Stdout, 0, 0, 3, ' ', 0)
+			for _, identifier := range keys {
+				res := all[identifier]
+
+				if showShortVersion {
+					// in "short" mode, skip all resources that are irrelevant on that platform
+					if !strings.HasPrefix(identifier, "all") && !strings.HasPrefix(identifier, runtime.GOOS) {
+						continue
+					}
+				}
+
+				fmt.Fprintf(tw, "   %s\t%s\n", identifier, res.SelectedVersion.VersionNumber)
+			}
+			return tw.Flush()
+		},
+	}
+)
+
+func init() {
+	flags := versionCmd.Flags()
+	{
+		flags.BoolVar(&showShortVersion, "short", false, "Print only the version number.")
+		flags.BoolVar(&showAllVersions, "all", false, "Dump versions for all assets.")
+	}
+
+	rootCmd.AddCommand(versionCmd)
+}
diff --git a/cmds/updatemgr/confirm.go b/cmds/updatemgr/confirm.go
new file mode 100644
index 00000000..293faaf6
--- /dev/null
+++ b/cmds/updatemgr/confirm.go
@@ -0,0 +1,20 @@
+package main
+
+import (
+	"bufio"
+	"fmt"
+	"os"
+	"strings"
+)
+
+func confirm(msg string) bool {
+	fmt.Printf("%s: [y|n] ", msg)
+
+	scanner := bufio.NewScanner(os.Stdin)
+	ok := scanner.Scan()
+	if ok && strings.TrimSpace(scanner.Text()) == "y" {
+		return true
+	}
+
+	return false
+}
diff --git a/cmds/updatemgr/main.go b/cmds/updatemgr/main.go
index e7703d77..acd9a0d4 100644
--- a/cmds/updatemgr/main.go
+++ b/cmds/updatemgr/main.go
@@ -3,18 +3,56 @@ package main
 import (
 	"fmt"
 	"os"
+	"path/filepath"
 
 	"github.com/spf13/cobra"
+
+	"github.com/safing/portmaster/base/updater"
+	"github.com/safing/portmaster/base/utils"
+)
+
+var (
+	registry *updater.ResourceRegistry
+	distDir  string
 )
 
 var rootCmd = &cobra.Command{
 	Use:   "updatemgr",
-	Short: "Manage update artifacts.",
+	Short: "A simple tool to assist in the update and release process",
+	PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
+		// Check if the distribution directory exists.
+		absDistPath, err := filepath.Abs(distDir)
+		if err != nil {
+			return fmt.Errorf("failed to get absolute path of distribution directory: %w", err)
+		}
+		_, err = os.Stat(absDistPath)
+		if err != nil {
+			return fmt.Errorf("failed to access distribution directory: %w", err)
+		}
+
+		registry = &updater.ResourceRegistry{}
+		err = registry.Initialize(utils.NewDirStructure(absDistPath, 0o0755))
+		if err != nil {
+			return err
+		}
+
+		err = registry.ScanStorage("")
+		if err != nil {
+			return err
+		}
+
+		return nil
+	},
+	SilenceUsage: true,
+}
+
+func init() {
+	flags := rootCmd.PersistentFlags()
+	flags.StringVar(&distDir, "dist-dir", "dist", "Set the distribution directory. Falls back to ./dist if available.")
 }
 
 func main() {
 	if err := rootCmd.Execute(); err != nil {
-		fmt.Println(err)
 		os.Exit(1)
 	}
 }
diff --git a/cmds/updatemgr/purge.go b/cmds/updatemgr/purge.go
new file mode 100644
index 00000000..d5b456ee
--- /dev/null
+++ b/cmds/updatemgr/purge.go
@@ -0,0 +1,33 @@
+package main
+
+import (
+	"fmt"
+
+	"github.com/spf13/cobra"
+
+	"github.com/safing/portmaster/base/log"
+)
+
+func init() {
+	rootCmd.AddCommand(purgeCmd)
+}
+
+var purgeCmd = &cobra.Command{
+	Use:   "purge",
+	Short: "Remove old resource versions that are superseded by at least three versions",
+	RunE:  purge,
+}
+
+func purge(cmd *cobra.Command, args []string) error {
+	log.SetLogLevel(log.TraceLevel)
+	err := log.Start()
+	if err != nil {
+		fmt.Printf("failed to start logging: %s\n", err)
+	}
+	defer log.Shutdown()
+
+	registry.SelectVersions()
+	registry.Purge(3)
+
+	return nil
+}
diff --git a/cmds/updatemgr/release.go b/cmds/updatemgr/release.go
new file mode 100644
index 00000000..0f5d596e
--- /dev/null
+++ b/cmds/updatemgr/release.go
@@ -0,0 +1,195 @@
+package main
+
+import (
+	"context"
+	"encoding/json"
+	"fmt"
+	"os"
+	"path/filepath"
+	"time"
+
+	"github.com/spf13/cobra"
+
+	"github.com/safing/portmaster/base/updater"
+)
+
+var (
+	releaseCmd = &cobra.Command{
+		Use:   "release",
+		Short: "Release scans the distribution directory and creates registry indexes and the symlink structure",
+		Args:  cobra.ExactArgs(1),
+		RunE:  release,
+	}
+	preReleaseCmd = &cobra.Command{
+		Use:   "prerelease",
+		Short: "Stage scans the specified directory and loads the indexes - it then creates a staging index with all files newer than the stable and beta indexes",
+		Args:  cobra.ExactArgs(1),
+		RunE:  release,
+	}
+	preReleaseFrom   string
+	resetPreReleases bool
+)
+
+func init() {
+	rootCmd.AddCommand(releaseCmd)
+	rootCmd.AddCommand(preReleaseCmd)
+
+	preReleaseCmd.Flags().StringVar(&preReleaseFrom, "from", "", "Make a pre-release based on the given channel")
+	_ = preReleaseCmd.MarkFlagRequired("from")
+	preReleaseCmd.Flags().BoolVar(&resetPreReleases, "reset", false, "Reset pre-release assets")
+}
+
+func release(cmd *cobra.Command, args []string) error {
+	channel := args[0]
+
+	// Check if we want to reset instead.
+	if resetPreReleases {
+		return removeFilesFromIndex(getChannelVersions(preReleaseFrom, true))
+	}
+
+	// Write new index.
+	err := writeIndex(
+		channel,
+		getChannelVersions(preReleaseFrom, false),
+	)
+	if err != nil {
+		return err
+	}
+
+	// Only when doing a release:
+	if preReleaseFrom == "" {
+		// Create symlinks to latest stable versions.
+		if !confirm("\nDo you want to write latest symlinks?") {
+			fmt.Println("aborted...")
+			return nil
+		}
+		symlinksDir := registry.StorageDir().ChildDir("latest", 0o755)
+		err = registry.CreateSymlinks(symlinksDir)
+		if err != nil {
+			return err
+		}
+		fmt.Println("written latest symlinks")
+	}
+
+	return nil
+}
+
+func writeIndex(channel string, versions map[string]string) error {
+	// Create new index file.
+	indexFile := &updater.IndexFile{
+		Channel:   channel,
+		Published: time.Now().UTC().Round(time.Second),
+		Releases:  versions,
+	}
+
+	// Export versions and format them.
+	confirmData, err := json.MarshalIndent(indexFile, "", " ")
+	if err != nil {
+		return err
+	}
+
+	// Build index paths.
+	oldIndexPath := filepath.Join(registry.StorageDir().Path, channel+".json")
+	newIndexPath := filepath.Join(registry.StorageDir().Path, channel+".v2.json")
+
+	// Print preview.
+	fmt.Printf("%s\n%s\n%s\n\n", channel, oldIndexPath, newIndexPath)
+	fmt.Println(string(confirmData))
+
+	// Ask for confirmation.
+	if !confirm("\nDo you want to write this index?") {
+		fmt.Println("aborted...")
+		return nil
+	}
+
+	// Write indexes.
+	err = writeAsJSON(oldIndexPath, versions)
+	if err != nil {
+		return fmt.Errorf("failed to write %s: %w", oldIndexPath, err)
+	}
+	err = writeAsJSON(newIndexPath, indexFile)
+	if err != nil {
+		return fmt.Errorf("failed to write %s: %w", newIndexPath, err)
+	}
+
+	return nil
+}
+
+func writeAsJSON(path string, data any) error {
+	// Marshal to JSON.
+	jsonData, err := json.MarshalIndent(data, "", " ")
+	if err != nil {
+		return err
+	}
+
+	// Write to disk.
+	err = os.WriteFile(path, jsonData, 0o0644) //nolint:gosec
+	if err != nil {
+		return err
+	}
+
+	fmt.Printf("written %s\n", path)
+	return nil
+}
+
+func removeFilesFromIndex(versions map[string]string) error {
+	// Print preview.
+	fmt.Println("To be deleted:")
+	for _, filePath := range versions {
+		fmt.Println(filePath)
+	}
+
+	// Ask for confirmation.
+	if !confirm("\nDo you want to delete these files?") {
+		fmt.Println("aborted...")
+		return nil
+	}
+
+	// Delete files.
+	for _, filePath := range versions {
+		err := os.Remove(filePath)
+		if err != nil {
+			return err
+		}
+	}
+	fmt.Println("deleted")
+
+	return nil
+}
+
+func getChannelVersions(prereleaseFrom string, storagePath bool) map[string]string {
+	if prereleaseFrom != "" {
+		registry.AddIndex(updater.Index{
+			Path:       prereleaseFrom + ".json",
+			PreRelease: false,
+		})
+		err := registry.LoadIndexes(context.Background())
+		if err != nil {
+			panic(err)
+		}
+	}
+
+	// Sort all versions.
+	registry.SelectVersions()
+	export := registry.Export()
+
+	// Go through all versions and save the highest version, if not stable or beta.
+	versions := make(map[string]string)
+	for _, rv := range export {
+		highestVersion := rv.Versions[0]
+
+		// Ignore versions that are in the reference release channel.
+		if highestVersion.CurrentRelease {
+			continue
+		}
+
+		// Add highest version of matching release channel.
+		if storagePath {
+			versions[rv.Identifier] = rv.GetFile().Path()
+		} else {
+			versions[rv.Identifier] = highestVersion.VersionNumber
+		}
+	}
+
+	return versions
+}
diff --git a/cmds/updatemgr/scan.go b/cmds/updatemgr/scan.go
index 9ef29f15..674a581b 100644
--- a/cmds/updatemgr/scan.go
+++ b/cmds/updatemgr/scan.go
@@ -4,82 +4,46 @@ import (
 	"encoding/json"
 	"fmt"
 
-	"github.com/safing/portmaster/service/updates"
 	"github.com/spf13/cobra"
 )
 
-var (
-	scanConfig = updates.IndexScanConfig{
-		Name:            "Portmaster Binaries",
-		PrimaryArtifact: "linux_amd64/portmaster-core",
-		BaseURL:         "https://updates.safing.io/",
-		IgnoreFiles: []string{
-			// Indexes, checksums, latest symlinks.
-			"*.json",
-			"sha256*.txt",
-			"latest/**",
-
-			// Signatures.
-			"*.sig",
-			"**/*.sig",
-
-			// Related, but not required artifacts.
-			"**/*.apk",
-			"**/*install*",
-			"**/spn-hub*",
-			"**/jess*",
-			"**/hubs*.json",
-			"**/*mini*.mmdb.gz",
-
-			// Unsupported platforms.
-			"darwin_amd64/**",
-			"darwin_arm64/**",
-
-			// Deprecated artifacts.
-			"**/portmaster-start*",
-			"**/portmaster-app*",
-			"**/portmaster-notifier*",
-			"**/portmaster-wintoast*.dll",
-			"**/portmaster-snoretoast*.exe",
-			"**/portmaster-kext*.dll",
-			"**/profilemgr*.zip",
-			"**/settings*.zip",
-			"**/monitor*.zip",
-			"**/base*.zip",
-			"**/console*.zip",
-		},
-		UnpackFiles: map[string]string{
-			"gz":  "**/*.gz",
-			"zip": "**/app2/**/portmaster-app*.zip",
-		},
-	}
-
-	scanCmd = &cobra.Command{
-		Use:   "scan",
-		Short: "Scans the contents of the specified directory and creates an index from it.",
-		RunE:  scan,
-	}
-
-	scanDir string
-)
-
 func init() {
 	rootCmd.AddCommand(scanCmd)
-	scanCmd.Flags().StringVarP(&scanDir, "dir", "d", "", "directory to create index from (required)")
-	_ = scanCmd.MarkFlagRequired("dir")
+}
+
+var scanCmd = &cobra.Command{
+	Use:   "scan",
+	Short: "Scan the specified directory and print the result",
+	RunE:  scan,
 }
 
 func scan(cmd *cobra.Command, args []string) error {
-	index, err := updates.GenerateIndexFromDir(scanDir, scanConfig)
+	// Reset and rescan.
+	registry.ResetResources()
+	err := registry.ScanStorage("")
 	if err != nil {
 		return err
 	}
 
-	indexJson, err := json.MarshalIndent(&index, "", "  ")
+	// Export latest versions.
+	data, err := json.MarshalIndent(exportSelected(true), "", " ")
 	if err != nil {
-		return fmt.Errorf("marshal index: %w", err)
+		return err
 	}
+	// Print them.
+	fmt.Println(string(data))
 
-	fmt.Printf("%s", indexJson)
 	return nil
 }
+
+func exportSelected(preReleases bool) map[string]string {
+	registry.SetUsePreReleases(preReleases)
+	registry.SelectVersions()
+	export := registry.Export()
+
+	versions := make(map[string]string)
+	for _, rv := range export {
+		versions[rv.Identifier] = rv.SelectedVersion.VersionNumber
+	}
+	return versions
+}
diff --git a/cmds/updatemgr/sign.go b/cmds/updatemgr/sign.go
index 5f6d8b31..02cfb15d 100644
--- a/cmds/updatemgr/sign.go
+++ b/cmds/updatemgr/sign.go
@@ -3,7 +3,9 @@ package main
 import (
 	"errors"
 	"fmt"
+	"io/fs"
 	"os"
+	"path/filepath"
 	"strings"
 
 	"github.com/spf13/cobra"
@@ -11,35 +13,43 @@ import (
 	"github.com/safing/jess"
 	"github.com/safing/jess/filesig"
 	"github.com/safing/jess/truststores"
-	"github.com/safing/portmaster/service/updates"
 )
 
 func init() {
 	rootCmd.AddCommand(signCmd)
 
 	// Required argument: envelope
-	signCmd.Flags().StringVarP(&envelopeName, "envelope", "", "",
+	signCmd.PersistentFlags().StringVarP(&envelopeName, "envelope", "", "",
 		"specify envelope name used for signing",
 	)
 	_ = signCmd.MarkFlagRequired("envelope")
 
 	// Optional arguments: verbose, tsdir, tskeyring
-	signCmd.Flags().BoolVarP(&signVerbose, "verbose", "v", false,
+	signCmd.PersistentFlags().BoolVarP(&signVerbose, "verbose", "v", false,
 		"enable verbose output",
 	)
-	signCmd.Flags().StringVarP(&trustStoreDir, "tsdir", "", "",
+	signCmd.PersistentFlags().StringVarP(&trustStoreDir, "tsdir", "", "",
 		"specify a truststore directory (default loaded from JESS_TS_DIR env variable)",
 	)
-	signCmd.Flags().StringVarP(&trustStoreKeyring, "tskeyring", "", "",
+	signCmd.PersistentFlags().StringVarP(&trustStoreKeyring, "tskeyring", "", "",
 		"specify a truststore keyring namespace (default loaded from JESS_TS_KEYRING env variable) - lower priority than tsdir",
 	)
+
+	// Subcommand for signing indexes.
+	signCmd.AddCommand(signIndexCmd)
 }
 
 var (
 	signCmd = &cobra.Command{
-		Use:   "sign [index.json file]",
-		Short: "Sign an index",
+		Use:   "sign",
+		Short: "Sign resources",
 		RunE:  sign,
+		Args:  cobra.NoArgs,
+	}
+	signIndexCmd = &cobra.Command{
+		Use:   "index",
+		Short: "Sign indexes",
+		RunE:  signIndex,
 		Args:  cobra.ExactArgs(1),
 	}
 
@@ -48,8 +58,6 @@ var (
 )
 
 func sign(cmd *cobra.Command, args []string) error {
-	indexFilename := args[0]
-
 	// Setup trust store.
 	trustStore, err := setupTrustStore()
 	if err != nil {
@@ -62,44 +70,159 @@ func sign(cmd *cobra.Command, args []string) error {
 		return err
 	}
 
-	// Read index file from disk.
-	unsignedIndexData, err := os.ReadFile(indexFilename)
-	if err != nil {
-		return fmt.Errorf("read index file: %w", err)
+	// Get all resources and iterate over all versions.
+	export := registry.Export()
+	var verified, signed, fails int
+	for _, rv := range export {
+		for _, version := range rv.Versions {
+			file := version.GetFile()
+
+			// Check if there is an existing signature.
+			_, err := os.Stat(file.Path() + filesig.Extension)
+			switch {
+			case err == nil || errors.Is(err, fs.ErrExist):
+				// If the file exists, just verify.
+				fileData, err := filesig.VerifyFile(
+					file.Path(),
+					file.Path()+filesig.Extension,
+					file.SigningMetadata(),
+					trustStore,
+				)
+				if err != nil {
+					fmt.Printf("[FAIL] signature error for %s: %s\n", file.Path(), err)
+					fails++
+				} else {
+					if signVerbose {
+						fmt.Printf("[ OK ] valid signature for %s: signed by %s\n", file.Path(), getSignedByMany(fileData, trustStore))
+					}
+					verified++
+				}
+
+			case errors.Is(err, fs.ErrNotExist):
+				// Attempt to sign file.
+				fileData, err := filesig.SignFile(
+					file.Path(),
+					file.Path()+filesig.Extension,
+					file.SigningMetadata(),
+					signingEnvelope,
+					trustStore,
+				)
+				if err != nil {
+					fmt.Printf("[FAIL] failed to sign %s: %s\n", file.Path(), err)
+					fails++
+				} else {
+					fmt.Printf("[SIGN] signed %s with %s\n", file.Path(), getSignedBySingle(fileData, trustStore))
+					signed++
+				}
+
+			default:
+				// File access error.
+				fmt.Printf("[FAIL] failed to access %s: %s\n", file.Path(), err)
+				fails++
+			}
+		}
 	}
 
-	// Parse index and check if it is valid.
-	index, err := updates.ParseIndex(unsignedIndexData, nil)
-	if err != nil {
-		return fmt.Errorf("invalid index: %w", err)
+	if verified > 0 {
+		fmt.Printf("[STAT] verified %d files\n", verified)
 	}
-	err = index.CanDoUpgrades()
+	if signed > 0 {
+		fmt.Printf("[STAT] signed %d files\n", signed)
+	}
+	if fails > 0 {
+		return fmt.Errorf("signing or verification failed on %d files", fails)
+	}
+	return nil
+}
+
+func signIndex(cmd *cobra.Command, args []string) error {
+	// Setup trust store.
+	trustStore, err := setupTrustStore()
 	if err != nil {
-		return fmt.Errorf("invalid index: %w", err)
+		return err
 	}
 
-	// Sign index.
-	signedIndexData, err := filesig.AddJSONSignature(unsignedIndexData, signingEnvelope, trustStore)
+	// Get envelope.
+	signingEnvelope, err := trustStore.GetEnvelope(envelopeName)
 	if err != nil {
-		return fmt.Errorf("sign: %w", err)
+		return err
 	}
 
-	// Check by parsing again.
-	index, err = updates.ParseIndex(signedIndexData, nil)
-	if err != nil {
-		return fmt.Errorf("invalid index after signing: %w", err)
-	}
-	err = index.CanDoUpgrades()
-	if err != nil {
-		return fmt.Errorf("invalid index after signing: %w", err)
+	// Resolve globs.
+	files := make([]string, 0, len(args))
+	for _, arg := range args {
+		matches, err := filepath.Glob(arg)
+		if err != nil {
+			return err
+		}
+		files = append(files, matches...)
 	}
 
-	// Write back to file.
-	err = os.WriteFile(indexFilename, signedIndexData, 0o0644)
-	if err != nil {
-		return fmt.Errorf("write signed index file: %w", err)
+	// Go through all files.
+	var verified, signed, fails int
+	for _, file := range files {
+		sigFile := file + filesig.Extension
+
+		// Ignore matches for the signatures.
+		if strings.HasSuffix(file, filesig.Extension) {
+			continue
+		}
+
+		// Check if there is an existing signature.
+		_, err := os.Stat(sigFile)
+		switch {
+		case err == nil || errors.Is(err, fs.ErrExist):
+			// If the file exists, just verify.
+			fileData, err := filesig.VerifyFile(
+				file,
+				sigFile,
+				nil,
+				trustStore,
+			)
+			if err == nil {
+				if signVerbose {
+					fmt.Printf("[ OK ] valid signature for %s: signed by %s\n", file, getSignedByMany(fileData, trustStore))
+				}
+				verified++
+
+				// Indexes are expected to change, so just sign the index again if verification fails.
+				continue
+			}
+
+			fallthrough
+		case errors.Is(err, fs.ErrNotExist):
+			// Attempt to sign file.
+			fileData, err := filesig.SignFile(
+				file,
+				sigFile,
+				nil,
+				signingEnvelope,
+				trustStore,
+			)
+			if err != nil {
+				fmt.Printf("[FAIL] failed to sign %s: %s\n", file, err)
+				fails++
+			} else {
+				fmt.Printf("[SIGN] signed %s with %s\n", file, getSignedBySingle(fileData, trustStore))
+				signed++
+			}
+
+		default:
+			// File access error.
+			fmt.Printf("[FAIL] failed to access %s: %s\n", sigFile, err)
+			fails++
+		}
 	}
 
+	if verified > 0 {
+		fmt.Printf("[STAT] verified %d files", verified)
+	}
+	if signed > 0 {
+		fmt.Printf("[STAT] signed %d files", signed)
+	}
+	if fails > 0 {
+		return fmt.Errorf("signing failed on %d files", fails)
+	}
 	return nil
 }
 
diff --git a/desktop/tauri/src-tauri/Cargo.lock b/desktop/tauri/src-tauri/Cargo.lock
index 72551e9a..8d938985 100644
--- a/desktop/tauri/src-tauri/Cargo.lock
+++ b/desktop/tauri/src-tauri/Cargo.lock
@@ -17,12 +17,6 @@ version = "1.0.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
 
-[[package]]
-name = "adler2"
-version = "2.0.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627"
-
 [[package]]
 name = "aead"
 version = "0.5.2"
@@ -216,167 +210,55 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da"
 
 [[package]]
-name = "app-store-connect"
-version = "0.5.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "33fb5489b9bfcfa3aec2f68cc79eafb999b5af9b9d9d70ca8dfe36acdd1b2b05"
+name = "app"
+version = "0.1.0"
 dependencies = [
- "anyhow",
- "base64 0.21.7",
- "clap 4.5.17",
- "dirs 5.0.1",
- "env_logger 0.10.2",
- "jsonwebtoken",
+ "assert_matches",
+ "cached",
+ "clap 4.5.16",
+ "ctor",
+ "dark-light",
+ "dataurl",
+ "dirs 1.0.5",
+ "futures-util",
+ "gdk-pixbuf",
+ "gdk-pixbuf-sys",
+ "gio-sys",
+ "glib",
+ "glib-sys",
+ "gtk",
+ "gtk-sys",
+ "http",
+ "lazy_static",
  "log",
- "pem",
- "rand 0.8.5",
- "reqwest 0.11.27",
- "rsa",
+ "notify-rust",
+ "open",
+ "reqwest",
+ "rfd",
+ "rust-ini",
  "serde",
  "serde_json",
- "thiserror",
- "x509-certificate",
-]
-
-[[package]]
-name = "apple-bundles"
-version = "0.19.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "abb7c27ee2ca7826adfdc84228cd4c5a84ab57b0a11d269d1d7cd0615238e5a2"
-dependencies = [
- "anyhow",
- "plist",
- "simple-file-manifest",
- "walkdir",
-]
-
-[[package]]
-name = "apple-codesign"
-version = "0.27.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "329820aac7259ca0529d3cc21dd3b4c11651225dfce9e0ce25b121b23f923164"
-dependencies = [
- "anyhow",
- "app-store-connect",
- "apple-bundles",
- "apple-flat-package",
- "apple-xar",
- "aws-config",
- "aws-sdk-s3",
- "aws-smithy-http",
- "aws-smithy-types",
- "base64 0.21.7",
- "bcder",
- "bitflags 2.6.0",
- "bytes",
- "chrono",
- "clap 4.5.17",
- "cryptographic-message-syntax",
- "der 0.7.9",
- "dialoguer",
- "difference",
- "digest",
- "dirs 5.0.1",
- "elliptic-curve 0.13.8",
- "env_logger 0.10.2",
- "figment",
- "filetime",
- "glob",
- "goblin",
- "hex",
- "log",
- "md-5",
- "minicbor",
- "num-traits",
- "object 0.32.2",
- "oid-registry",
- "once_cell",
- "p12",
- "p256 0.13.2",
- "pem",
- "pkcs1",
- "pkcs8 0.10.2",
- "plist",
- "rand 0.8.5",
- "rasn",
- "rayon",
- "regex",
- "reqwest 0.11.27",
- "ring",
- "rsa",
- "scroll",
- "security-framework",
- "security-framework-sys",
- "semver",
- "serde",
- "serde_json",
- "serde_yaml",
- "sha2",
- "signature 2.2.0",
- "simple-file-manifest",
- "spake2",
- "spki 0.7.3",
- "subtle",
- "tempfile",
+ "sha",
+ "tauri",
+ "tauri-build",
+ "tauri-cli",
+ "tauri-plugin-clipboard-manager",
+ "tauri-plugin-dialog",
+ "tauri-plugin-log",
+ "tauri-plugin-notification",
+ "tauri-plugin-os",
+ "tauri-plugin-shell",
+ "tauri-plugin-single-instance",
+ "tauri-plugin-window-state",
+ "tauri-winrt-notification 0.3.1",
  "thiserror",
  "tokio",
- "tungstenite",
- "uuid",
- "walkdir",
- "widestring",
- "windows-sys 0.52.0",
- "x509",
- "x509-certificate",
- "xml-rs",
- "yasna",
- "zeroize",
- "zip 0.6.6",
- "zip_structs",
-]
-
-[[package]]
-name = "apple-flat-package"
-version = "0.18.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b6adc520e05304de5ec383487786fa20e9c636fe972e59719cdd93621a2db6f1"
-dependencies = [
- "apple-xar",
- "cpio-archive",
- "flate2",
- "scroll",
- "serde",
- "serde-xml-rs",
- "thiserror",
-]
-
-[[package]]
-name = "apple-xar"
-version = "0.18.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "844e00dc1e665b3cf0bba745aa9c6464292ca512db0c11384511586701eb0335"
-dependencies = [
- "base64 0.21.7",
- "bcder",
- "bzip2",
- "chrono",
- "cryptographic-message-syntax",
- "digest",
- "flate2",
- "log",
- "md-5",
- "rand 0.8.5",
- "reqwest 0.11.27",
- "scroll",
- "serde",
- "serde-xml-rs",
- "sha1",
- "sha2",
- "signature 2.2.0",
- "thiserror",
+ "tokio-websockets",
  "url",
- "x509-certificate",
- "xml-rs",
- "xz2",
+ "uuid",
+ "which",
+ "windows 0.54.0",
+ "windows-service",
 ]
 
 [[package]]
@@ -402,7 +284,7 @@ checksum = "9fb4009533e8ff8f1450a5bcbc30f4242a1d34442221f72314bea1f5dc9c7f89"
 dependencies = [
  "clipboard-win",
  "core-graphics 0.23.2",
- "image",
+ "image 0.25.2",
  "log",
  "objc2",
  "objc2-app-kit",
@@ -420,7 +302,7 @@ checksum = "0ae92a5119aa49cdbcf6b9f893fe4e1d98b04ccbf82ee0584ad948a44a734dea"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.77",
+ "syn 2.0.75",
 ]
 
 [[package]]
@@ -488,65 +370,6 @@ dependencies = [
  "zbus 4.4.0",
 ]
 
-[[package]]
-name = "ashpd"
-version = "0.9.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bfe7e0dd0ac5a401dc116ed9f9119cf9decc625600474cb41f0fc0a0050abc9a"
-dependencies = [
- "enumflags2",
- "futures-channel",
- "futures-util",
- "rand 0.8.5",
- "raw-window-handle",
- "serde",
- "serde_repr",
- "tokio",
- "url",
- "wayland-backend",
- "wayland-client",
- "wayland-protocols",
- "zbus 4.4.0",
-]
-
-[[package]]
-name = "asn1-rs"
-version = "0.5.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7f6fd5ddaf0351dff5b8da21b2fb4ff8e08ddd02857f0bf69c47639106c0fff0"
-dependencies = [
- "asn1-rs-derive",
- "asn1-rs-impl",
- "displaydoc",
- "nom",
- "num-traits",
- "rusticata-macros",
- "thiserror",
-]
-
-[[package]]
-name = "asn1-rs-derive"
-version = "0.4.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "726535892e8eae7e70657b4c8ea93d26b8553afb1ce617caee529ef96d7dee6c"
-dependencies = [
- "proc-macro2",
- "quote",
- "syn 1.0.109",
- "synstructure",
-]
-
-[[package]]
-name = "asn1-rs-impl"
-version = "0.1.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2777730b2039ac0f95f093556e61b6d26cebed5393ca6f152717777cec3a42ed"
-dependencies = [
- "proc-macro2",
- "quote",
- "syn 1.0.109",
-]
-
 [[package]]
 name = "assert-unchecked"
 version = "0.1.2"
@@ -601,7 +424,7 @@ checksum = "d7ebdfa2ebdab6b1760375fa7d6f382b9f486eac35fc994625a00e89280bdbb7"
 dependencies = [
  "async-task",
  "concurrent-queue",
- "fastrand 2.1.1",
+ "fastrand 2.1.0",
  "futures-lite 2.3.0",
  "slab",
 ]
@@ -662,7 +485,7 @@ dependencies = [
  "futures-lite 2.3.0",
  "parking",
  "polling 3.7.3",
- "rustix 0.38.35",
+ "rustix 0.38.34",
  "slab",
  "tracing",
  "windows-sys 0.59.0",
@@ -712,7 +535,7 @@ dependencies = [
  "cfg-if",
  "event-listener 3.1.0",
  "futures-lite 1.13.0",
- "rustix 0.38.35",
+ "rustix 0.38.34",
  "windows-sys 0.48.0",
 ]
 
@@ -731,7 +554,7 @@ dependencies = [
  "cfg-if",
  "event-listener 5.3.1",
  "futures-lite 2.3.0",
- "rustix 0.38.35",
+ "rustix 0.38.34",
  "tracing",
  "windows-sys 0.59.0",
 ]
@@ -744,7 +567,7 @@ checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.77",
+ "syn 2.0.75",
 ]
 
 [[package]]
@@ -759,7 +582,7 @@ dependencies = [
  "cfg-if",
  "futures-core",
  "futures-io",
- "rustix 0.38.35",
+ "rustix 0.38.34",
  "signal-hook-registry",
  "slab",
  "windows-sys 0.59.0",
@@ -773,13 +596,13 @@ checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de"
 
 [[package]]
 name = "async-trait"
-version = "0.1.82"
+version = "0.1.81"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a27b8a3a6e1a44fa4c8baf1f653e4172e81486d4941f2237e20dc2d0cf4ddff1"
+checksum = "6e0c28dcc82d7c8ead5cb13beb15405b57b8546e93215673ff8ca0349a028107"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.77",
+ "syn 2.0.75",
 ]
 
 [[package]]
@@ -805,15 +628,6 @@ dependencies = [
  "system-deps",
 ]
 
-[[package]]
-name = "atomic"
-version = "0.6.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8d818003e740b63afc82337e3160717f4f63078720a810b7b903e70a5d1d2994"
-dependencies = [
- "bytemuck",
-]
-
 [[package]]
 name = "atomic-waker"
 version = "1.1.2"
@@ -860,381 +674,6 @@ dependencies = [
  "arrayvec 0.7.6",
 ]
 
-[[package]]
-name = "aws-config"
-version = "1.5.8"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7198e6f03240fdceba36656d8be440297b6b82270325908c7381f37d826a74f6"
-dependencies = [
- "aws-credential-types",
- "aws-runtime",
- "aws-sdk-sso",
- "aws-sdk-ssooidc",
- "aws-sdk-sts",
- "aws-smithy-async",
- "aws-smithy-http",
- "aws-smithy-json",
- "aws-smithy-runtime",
- "aws-smithy-runtime-api",
- "aws-smithy-types",
- "aws-types",
- "bytes",
- "fastrand 2.1.1",
- "hex",
- "http 0.2.12",
- "ring",
- "time",
- "tokio",
- "tracing",
- "url",
- "zeroize",
-]
-
-[[package]]
-name = "aws-credential-types"
-version = "1.2.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "60e8f6b615cb5fc60a98132268508ad104310f0cfb25a1c22eee76efdf9154da"
-dependencies = [
- "aws-smithy-async",
- "aws-smithy-runtime-api",
- "aws-smithy-types",
- "zeroize",
-]
-
-[[package]]
-name = "aws-runtime"
-version = "1.4.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a10d5c055aa540164d9561a0e2e74ad30f0dcf7393c3a92f6733ddf9c5762468"
-dependencies = [
- "aws-credential-types",
- "aws-sigv4",
- "aws-smithy-async",
- "aws-smithy-eventstream",
- "aws-smithy-http",
- "aws-smithy-runtime",
- "aws-smithy-runtime-api",
- "aws-smithy-types",
- "aws-types",
- "bytes",
- "fastrand 2.1.1",
- "http 0.2.12",
- "http-body 0.4.6",
- "once_cell",
- "percent-encoding",
- "pin-project-lite",
- "tracing",
- "uuid",
-]
-
-[[package]]
-name = "aws-sdk-s3"
-version = "1.54.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e2f2a62020f3e06f9b352b2a23547f6e1d110b6bf1e18a6b588ae36114eaf6e2"
-dependencies = [
- "ahash 0.8.11",
- "aws-credential-types",
- "aws-runtime",
- "aws-sigv4",
- "aws-smithy-async",
- "aws-smithy-checksums",
- "aws-smithy-eventstream",
- "aws-smithy-http",
- "aws-smithy-json",
- "aws-smithy-runtime",
- "aws-smithy-runtime-api",
- "aws-smithy-types",
- "aws-smithy-xml",
- "aws-types",
- "bytes",
- "fastrand 2.1.1",
- "hex",
- "hmac",
- "http 0.2.12",
- "http-body 0.4.6",
- "lru",
- "once_cell",
- "percent-encoding",
- "regex-lite",
- "sha2",
- "tracing",
- "url",
-]
-
-[[package]]
-name = "aws-sdk-sso"
-version = "1.45.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e33ae899566f3d395cbf42858e433930682cc9c1889fa89318896082fef45efb"
-dependencies = [
- "aws-credential-types",
- "aws-runtime",
- "aws-smithy-async",
- "aws-smithy-http",
- "aws-smithy-json",
- "aws-smithy-runtime",
- "aws-smithy-runtime-api",
- "aws-smithy-types",
- "aws-types",
- "bytes",
- "http 0.2.12",
- "once_cell",
- "regex-lite",
- "tracing",
-]
-
-[[package]]
-name = "aws-sdk-ssooidc"
-version = "1.46.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f39c09e199ebd96b9f860b0fce4b6625f211e064ad7c8693b72ecf7ef03881e0"
-dependencies = [
- "aws-credential-types",
- "aws-runtime",
- "aws-smithy-async",
- "aws-smithy-http",
- "aws-smithy-json",
- "aws-smithy-runtime",
- "aws-smithy-runtime-api",
- "aws-smithy-types",
- "aws-types",
- "bytes",
- "http 0.2.12",
- "once_cell",
- "regex-lite",
- "tracing",
-]
-
-[[package]]
-name = "aws-sdk-sts"
-version = "1.45.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3d95f93a98130389eb6233b9d615249e543f6c24a68ca1f109af9ca5164a8765"
-dependencies = [
- "aws-credential-types",
- "aws-runtime",
- "aws-smithy-async",
- "aws-smithy-http",
- "aws-smithy-json",
- "aws-smithy-query",
- "aws-smithy-runtime",
- "aws-smithy-runtime-api",
- "aws-smithy-types",
- "aws-smithy-xml",
- "aws-types",
- "http 0.2.12",
- "once_cell",
- "regex-lite",
- "tracing",
-]
-
-[[package]]
-name = "aws-sigv4"
-version = "1.2.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cc8db6904450bafe7473c6ca9123f88cc11089e41a025408f992db4e22d3be68"
-dependencies = [
- "aws-credential-types",
- "aws-smithy-eventstream",
- "aws-smithy-http",
- "aws-smithy-runtime-api",
- "aws-smithy-types",
- "bytes",
- "crypto-bigint 0.5.5",
- "form_urlencoded",
- "hex",
- "hmac",
- "http 0.2.12",
- "http 1.1.0",
- "once_cell",
- "p256 0.11.1",
- "percent-encoding",
- "ring",
- "sha2",
- "subtle",
- "time",
- "tracing",
- "zeroize",
-]
-
-[[package]]
-name = "aws-smithy-async"
-version = "1.2.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "62220bc6e97f946ddd51b5f1361f78996e704677afc518a4ff66b7a72ea1378c"
-dependencies = [
- "futures-util",
- "pin-project-lite",
- "tokio",
-]
-
-[[package]]
-name = "aws-smithy-checksums"
-version = "0.60.12"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "598b1689d001c4d4dc3cb386adb07d37786783aee3ac4b324bcadac116bf3d23"
-dependencies = [
- "aws-smithy-http",
- "aws-smithy-types",
- "bytes",
- "crc32c",
- "crc32fast",
- "hex",
- "http 0.2.12",
- "http-body 0.4.6",
- "md-5",
- "pin-project-lite",
- "sha1",
- "sha2",
- "tracing",
-]
-
-[[package]]
-name = "aws-smithy-eventstream"
-version = "0.60.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cef7d0a272725f87e51ba2bf89f8c21e4df61b9e49ae1ac367a6d69916ef7c90"
-dependencies = [
- "aws-smithy-types",
- "bytes",
- "crc32fast",
-]
-
-[[package]]
-name = "aws-smithy-http"
-version = "0.60.11"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5c8bc3e8fdc6b8d07d976e301c02fe553f72a39b7a9fea820e023268467d7ab6"
-dependencies = [
- "aws-smithy-eventstream",
- "aws-smithy-runtime-api",
- "aws-smithy-types",
- "bytes",
- "bytes-utils",
- "futures-core",
- "http 0.2.12",
- "http-body 0.4.6",
- "once_cell",
- "percent-encoding",
- "pin-project-lite",
- "pin-utils",
- "tracing",
-]
-
-[[package]]
-name = "aws-smithy-json"
-version = "0.60.7"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4683df9469ef09468dad3473d129960119a0d3593617542b7d52086c8486f2d6"
-dependencies = [
- "aws-smithy-types",
-]
-
-[[package]]
-name = "aws-smithy-query"
-version = "0.60.7"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f2fbd61ceb3fe8a1cb7352e42689cec5335833cd9f94103a61e98f9bb61c64bb"
-dependencies = [
- "aws-smithy-types",
- "urlencoding",
-]
-
-[[package]]
-name = "aws-smithy-runtime"
-version = "1.7.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d1ce695746394772e7000b39fe073095db6d45a862d0767dd5ad0ac0d7f8eb87"
-dependencies = [
- "aws-smithy-async",
- "aws-smithy-http",
- "aws-smithy-runtime-api",
- "aws-smithy-types",
- "bytes",
- "fastrand 2.1.1",
- "h2 0.3.26",
- "http 0.2.12",
- "http-body 0.4.6",
- "http-body 1.0.1",
- "httparse",
- "hyper 0.14.30",
- "hyper-rustls 0.24.2",
- "once_cell",
- "pin-project-lite",
- "pin-utils",
- "rustls 0.21.12",
- "tokio",
- "tracing",
-]
-
-[[package]]
-name = "aws-smithy-runtime-api"
-version = "1.7.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e086682a53d3aa241192aa110fa8dfce98f2f5ac2ead0de84d41582c7e8fdb96"
-dependencies = [
- "aws-smithy-async",
- "aws-smithy-types",
- "bytes",
- "http 0.2.12",
- "http 1.1.0",
- "pin-project-lite",
- "tokio",
- "tracing",
- "zeroize",
-]
-
-[[package]]
-name = "aws-smithy-types"
-version = "1.2.7"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "147100a7bea70fa20ef224a6bad700358305f5dc0f84649c53769761395b355b"
-dependencies = [
- "base64-simd",
- "bytes",
- "bytes-utils",
- "futures-core",
- "http 0.2.12",
- "http 1.1.0",
- "http-body 0.4.6",
- "http-body 1.0.1",
- "http-body-util",
- "itoa 1.0.11",
- "num-integer",
- "pin-project-lite",
- "pin-utils",
- "ryu",
- "serde",
- "time",
- "tokio",
- "tokio-util",
-]
-
-[[package]]
-name = "aws-smithy-xml"
-version = "0.60.9"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ab0b0166827aa700d3dc519f72f8b3a91c35d0b8d042dc5d643a91e6f80648fc"
-dependencies = [
- "xmlparser",
-]
-
-[[package]]
-name = "aws-types"
-version = "1.3.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5221b91b3e441e6675310829fd8984801b772cb1546ef6c0e54dec9f1ac13fef"
-dependencies = [
- "aws-credential-types",
- "aws-smithy-async",
- "aws-smithy-runtime-api",
- "aws-smithy-types",
- "rustc_version",
- "tracing",
-]
-
 [[package]]
 name = "axum"
 version = "0.7.5"
@@ -1246,10 +685,10 @@ dependencies = [
  "base64 0.21.7",
  "bytes",
  "futures-util",
- "http 1.1.0",
- "http-body 1.0.1",
+ "http",
+ "http-body",
  "http-body-util",
- "hyper 1.4.1",
+ "hyper",
  "hyper-util",
  "itoa 1.0.11",
  "matchit",
@@ -1281,8 +720,8 @@ dependencies = [
  "async-trait",
  "bytes",
  "futures-util",
- "http 1.1.0",
- "http-body 1.0.1",
+ "http",
+ "http-body",
  "http-body-util",
  "mime",
  "pin-project-lite",
@@ -1303,17 +742,11 @@ dependencies = [
  "cc",
  "cfg-if",
  "libc",
- "miniz_oxide 0.7.4",
- "object 0.36.4",
+ "miniz_oxide",
+ "object",
  "rustc-demangle",
 ]
 
-[[package]]
-name = "base16ct"
-version = "0.1.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "349a06037c7bf932dd7e7d1f653678b2038b9ad46a74102f1fc7bd7872678cce"
-
 [[package]]
 name = "base16ct"
 version = "0.2.0"
@@ -1338,16 +771,6 @@ version = "0.22.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
 
-[[package]]
-name = "base64-simd"
-version = "0.8.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "339abbe78e73178762e23bea9dfd08e697eb3f3301cd4be981c0f78ba5859195"
-dependencies = [
- "outref",
- "vsimd",
-]
-
 [[package]]
 name = "base64ct"
 version = "1.6.0"
@@ -1420,9 +843,9 @@ dependencies = [
 
 [[package]]
 name = "bitstream-io"
-version = "2.5.3"
+version = "2.5.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b81e1519b0d82120d2fd469d5bfb2919a9361c48b02d82d04befc1cdd2002452"
+checksum = "3dcde5f311c85b8ca30c2e4198d4326bc342c76541590106f5fa4a50946ea499"
 
 [[package]]
 name = "bitvec"
@@ -1436,16 +859,6 @@ dependencies = [
  "wyz",
 ]
 
-[[package]]
-name = "bitvec-nom2"
-version = "0.2.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d988fcc40055ceaa85edc55875a08f8abd29018582647fd82ad6128dba14a5f0"
-dependencies = [
- "bitvec",
- "nom",
-]
-
 [[package]]
 name = "blake2"
 version = "0.10.6"
@@ -1539,10 +952,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "c3ef8005764f53cd4dca619f5bf64cafd4664dada50ece25e4d81de54c80cc0b"
 dependencies = [
  "once_cell",
- "proc-macro-crate 3.2.0",
+ "proc-macro-crate 3.1.0",
  "proc-macro2",
  "quote",
- "syn 2.0.77",
+ "syn 2.0.75",
  "syn_derive",
 ]
 
@@ -1648,9 +1061,9 @@ checksum = "5ce89b21cab1437276d2650d57e971f9d548a2d9037cc231abdc0562b97498ce"
 
 [[package]]
 name = "bytemuck"
-version = "1.17.1"
+version = "1.17.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "773d90827bc3feecfb67fab12e24de0749aad83c74b9504ecde46237b5cd24e2"
+checksum = "6fd4c6dcc3b0aea2f5c0b4b82c2b15fe39ddbc76041a310848f4706edf76bb31"
 
 [[package]]
 name = "byteorder"
@@ -1673,22 +1086,6 @@ dependencies = [
  "serde",
 ]
 
-[[package]]
-name = "bytes-utils"
-version = "0.1.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7dafe3a8757b027e2be6e4e5601ed563c55989fcf1546e933c66c8eb3a058d35"
-dependencies = [
- "bytes",
- "either",
-]
-
-[[package]]
-name = "bytesize"
-version = "1.3.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a3e368af43e418a04d52505cf3dbc23dda4e3407ae2fa99fd0e4f308ce546acc"
-
 [[package]]
 name = "bzip2"
 version = "0.4.4"
@@ -1789,9 +1186,9 @@ dependencies = [
 
 [[package]]
 name = "cargo-mobile2"
-version = "0.17.3"
+version = "0.15.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5c8052fc43184dc6c572437c2f8dae83e4ca9a5b27c790e269b90b080c1b9301"
+checksum = "d0b8132519bea2d46174e777bd36d480d93afbe1df31c27cacfb411ff152bba1"
 dependencies = [
  "colored",
  "core-foundation 0.10.0",
@@ -1874,20 +1271,11 @@ dependencies = [
  "rustversion",
 ]
 
-[[package]]
-name = "cbc"
-version = "0.1.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "26b52a9543ae338f279b96b0b9fed9c8093744685043739079ce85cd58f289a6"
-dependencies = [
- "cipher",
-]
-
 [[package]]
 name = "cc"
-version = "1.1.16"
+version = "1.1.13"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e9d013ecb737093c0e86b151a7b837993cf9ec6c502946cfb44bedc392421e0b"
+checksum = "72db2f7947ecee9b03b510377e8bb9077afa27176fdbff55c51027e976fdcc48"
 dependencies = [
  "jobserver",
  "libc",
@@ -1984,9 +1372,9 @@ dependencies = [
 
 [[package]]
 name = "clap"
-version = "4.5.17"
+version = "4.5.16"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3e5a21b8495e732f1b3c364c9949b201ca7bae518c502c80256c96ad79eaf6ac"
+checksum = "ed6719fffa43d0d87e5fd8caeab59be1554fb028cd30edc88fc4369b17971019"
 dependencies = [
  "clap_builder",
  "clap_derive",
@@ -1994,9 +1382,9 @@ dependencies = [
 
 [[package]]
 name = "clap_builder"
-version = "4.5.17"
+version = "4.5.15"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8cf2dd12af7a047ad9d6da2b6b249759a22a7abc0f474c1dae1777afa4b21a73"
+checksum = "216aec2b177652e3846684cbfe25c9964d18ec45234f0f5da5157b207ed1aab6"
 dependencies = [
  "anstream",
  "anstyle",
@@ -2006,11 +1394,11 @@ dependencies = [
 
 [[package]]
 name = "clap_complete"
-version = "4.5.25"
+version = "4.5.18"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "18d7f143a7e709cbe6c34853dcd3bb1370c7e1bb4d9e7310ca8cb40b490ae035"
+checksum = "1ee158892bd7ce77aa15c208abbdb73e155d191c287a659b57abd5adb92feb03"
 dependencies = [
- "clap 4.5.17",
+ "clap 4.5.16",
 ]
 
 [[package]]
@@ -2022,7 +1410,7 @@ dependencies = [
  "heck 0.5.0",
  "proc-macro2",
  "quote",
- "syn 2.0.77",
+ "syn 2.0.75",
 ]
 
 [[package]]
@@ -2051,6 +1439,22 @@ dependencies = [
  "digest",
 ]
 
+[[package]]
+name = "cocoa"
+version = "0.25.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f6140449f97a6e97f9511815c5632d84c8aacf8ac271ad77c559218161a1373c"
+dependencies = [
+ "bitflags 1.3.2",
+ "block",
+ "cocoa-foundation 0.1.2",
+ "core-foundation 0.9.4",
+ "core-graphics 0.23.2",
+ "foreign-types 0.5.0",
+ "libc",
+ "objc",
+]
+
 [[package]]
 name = "cocoa"
 version = "0.26.0"
@@ -2059,7 +1463,7 @@ checksum = "f79398230a6e2c08f5c9760610eb6924b52aa9e7950a619602baba59dcbbdbb2"
 dependencies = [
  "bitflags 2.6.0",
  "block",
- "cocoa-foundation",
+ "cocoa-foundation 0.2.0",
  "core-foundation 0.10.0",
  "core-graphics 0.24.0",
  "foreign-types 0.5.0",
@@ -2067,6 +1471,20 @@ dependencies = [
  "objc",
 ]
 
+[[package]]
+name = "cocoa-foundation"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8c6234cbb2e4c785b456c0644748b1ac416dd045799740356f8363dfe00c93f7"
+dependencies = [
+ "bitflags 1.3.2",
+ "block",
+ "core-foundation 0.9.4",
+ "core-graphics-types 0.1.3",
+ "libc",
+ "objc",
+]
+
 [[package]]
 name = "cocoa-foundation"
 version = "0.2.0"
@@ -2181,12 +1599,6 @@ dependencies = [
  "tiny-keccak",
 ]
 
-[[package]]
-name = "const_panic"
-version = "0.2.10"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "013b6c2c3a14d678f38cd23994b02da3a1a1b6a5d1eedddfe63a5a5f11b13a81"
-
 [[package]]
 name = "constant_time_eq"
 version = "0.1.5"
@@ -2199,15 +1611,6 @@ version = "0.4.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e"
 
-[[package]]
-name = "cookie-factory"
-version = "0.3.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9885fa71e26b8ab7855e2ec7cae6e9b380edff76cd052e07c683a0319d51b3a2"
-dependencies = [
- "futures",
-]
-
 [[package]]
 name = "core-foundation"
 version = "0.9.4"
@@ -2297,18 +1700,6 @@ version = "0.4.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "80e3adec7390c7643049466136117057188edf5f23efc5c8b4fc8079c8dc34a6"
 
-[[package]]
-name = "cpio-archive"
-version = "0.9.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "63d5133d716d3d82da8c76367ddb0ab1733e2629f1462e4f39947e13b8b4b741"
-dependencies = [
- "chrono",
- "is_executable",
- "simple-file-manifest",
- "thiserror",
-]
-
 [[package]]
 name = "cpufeatures"
 version = "0.2.13"
@@ -2324,15 +1715,6 @@ version = "0.1.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "fd121741cf3eb82c08dd3023eb55bf2665e5f60ec20f89760cf836ae4562e6a0"
 
-[[package]]
-name = "crc32c"
-version = "0.6.8"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3a47af21622d091a8f0fb295b88bc886ac74efcc613efc19f5d0b21de5c89e47"
-dependencies = [
- "rustc_version",
-]
-
 [[package]]
 name = "crc32fast"
 version = "1.4.2"
@@ -2382,18 +1764,6 @@ version = "0.2.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7"
 
-[[package]]
-name = "crypto-bigint"
-version = "0.4.9"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ef2b4b23cddf68b89b8f8069890e8c270d54e2d5fe1b143820234805e4cb17ef"
-dependencies = [
- "generic-array",
- "rand_core 0.6.4",
- "subtle",
- "zeroize",
-]
-
 [[package]]
 name = "crypto-bigint"
 version = "0.5.5"
@@ -2417,23 +1787,6 @@ dependencies = [
  "typenum",
 ]
 
-[[package]]
-name = "cryptographic-message-syntax"
-version = "0.26.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "43c324ba1028cef7e3a71a00cbf585637bb0215dec2f6a2b566d094190a1309b"
-dependencies = [
- "bcder",
- "bytes",
- "chrono",
- "hex",
- "pem",
- "reqwest 0.11.27",
- "ring",
- "signature 2.2.0",
- "x509-certificate",
-]
-
 [[package]]
 name = "css-color"
 version = "0.2.8"
@@ -2464,7 +1817,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "13b588ba4ac1a99f7f2964d24b3d896ddc6bf847ee3855dbd4366f058cfcd331"
 dependencies = [
  "quote",
- "syn 2.0.77",
+ "syn 2.0.75",
 ]
 
 [[package]]
@@ -2474,7 +1827,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "edb49164822f3ee45b17acd4a208cfc1251410cf0cad9a833234c9890774dd9f"
 dependencies = [
  "quote",
- "syn 2.0.77",
+ "syn 2.0.75",
 ]
 
 [[package]]
@@ -2507,7 +1860,6 @@ dependencies = [
  "curve25519-dalek-derive",
  "digest",
  "fiat-crypto",
- "rand_core 0.6.4",
  "rustc_version",
  "subtle",
  "zeroize",
@@ -2521,7 +1873,7 @@ checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.77",
+ "syn 2.0.75",
 ]
 
 [[package]]
@@ -2537,7 +1889,7 @@ dependencies = [
  "objc",
  "rust-ini",
  "web-sys",
- "winreg 0.52.0",
+ "winreg",
  "xdg",
  "zbus 3.15.2",
 ]
@@ -2587,7 +1939,7 @@ dependencies = [
  "proc-macro2",
  "quote",
  "strsim 0.11.1",
- "syn 2.0.77",
+ "syn 2.0.75",
 ]
 
 [[package]]
@@ -2609,7 +1961,7 @@ checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806"
 dependencies = [
  "darling_core 0.20.10",
  "quote",
- "syn 2.0.77",
+ "syn 2.0.75",
 ]
 
 [[package]]
@@ -2667,16 +2019,6 @@ version = "0.3.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "7046468a81e6a002061c01e6a7c83139daf91b11c30e66795b13217c2d885c8b"
 
-[[package]]
-name = "der"
-version = "0.6.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f1a467a65c5e759bce6e65eaf91cc29f466cdc57cb65777bd646872a8a1fd4de"
-dependencies = [
- "const-oid",
- "zeroize",
-]
-
 [[package]]
 name = "der"
 version = "0.7.9"
@@ -2717,38 +2059,38 @@ checksum = "67e77553c4162a157adbf834ebae5b415acbecbeafc7a74b0e886657506a7611"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.77",
+ "syn 2.0.75",
 ]
 
 [[package]]
 name = "derive_builder"
-version = "0.20.1"
+version = "0.20.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cd33f37ee6a119146a1781d3356a7c26028f83d779b2e04ecd45fdc75c76877b"
+checksum = "0350b5cb0331628a5916d6c5c0b72e97393b8b6b03b47a9284f4e7f5a405ffd7"
 dependencies = [
  "derive_builder_macro",
 ]
 
 [[package]]
 name = "derive_builder_core"
-version = "0.20.1"
+version = "0.20.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7431fa049613920234f22c47fdc33e6cf3ee83067091ea4277a3f8c4587aae38"
+checksum = "d48cda787f839151732d396ac69e3473923d54312c070ee21e9effcaa8ca0b1d"
 dependencies = [
  "darling 0.20.10",
  "proc-macro2",
  "quote",
- "syn 2.0.77",
+ "syn 2.0.75",
 ]
 
 [[package]]
 name = "derive_builder_macro"
-version = "0.20.1"
+version = "0.20.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4abae7035bf79b9877b779505d8cf3749285b80c43941eda66604841889451dc"
+checksum = "206868b8242f27cecce124c19fd88157fbd0dd334df2587f36417bafbc85097b"
 dependencies = [
  "derive_builder_core",
- "syn 2.0.77",
+ "syn 2.0.75",
 ]
 
 [[package]]
@@ -2761,7 +2103,7 @@ dependencies = [
  "proc-macro2",
  "quote",
  "rustc_version",
- "syn 2.0.77",
+ "syn 2.0.75",
 ]
 
 [[package]]
@@ -2798,12 +2140,6 @@ dependencies = [
  "zeroize",
 ]
 
-[[package]]
-name = "difference"
-version = "2.0.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "524cbf6897b527295dff137cec09ecf3a05f4fddffd7dfcd1585403449e74198"
-
 [[package]]
 name = "digest"
 version = "0.10.7"
@@ -2854,7 +2190,7 @@ checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c"
 dependencies = [
  "libc",
  "option-ext",
- "redox_users 0.4.6",
+ "redox_users 0.4.5",
  "windows-sys 0.48.0",
 ]
 
@@ -2865,7 +2201,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d"
 dependencies = [
  "libc",
- "redox_users 0.4.6",
+ "redox_users 0.4.5",
  "winapi",
 ]
 
@@ -2883,16 +2219,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.77",
-]
-
-[[package]]
-name = "dlib"
-version = "0.5.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "330c60081dcc4c72131f8eb70510f1ac07223e5d4163db481a04a0befcffa412"
-dependencies = [
- "libloading",
+ "syn 2.0.75",
 ]
 
 [[package]]
@@ -2915,7 +2242,7 @@ checksum = "f2b99bf03862d7f545ebc28ddd33a665b50865f4dfd84031a393823879bd4c54"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.77",
+ "syn 2.0.75",
 ]
 
 [[package]]
@@ -2927,18 +2254,6 @@ dependencies = [
  "const-random",
 ]
 
-[[package]]
-name = "doc-comment"
-version = "0.3.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10"
-
-[[package]]
-name = "downcast-rs"
-version = "1.2.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2"
-
 [[package]]
 name = "dpi"
 version = "0.1.1"
@@ -2957,10 +2272,10 @@ dependencies = [
  "digest",
  "num-bigint-dig",
  "num-traits",
- "pkcs8 0.10.2",
- "rfc6979 0.4.0",
+ "pkcs8",
+ "rfc6979",
  "sha2",
- "signature 2.2.0",
+ "signature",
  "zeroize",
 ]
 
@@ -3016,30 +2331,18 @@ dependencies = [
  "subtle",
 ]
 
-[[package]]
-name = "ecdsa"
-version = "0.14.8"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "413301934810f597c1d19ca71c8710e99a3f1ba28a0d2ebc01551a2daeea3c5c"
-dependencies = [
- "der 0.6.1",
- "elliptic-curve 0.12.3",
- "rfc6979 0.3.1",
- "signature 1.6.4",
-]
-
 [[package]]
 name = "ecdsa"
 version = "0.16.9"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca"
 dependencies = [
- "der 0.7.9",
+ "der",
  "digest",
- "elliptic-curve 0.13.8",
- "rfc6979 0.4.0",
- "signature 2.2.0",
- "spki 0.7.3",
+ "elliptic-curve",
+ "rfc6979",
+ "signature",
+ "spki",
 ]
 
 [[package]]
@@ -3048,8 +2351,8 @@ version = "2.2.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53"
 dependencies = [
- "pkcs8 0.10.2",
- "signature 2.2.0",
+ "pkcs8",
+ "signature",
 ]
 
 [[package]]
@@ -3078,43 +2381,23 @@ version = "0.7.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "4445909572dbd556c457c849c4ca58623d84b27c8fff1e74b0b4227d8b90d17b"
 
-[[package]]
-name = "elliptic-curve"
-version = "0.12.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e7bb888ab5300a19b8e5bceef25ac745ad065f3c9f7efc6de1b91958110891d3"
-dependencies = [
- "base16ct 0.1.1",
- "crypto-bigint 0.4.9",
- "der 0.6.1",
- "digest",
- "ff 0.12.1",
- "generic-array",
- "group 0.12.1",
- "pkcs8 0.9.0",
- "rand_core 0.6.4",
- "sec1 0.3.0",
- "subtle",
- "zeroize",
-]
-
 [[package]]
 name = "elliptic-curve"
 version = "0.13.8"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47"
 dependencies = [
- "base16ct 0.2.0",
- "crypto-bigint 0.5.5",
+ "base16ct",
+ "crypto-bigint",
  "digest",
- "ff 0.13.0",
+ "ff",
  "generic-array",
- "group 0.13.0",
+ "group",
  "hkdf",
  "pem-rfc7468",
- "pkcs8 0.10.2",
+ "pkcs8",
  "rand_core 0.6.4",
- "sec1 0.7.3",
+ "sec1",
  "subtle",
  "zeroize",
 ]
@@ -3130,7 +2413,7 @@ dependencies = [
  "rustc_version",
  "toml 0.8.19",
  "vswhom",
- "winreg 0.52.0",
+ "winreg",
 ]
 
 [[package]]
@@ -3185,7 +2468,7 @@ checksum = "ba7795da175654fe16979af73f81f26a8ea27638d8d9823d317016888a63dc4c"
 dependencies = [
  "num-traits",
  "quote",
- "syn 2.0.77",
+ "syn 2.0.75",
 ]
 
 [[package]]
@@ -3206,7 +2489,7 @@ checksum = "de0d48a183585823424a4ce1aa132d174a6a81bd540895822eb4c8373a8e49e8"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.77",
+ "syn 2.0.75",
 ]
 
 [[package]]
@@ -3219,19 +2502,6 @@ dependencies = [
  "regex",
 ]
 
-[[package]]
-name = "env_logger"
-version = "0.10.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4cd405aab171cb85d6735e5c8d9db038c17d3ca007a4d2c25f337935c3d90580"
-dependencies = [
- "humantime",
- "is-terminal",
- "log",
- "regex",
- "termcolor",
-]
-
 [[package]]
 name = "env_logger"
 version = "0.11.5"
@@ -3325,7 +2595,7 @@ dependencies = [
  "flume",
  "half",
  "lebe",
- "miniz_oxide 0.7.4",
+ "miniz_oxide",
  "rayon-core",
  "smallvec",
  "zune-inflate",
@@ -3338,8 +2608,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "531e46835a22af56d1e3b66f04844bed63158bc094a628bec1d321d9b4c44bf2"
 dependencies = [
  "bit-set",
- "regex-automata",
- "regex-syntax",
+ "regex-automata 0.4.7",
+ "regex-syntax 0.8.4",
 ]
 
 [[package]]
@@ -3353,9 +2623,9 @@ dependencies = [
 
 [[package]]
 name = "fastrand"
-version = "2.1.1"
+version = "2.1.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e8c02a5121d4ea3eb16a80748c74f5549a5665e4c21333c6098f283870fbdea6"
+checksum = "9fc0510504f03c51ada170672ac806f1f105a88aa97a5281117e1ddc3368e51a"
 
 [[package]]
 name = "fdeflate"
@@ -3375,16 +2645,6 @@ dependencies = [
  "log",
 ]
 
-[[package]]
-name = "ff"
-version = "0.12.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d013fc25338cc558c5c2cfbad646908fb23591e2404481826742b651c9af7160"
-dependencies = [
- "rand_core 0.6.4",
- "subtle",
-]
-
 [[package]]
 name = "ff"
 version = "0.13.0"
@@ -3411,25 +2671,11 @@ dependencies = [
  "rustc_version",
 ]
 
-[[package]]
-name = "figment"
-version = "0.10.19"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8cb01cd46b0cf372153850f4c6c272d9cbea2da513e07538405148f95bd789f3"
-dependencies = [
- "atomic",
- "pear",
- "serde",
- "toml 0.8.19",
- "uncased",
- "version_check",
-]
-
 [[package]]
 name = "filetime"
-version = "0.2.25"
+version = "0.2.24"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "35c0522e981e68cbfa8c3f978441a5f34b30b96e146b33cd3359176b50fe8586"
+checksum = "bf401df4a4e3872c4fe8151134cf483738e74b67fc934d6532c882b3d24a4550"
 dependencies = [
  "cfg-if",
  "libc",
@@ -3439,12 +2685,12 @@ dependencies = [
 
 [[package]]
 name = "flate2"
-version = "1.0.33"
+version = "1.0.31"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "324a1be68054ef05ad64b861cc9eaf1d623d2d8cb25b4bf2cb9cdd902b4bf253"
+checksum = "7f211bbe8e69bbd0cfdea405084f128ae8b4aaa6b0b522fc8f2b009084797920"
 dependencies = [
  "crc32fast",
- "miniz_oxide 0.8.0",
+ "miniz_oxide",
 ]
 
 [[package]]
@@ -3477,12 +2723,6 @@ version = "1.0.7"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
 
-[[package]]
-name = "foldhash"
-version = "0.1.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f81ec6369c545a7d40e4589b5597581fa1c441fe1cce96dd1de43159910a36a2"
-
 [[package]]
 name = "fontconfig-parser"
 version = "0.5.7"
@@ -3533,7 +2773,7 @@ checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.77",
+ "syn 2.0.75",
 ]
 
 [[package]]
@@ -3671,7 +2911,7 @@ version = "2.3.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "52527eb5074e35e9339c6b4e8d12600c7128b68fb25dcb9fa9dec18f7c25f3a5"
 dependencies = [
- "fastrand 2.1.1",
+ "fastrand 2.1.0",
  "futures-core",
  "futures-io",
  "parking",
@@ -3686,7 +2926,7 @@ checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.77",
+ "syn 2.0.75",
 ]
 
 [[package]]
@@ -3833,6 +3073,19 @@ dependencies = [
  "x11",
 ]
 
+[[package]]
+name = "generator"
+version = "0.7.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5cc16584ff22b460a382b7feec54b23d2908d858152e5739a120b949293bd74e"
+dependencies = [
+ "cc",
+ "libc",
+ "log",
+ "rustversion",
+ "windows 0.48.0",
+]
+
 [[package]]
 name = "generic-array"
 version = "0.14.7"
@@ -3854,16 +3107,6 @@ dependencies = [
  "windows-targets 0.48.5",
 ]
 
-[[package]]
-name = "gethostname"
-version = "0.5.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "dc3655aa6818d65bc620d6911f05aa7b6aeb596291e1e9f79e52df85583d1e30"
-dependencies = [
- "rustix 0.38.35",
- "windows-targets 0.52.6",
-]
-
 [[package]]
 name = "getrandom"
 version = "0.1.16"
@@ -3980,7 +3223,7 @@ dependencies = [
  "proc-macro-error",
  "proc-macro2",
  "quote",
- "syn 2.0.77",
+ "syn 2.0.75",
 ]
 
 [[package]]
@@ -4008,8 +3251,8 @@ dependencies = [
  "aho-corasick",
  "bstr",
  "log",
- "regex-automata",
- "regex-syntax",
+ "regex-automata 0.4.7",
+ "regex-syntax 0.8.4",
 ]
 
 [[package]]
@@ -4023,35 +3266,13 @@ dependencies = [
  "system-deps",
 ]
 
-[[package]]
-name = "goblin"
-version = "0.8.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1b363a30c165f666402fe6a3024d3bec7ebc898f96a4a23bd1c99f8dbf3f4f47"
-dependencies = [
- "log",
- "plain",
- "scroll",
-]
-
-[[package]]
-name = "group"
-version = "0.12.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5dfbfb3a6cfbd390d5c9564ab283a0349b9b9fcd46a706c1eb10e0db70bfbac7"
-dependencies = [
- "ff 0.12.1",
- "rand_core 0.6.4",
- "subtle",
-]
-
 [[package]]
 name = "group"
 version = "0.13.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63"
 dependencies = [
- "ff 0.13.0",
+ "ff",
  "rand_core 0.6.4",
  "subtle",
 ]
@@ -4105,41 +3326,22 @@ dependencies = [
  "proc-macro-error",
  "proc-macro2",
  "quote",
- "syn 2.0.77",
+ "syn 2.0.75",
 ]
 
 [[package]]
 name = "h2"
-version = "0.3.26"
+version = "0.4.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "81fe527a889e1532da5c525686d96d4c2e74cdd345badf8dfef9f6b39dd5f5e8"
-dependencies = [
- "bytes",
- "fnv",
- "futures-core",
- "futures-sink",
- "futures-util",
- "http 0.2.12",
- "indexmap 2.5.0",
- "slab",
- "tokio",
- "tokio-util",
- "tracing",
-]
-
-[[package]]
-name = "h2"
-version = "0.4.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "524e8ac6999421f49a846c2d4411f337e53497d8ec55d67753beffa43c5d9205"
+checksum = "fa82e28a107a8cc405f0839610bdc9b15f1e25ec7d696aa5cf173edbcb1486ab"
 dependencies = [
  "atomic-waker",
  "bytes",
  "fnv",
  "futures-core",
  "futures-sink",
- "http 1.1.0",
- "indexmap 2.5.0",
+ "http",
+ "indexmap 2.4.0",
  "slab",
  "tokio",
  "tokio-util",
@@ -4189,17 +3391,6 @@ dependencies = [
  "allocator-api2",
 ]
 
-[[package]]
-name = "hashbrown"
-version = "0.15.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1e087f84d4f86bf4b218b927129862374b72199ae7d8657835f1e89000eea4fb"
-dependencies = [
- "allocator-api2",
- "equivalent",
- "foldhash",
-]
-
 [[package]]
 name = "heck"
 version = "0.4.1"
@@ -4280,17 +3471,6 @@ dependencies = [
  "syn 1.0.109",
 ]
 
-[[package]]
-name = "http"
-version = "0.2.12"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1"
-dependencies = [
- "bytes",
- "fnv",
- "itoa 1.0.11",
-]
-
 [[package]]
 name = "http"
 version = "1.1.0"
@@ -4302,17 +3482,6 @@ dependencies = [
  "itoa 1.0.11",
 ]
 
-[[package]]
-name = "http-body"
-version = "0.4.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2"
-dependencies = [
- "bytes",
- "http 0.2.12",
- "pin-project-lite",
-]
-
 [[package]]
 name = "http-body"
 version = "1.0.1"
@@ -4320,7 +3489,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184"
 dependencies = [
  "bytes",
- "http 1.1.0",
+ "http",
 ]
 
 [[package]]
@@ -4331,8 +3500,8 @@ checksum = "793429d76616a256bcb62c2a2ec2bed781c8307e797e2598c50010f2bee2544f"
 dependencies = [
  "bytes",
  "futures-util",
- "http 1.1.0",
- "http-body 1.0.1",
+ "http",
+ "http-body",
  "pin-project-lite",
 ]
 
@@ -4354,30 +3523,6 @@ version = "2.1.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4"
 
-[[package]]
-name = "hyper"
-version = "0.14.30"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a152ddd61dfaec7273fe8419ab357f33aee0d914c5f4efbf0d96fa749eea5ec9"
-dependencies = [
- "bytes",
- "futures-channel",
- "futures-core",
- "futures-util",
- "h2 0.3.26",
- "http 0.2.12",
- "http-body 0.4.6",
- "httparse",
- "httpdate",
- "itoa 1.0.11",
- "pin-project-lite",
- "socket2 0.5.7",
- "tokio",
- "tower-service",
- "tracing",
- "want",
-]
-
 [[package]]
 name = "hyper"
 version = "1.4.1"
@@ -4387,9 +3532,9 @@ dependencies = [
  "bytes",
  "futures-channel",
  "futures-util",
- "h2 0.4.6",
- "http 1.1.0",
- "http-body 1.0.1",
+ "h2",
+ "http",
+ "http-body",
  "httparse",
  "httpdate",
  "itoa 1.0.11",
@@ -4401,29 +3546,13 @@ dependencies = [
 
 [[package]]
 name = "hyper-rustls"
-version = "0.24.2"
+version = "0.27.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ec3efd23720e2049821a693cbc7e65ea87c72f1c58ff2f9522ff332b1491e590"
+checksum = "5ee4be2c948921a1a5320b629c4193916ed787a7f7f293fd3f7f5a6c9de74155"
 dependencies = [
  "futures-util",
- "http 0.2.12",
- "hyper 0.14.30",
- "log",
- "rustls 0.21.12",
- "rustls-native-certs 0.6.3",
- "tokio",
- "tokio-rustls 0.24.1",
-]
-
-[[package]]
-name = "hyper-rustls"
-version = "0.27.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "08afdbb5c31130e3034af566421053ab03787c640246a446327f550d11bcb333"
-dependencies = [
- "futures-util",
- "http 1.1.0",
- "hyper 1.4.1",
+ "http",
+ "hyper",
  "hyper-util",
  "rustls 0.23.12",
  "rustls-pki-types",
@@ -4440,7 +3569,7 @@ checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0"
 dependencies = [
  "bytes",
  "http-body-util",
- "hyper 1.4.1",
+ "hyper",
  "hyper-util",
  "native-tls",
  "tokio",
@@ -4457,9 +3586,9 @@ dependencies = [
  "bytes",
  "futures-channel",
  "futures-util",
- "http 1.1.0",
- "http-body 1.0.1",
- "hyper 1.4.1",
+ "http",
+ "http-body",
+ "hyper",
  "pin-project-lite",
  "socket2 0.5.7",
  "tokio",
@@ -4536,12 +3665,30 @@ dependencies = [
  "globset",
  "log",
  "memchr",
- "regex-automata",
+ "regex-automata 0.4.7",
  "same-file",
  "walkdir",
  "winapi-util",
 ]
 
+[[package]]
+name = "image"
+version = "0.24.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5690139d2f55868e080017335e4b94cb7414274c74f1669c84fb5feba2c9f69d"
+dependencies = [
+ "bytemuck",
+ "byteorder",
+ "color_quant",
+ "exr",
+ "gif",
+ "jpeg-decoder",
+ "num-traits",
+ "png",
+ "qoi",
+ "tiff",
+]
+
 [[package]]
 name = "image"
 version = "0.25.2"
@@ -4619,9 +3766,9 @@ dependencies = [
 
 [[package]]
 name = "indexmap"
-version = "2.5.0"
+version = "2.4.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "68b900aa2f7301e21c36462b170ee99994de34dff39a4a6a528e80e7376d07e5"
+checksum = "93ead53efc7ea8ed3cfb0c79fc8023fbb782a5432b52830b6518941cebe6505c"
 dependencies = [
  "equivalent",
  "hashbrown 0.14.5",
@@ -4639,19 +3786,13 @@ dependencies = [
 
 [[package]]
 name = "infer"
-version = "0.16.0"
+version = "0.15.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bc150e5ce2330295b8616ce0e3f53250e53af31759a9dbedad1621ba29151847"
+checksum = "cb33622da908807a06f9513c19b3c1ad50fab3e4137d82a78107d502075aa199"
 dependencies = [
  "cfb",
 ]
 
-[[package]]
-name = "inlinable_string"
-version = "0.1.15"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c8fae54786f62fb2918dcfae3d568594e50eb9b5c25bf04371af6fe7516452fb"
-
 [[package]]
 name = "inotify"
 version = "0.9.6"
@@ -4678,7 +3819,6 @@ version = "0.1.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "a0c10553d664a4d0bcff9f4215d0aac67a639cc68ef660840afe309b807bc9f5"
 dependencies = [
- "block-padding",
  "generic-array",
 ]
 
@@ -4699,7 +3839,7 @@ checksum = "c34819042dc3d3971c46c2190835914dfbe0c3c13f61449b2997f4e9722dfa60"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.77",
+ "syn 2.0.75",
 ]
 
 [[package]]
@@ -4728,17 +3868,6 @@ dependencies = [
  "once_cell",
 ]
 
-[[package]]
-name = "is-terminal"
-version = "0.4.13"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "261f68e344040fbd0edea105bef17c66edf46f984ddb1115b775ce31be948f4b"
-dependencies = [
- "hermit-abi 0.4.0",
- "libc",
- "windows-sys 0.52.0",
-]
-
 [[package]]
 name = "is-wsl"
 version = "0.4.0"
@@ -4749,15 +3878,6 @@ dependencies = [
  "once_cell",
 ]
 
-[[package]]
-name = "is_executable"
-version = "1.0.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d4a1b5bad6f9072935961dfbf1cced2f3d129963d091b6f69f007fe04e758ae2"
-dependencies = [
- "winapi",
-]
-
 [[package]]
 name = "is_terminal_polyfill"
 version = "1.70.1"
@@ -4775,18 +3895,9 @@ dependencies = [
 
 [[package]]
 name = "iter-read"
-version = "1.1.0"
+version = "1.0.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "071ed4cc1afd86650602c7b11aa2e1ce30762a1c27193201cb5cee9c6ebb1294"
-
-[[package]]
-name = "itertools"
-version = "0.10.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473"
-dependencies = [
- "either",
-]
+checksum = "a598c1abae8e3456ebda517868b254b6bc2a9bb6501ffd5b9d0875bf332e048b"
 
 [[package]]
 name = "itertools"
@@ -4888,6 +3999,9 @@ name = "jpeg-decoder"
 version = "0.3.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "f5d4a7da358eff58addd2877a45865158f0d78c911d43a5784ceb7bbf52833b0"
+dependencies = [
+ "rayon",
+]
 
 [[package]]
 name = "js-sys"
@@ -4945,9 +4059,9 @@ dependencies = [
 
 [[package]]
 name = "jsonrpsee"
-version = "0.24.6"
+version = "0.24.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "02f01f48e04e0d7da72280ab787c9943695699c9b32b99158ece105e8ad0afea"
+checksum = "5ec465b607a36dc5dd45d48b7689bc83f679f66a3ac6b6b21cc787a11e0f8685"
 dependencies = [
  "jsonrpsee-core",
  "jsonrpsee-server",
@@ -4957,13 +4071,13 @@ dependencies = [
 
 [[package]]
 name = "jsonrpsee-client-transport"
-version = "0.24.6"
+version = "0.24.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d80eccbd47a7b9f1e67663fd846928e941cb49c65236e297dd11c9ea3c5e3387"
+checksum = "90f0977f9c15694371b8024c35ab58ca043dbbf4b51ccb03db8858a021241df1"
 dependencies = [
  "base64 0.22.1",
  "futures-util",
- "http 1.1.0",
+ "http",
  "jsonrpsee-core",
  "pin-project",
  "soketto",
@@ -4976,16 +4090,16 @@ dependencies = [
 
 [[package]]
 name = "jsonrpsee-core"
-version = "0.24.6"
+version = "0.24.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3c2709a32915d816a6e8f625bf72cf74523ebe5d8829f895d6b041b1d3137818"
+checksum = "e942c55635fbf5dc421938b8558a8141c7e773720640f4f1dbe1f4164ca4e221"
 dependencies = [
  "async-trait",
  "bytes",
  "futures-timer",
  "futures-util",
- "http 1.1.0",
- "http-body 1.0.1",
+ "http",
+ "http-body",
  "http-body-util",
  "jsonrpsee-types",
  "parking_lot",
@@ -5002,15 +4116,15 @@ dependencies = [
 
 [[package]]
 name = "jsonrpsee-server"
-version = "0.24.6"
+version = "0.24.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e30110d0f2d7866c8cc6c86483bdab2eb9f4d2f0e20db55518b2bca84651ba8e"
+checksum = "038fb697a709bec7134e9ccbdbecfea0e2d15183f7140254afef7c5610a3f488"
 dependencies = [
  "futures-util",
- "http 1.1.0",
- "http-body 1.0.1",
+ "http",
+ "http-body",
  "http-body-util",
- "hyper 1.4.1",
+ "hyper",
  "hyper-util",
  "jsonrpsee-core",
  "jsonrpsee-types",
@@ -5029,11 +4143,11 @@ dependencies = [
 
 [[package]]
 name = "jsonrpsee-types"
-version = "0.24.6"
+version = "0.24.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1ca331cd7b3fe95b33432825c2d4c9f5a43963e207fdc01ae67f9fd80ab0930f"
+checksum = "23b67d6e008164f027afbc2e7bb79662650158d26df200040282d2aa1cbb093b"
 dependencies = [
- "http 1.1.0",
+ "http",
  "serde",
  "serde_json",
  "thiserror",
@@ -5041,11 +4155,11 @@ dependencies = [
 
 [[package]]
 name = "jsonrpsee-ws-client"
-version = "0.24.6"
+version = "0.24.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "755ca3da1c67671f1fae01cd1a47f41dfb2233a8f19a643e587ab0a663942044"
+checksum = "992bf67d1132f88edf4a4f8cff474cf01abb2be203004a2b8e11c2b20795b99e"
 dependencies = [
- "http 1.1.0",
+ "http",
  "jsonrpsee-client-transport",
  "jsonrpsee-core",
  "jsonrpsee-types",
@@ -5054,15 +4168,15 @@ dependencies = [
 
 [[package]]
 name = "jsonschema"
-version = "0.18.1"
+version = "0.18.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f5f037c58cadb17e8591b620b523cc6a7ab2b91b6ce3121f8eb4171f8d80115c"
+checksum = "ec0afd06142c9bcb03f4a8787c77897a87b6be9c4918f1946c33caa714c27578"
 dependencies = [
  "ahash 0.8.11",
  "anyhow",
  "base64 0.22.1",
  "bytecount",
- "clap 4.5.17",
+ "clap 4.5.16",
  "fancy-regex",
  "fraction",
  "getrandom 0.2.15",
@@ -5074,7 +4188,7 @@ dependencies = [
  "parking_lot",
  "percent-encoding",
  "regex",
- "reqwest 0.12.7",
+ "reqwest",
  "serde",
  "serde_json",
  "time",
@@ -5082,27 +4196,6 @@ dependencies = [
  "uuid",
 ]
 
-[[package]]
-name = "jsonwebtoken"
-version = "9.3.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b9ae10193d25051e74945f1ea2d0b42e03cc3b890f7e4cc5faa44997d808193f"
-dependencies = [
- "base64 0.21.7",
- "js-sys",
- "pem",
- "ring",
- "serde",
- "serde_json",
- "simple_asn1",
-]
-
-[[package]]
-name = "jzon"
-version = "0.12.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "17ab85f84ca42c5ec520e6f3c9966ba1fd62909ce260f8837e248857d2560509"
-
 [[package]]
 name = "k256"
 version = "0.13.3"
@@ -5110,11 +4203,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "956ff9b67e26e1a6a866cb758f12c6f8746208489e3e4a4b5580802f2f0a587b"
 dependencies = [
  "cfg-if",
- "ecdsa 0.16.9",
- "elliptic-curve 0.13.8",
+ "ecdsa",
+ "elliptic-curve",
  "once_cell",
  "sha2",
- "signature 2.2.0",
+ "signature",
 ]
 
 [[package]]
@@ -5137,26 +4230,6 @@ dependencies = [
  "unicode-segmentation",
 ]
 
-[[package]]
-name = "konst"
-version = "0.3.9"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "50a0ba6de5f7af397afff922f22c149ff605c766cd3269cf6c1cd5e466dbe3b9"
-dependencies = [
- "const_panic",
- "konst_kernel",
- "typewit",
-]
-
-[[package]]
-name = "konst_kernel"
-version = "0.3.9"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "be0a455a1719220fd6adf756088e1c69a85bf14b6a9e24537a5cc04f503edb2b"
-dependencies = [
- "typewit",
-]
-
 [[package]]
 name = "kqueue"
 version = "1.0.8"
@@ -5297,9 +4370,9 @@ checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89"
 
 [[package]]
 name = "local-ip-address"
-version = "0.6.2"
+version = "0.6.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b435d7dd476416a905f9634dff8c330cee8d3168fdd1fbd472a17d1a75c00c3e"
+checksum = "136ef34e18462b17bf39a7826f8f3bbc223341f8e83822beb8b77db9a3d49696"
 dependencies = [
  "libc",
  "neli",
@@ -5332,6 +4405,21 @@ dependencies = [
  "value-bag",
 ]
 
+[[package]]
+name = "loom"
+version = "0.5.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ff50ecb28bb86013e935fb6683ab1f6d3a20016f123c76fd4c27470076ac30f5"
+dependencies = [
+ "cfg-if",
+ "generator",
+ "scoped-tls",
+ "serde",
+ "serde_json",
+ "tracing",
+ "tracing-subscriber",
+]
+
 [[package]]
 name = "loop9"
 version = "0.1.5"
@@ -5341,15 +4429,6 @@ dependencies = [
  "imgref",
 ]
 
-[[package]]
-name = "lru"
-version = "0.12.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "234cf4f4a04dc1f57e24b96cc0cd600cf2af460d4161ac5ecdd0af8e1f3b2a38"
-dependencies = [
- "hashbrown 0.15.0",
-]
-
 [[package]]
 name = "lzma-sys"
 version = "0.1.20"
@@ -5416,6 +4495,15 @@ dependencies = [
  "tendril",
 ]
 
+[[package]]
+name = "matchers"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558"
+dependencies = [
+ "regex-automata 0.1.10",
+]
+
 [[package]]
 name = "matches"
 version = "0.1.10"
@@ -5508,7 +4596,7 @@ checksum = "dcf09caffaac8068c346b6df2a7fc27a177fd20b39421a39ce0a211bde679a6c"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.77",
+ "syn 2.0.75",
 ]
 
 [[package]]
@@ -5517,26 +4605,6 @@ version = "0.3.17"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
 
-[[package]]
-name = "minicbor"
-version = "0.20.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9d15f4203d71fdf90903c2696e55426ac97a363c67b218488a73b534ce7aca10"
-dependencies = [
- "minicbor-derive",
-]
-
-[[package]]
-name = "minicbor-derive"
-version = "0.13.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1154809406efdb7982841adb6311b3d095b46f78342dd646736122fe6b19e267"
-dependencies = [
- "proc-macro2",
- "quote",
- "syn 1.0.109",
-]
-
 [[package]]
 name = "minimal-lexical"
 version = "0.2.1"
@@ -5564,15 +4632,6 @@ dependencies = [
  "simd-adler32",
 ]
 
-[[package]]
-name = "miniz_oxide"
-version = "0.8.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e2d80299ef12ff69b16a84bb182e3b9df68b5a91574d3d4fa6e41b65deec4df1"
-dependencies = [
- "adler2",
-]
-
 [[package]]
 name = "mio"
 version = "0.8.11"
@@ -5599,17 +4658,16 @@ dependencies = [
 
 [[package]]
 name = "muda"
-version = "0.15.1"
+version = "0.14.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b8123dfd4996055ac9b15a60ad263b44b01e539007523ad7a4a533a3d93b0591"
+checksum = "86c410a9d21523a819e84881603fbc00331c8001eb899964952046671deddb9c"
 dependencies = [
+ "cocoa 0.26.0",
  "crossbeam-channel",
  "dpi",
  "gtk",
  "keyboard-types",
- "objc2",
- "objc2-app-kit",
- "objc2-foundation",
+ "objc",
  "once_cell",
  "png",
  "serde",
@@ -5791,6 +4849,16 @@ dependencies = [
  "zbus 4.4.0",
 ]
 
+[[package]]
+name = "nu-ansi-term"
+version = "0.46.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84"
+dependencies = [
+ "overload",
+ "winapi",
+]
+
 [[package]]
 name = "num"
 version = "0.4.3"
@@ -5862,7 +4930,7 @@ checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.77",
+ "syn 2.0.75",
 ]
 
 [[package]]
@@ -5921,10 +4989,10 @@ version = "0.7.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "af1844ef2428cc3e1cb900be36181049ef3d3193c63e43026cfe202983b27a56"
 dependencies = [
- "proc-macro-crate 3.2.0",
+ "proc-macro-crate 3.1.0",
  "proc-macro2",
  "quote",
- "syn 2.0.77",
+ "syn 2.0.75",
 ]
 
 [[package]]
@@ -6077,23 +5145,9 @@ dependencies = [
 
 [[package]]
 name = "object"
-version = "0.32.2"
+version = "0.36.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a6a622008b6e321afc04970976f62ee297fdbaa6f95318ca343e3eebb9648441"
-dependencies = [
- "crc32fast",
- "flate2",
- "hashbrown 0.14.5",
- "indexmap 2.5.0",
- "memchr",
- "ruzstd",
-]
-
-[[package]]
-name = "object"
-version = "0.36.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "084f1a5821ac4c651660a94a7153d27ac9d8a53736203f58b31945ded098070a"
+checksum = "27b64972346851a39438c60b341ebc01bba47464ae329e55cf343eb93964efd9"
 dependencies = [
  "memchr",
 ]
@@ -6110,15 +5164,6 @@ dependencies = [
  "subtle",
 ]
 
-[[package]]
-name = "oid-registry"
-version = "0.6.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9bedf36ffb6ba96c2eb7144ef6270557b52e54b20c0a8e1eb2ff99a6c6959bff"
-dependencies = [
- "asn1-rs",
-]
-
 [[package]]
 name = "once-cell-regex"
 version = "0.2.1"
@@ -6175,7 +5220,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.77",
+ "syn 2.0.75",
 ]
 
 [[package]]
@@ -6253,10 +5298,10 @@ dependencies = [
 ]
 
 [[package]]
-name = "outref"
-version = "0.5.1"
+name = "overload"
+version = "0.1.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4030760ffd992bef45b0ae3f10ce1aba99e33464c90d14dd7c039884963ddc7a"
+checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39"
 
 [[package]]
 name = "owo-colors"
@@ -6296,7 +5341,7 @@ checksum = "d0a07c44bbe07756ba25605059fa4a94543f6a75730fd8bd1105795d0b3d668d"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.77",
+ "syn 2.0.75",
 ]
 
 [[package]]
@@ -6367,42 +5412,14 @@ dependencies = [
  "unicode-id-start",
 ]
 
-[[package]]
-name = "p12"
-version = "0.6.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d4873306de53fe82e7e484df31e1e947d61514b6ea2ed6cd7b45d63006fd9224"
-dependencies = [
- "cbc",
- "cipher",
- "des",
- "getrandom 0.2.15",
- "hmac",
- "lazy_static",
- "rc2",
- "sha1",
- "yasna",
-]
-
-[[package]]
-name = "p256"
-version = "0.11.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "51f44edd08f51e2ade572f141051021c5af22677e42b7dd28a88155151c33594"
-dependencies = [
- "ecdsa 0.14.8",
- "elliptic-curve 0.12.3",
- "sha2",
-]
-
 [[package]]
 name = "p256"
 version = "0.13.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "c9863ad85fa8f4460f9c48cb909d38a0d689dba1f6f6988a5e3e0d31071bcd4b"
 dependencies = [
- "ecdsa 0.16.9",
- "elliptic-curve 0.13.8",
+ "ecdsa",
+ "elliptic-curve",
  "primeorder",
  "sha2",
 ]
@@ -6413,8 +5430,8 @@ version = "0.13.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "70786f51bcc69f6a4c0360e063a4cac5419ef7c5cd5b3c99ad70f3be5ba79209"
 dependencies = [
- "ecdsa 0.16.9",
- "elliptic-curve 0.13.8",
+ "ecdsa",
+ "elliptic-curve",
  "primeorder",
  "sha2",
 ]
@@ -6425,9 +5442,9 @@ version = "0.13.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "0fc9e2161f1f215afdfce23677034ae137bbd45016a880c2eb3ba8eb95f085b2"
 dependencies = [
- "base16ct 0.2.0",
- "ecdsa 0.16.9",
- "elliptic-curve 0.13.8",
+ "base16ct",
+ "ecdsa",
+ "elliptic-curve",
  "primeorder",
  "rand_core 0.6.4",
  "sha2",
@@ -6532,29 +5549,6 @@ dependencies = [
  "hmac",
 ]
 
-[[package]]
-name = "pear"
-version = "0.2.9"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bdeeaa00ce488657faba8ebf44ab9361f9365a97bd39ffb8a60663f57ff4b467"
-dependencies = [
- "inlinable_string",
- "pear_codegen",
- "yansi",
-]
-
-[[package]]
-name = "pear_codegen"
-version = "0.2.9"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4bab5b985dc082b345f812b7df84e1bef27e7207b39e448439ba8bd69c93f147"
-dependencies = [
- "proc-macro2",
- "proc-macro2-diagnostics",
- "quote",
- "syn 2.0.77",
-]
-
 [[package]]
 name = "pem"
 version = "3.0.4"
@@ -6611,7 +5605,7 @@ dependencies = [
  "pest_meta",
  "proc-macro2",
  "quote",
- "syn 2.0.77",
+ "syn 2.0.75",
 ]
 
 [[package]]
@@ -6654,9 +5648,9 @@ dependencies = [
  "digest",
  "dsa",
  "eax",
- "ecdsa 0.16.9",
+ "ecdsa",
  "ed25519-dalek",
- "elliptic-curve 0.13.8",
+ "elliptic-curve",
  "flate2",
  "generic-array",
  "hex",
@@ -6671,7 +5665,7 @@ dependencies = [
  "num-traits",
  "num_enum",
  "ocb3",
- "p256 0.13.2",
+ "p256",
  "p384",
  "p521",
  "rand 0.8.5",
@@ -6681,7 +5675,7 @@ dependencies = [
  "sha1-checked",
  "sha2",
  "sha3",
- "signature 2.2.0",
+ "signature",
  "smallvec",
  "thiserror",
  "twofish",
@@ -6793,7 +5787,7 @@ dependencies = [
  "phf_shared 0.11.2",
  "proc-macro2",
  "quote",
- "syn 2.0.77",
+ "syn 2.0.75",
 ]
 
 [[package]]
@@ -6846,7 +5840,7 @@ checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.77",
+ "syn 2.0.75",
 ]
 
 [[package]]
@@ -6868,7 +5862,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "96c8c490f422ef9a4efd2cb5b42b76c8613d7e7dfc1caf667b8a3350a5acc066"
 dependencies = [
  "atomic-waker",
- "fastrand 2.1.1",
+ "fastrand 2.1.0",
  "futures-io",
 ]
 
@@ -6878,19 +5872,9 @@ version = "0.7.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f"
 dependencies = [
- "der 0.7.9",
- "pkcs8 0.10.2",
- "spki 0.7.3",
-]
-
-[[package]]
-name = "pkcs8"
-version = "0.9.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9eca2c590a5f85da82668fa685c09ce2888b9430e83299debf1f34b65fd4a4ba"
-dependencies = [
- "der 0.6.1",
- "spki 0.6.0",
+ "der",
+ "pkcs8",
+ "spki",
 ]
 
 [[package]]
@@ -6899,8 +5883,8 @@ version = "0.10.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7"
 dependencies = [
- "der 0.7.9",
- "spki 0.7.3",
+ "der",
+ "spki",
 ]
 
 [[package]]
@@ -6909,12 +5893,6 @@ version = "0.3.30"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec"
 
-[[package]]
-name = "plain"
-version = "0.2.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6"
-
 [[package]]
 name = "plist"
 version = "1.7.0"
@@ -6922,7 +5900,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "42cf17e9a1800f5f396bc67d193dc9411b59012a5876445ef450d449881e1016"
 dependencies = [
  "base64 0.22.1",
- "indexmap 2.5.0",
+ "indexmap 2.4.0",
  "quick-xml 0.32.0",
  "serde",
  "time",
@@ -6938,7 +5916,7 @@ dependencies = [
  "crc32fast",
  "fdeflate",
  "flate2",
- "miniz_oxide 0.7.4",
+ "miniz_oxide",
 ]
 
 [[package]]
@@ -6967,7 +5945,7 @@ dependencies = [
  "concurrent-queue",
  "hermit-abi 0.4.0",
  "pin-project-lite",
- "rustix 0.38.35",
+ "rustix 0.38.34",
  "tracing",
  "windows-sys 0.59.0",
 ]
@@ -6984,58 +5962,6 @@ dependencies = [
  "universal-hash",
 ]
 
-[[package]]
-name = "portmaster"
-version = "0.1.0"
-dependencies = [
- "assert_matches",
- "cached",
- "clap_lex",
- "ctor",
- "dark-light",
- "dataurl",
- "dirs 1.0.5",
- "futures-util",
- "gdk-pixbuf",
- "gdk-pixbuf-sys",
- "gio-sys",
- "glib",
- "glib-sys",
- "gtk",
- "gtk-sys",
- "http 1.1.0",
- "lazy_static",
- "log",
- "notify-rust",
- "open",
- "reqwest 0.12.7",
- "rfd 0.14.1",
- "rust-ini",
- "serde",
- "serde_json",
- "sha",
- "tauri",
- "tauri-build",
- "tauri-cli",
- "tauri-plugin-clipboard-manager",
- "tauri-plugin-dialog",
- "tauri-plugin-log",
- "tauri-plugin-notification",
- "tauri-plugin-os",
- "tauri-plugin-shell",
- "tauri-plugin-single-instance",
- "tauri-plugin-window-state",
- "tauri-winrt-notification 0.3.1",
- "thiserror",
- "tokio",
- "tokio-websockets",
- "url",
- "uuid",
- "which",
- "windows 0.54.0",
- "windows-service",
-]
-
 [[package]]
 name = "powerfmt"
 version = "0.2.0"
@@ -7063,7 +5989,7 @@ version = "0.13.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "353e1ca18966c16d9deb1c69278edbc5f194139612772bd9537af60ac231e1e6"
 dependencies = [
- "elliptic-curve 0.13.8",
+ "elliptic-curve",
 ]
 
 [[package]]
@@ -7087,11 +6013,11 @@ dependencies = [
 
 [[package]]
 name = "proc-macro-crate"
-version = "3.2.0"
+version = "3.1.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8ecf48c7ca261d60b74ab1a7b20da18bede46776b2e55535cb958eb595c5fa7b"
+checksum = "6d37c51ca738a55da99dc0c4a34860fd675453b8b36209178c2249bb13651284"
 dependencies = [
- "toml_edit 0.22.20",
+ "toml_edit 0.21.1",
 ]
 
 [[package]]
@@ -7133,19 +6059,6 @@ dependencies = [
  "unicode-ident",
 ]
 
-[[package]]
-name = "proc-macro2-diagnostics"
-version = "0.10.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "af066a9c399a26e020ada66a034357a868728e72cd426f3adcd35f80d88d88c8"
-dependencies = [
- "proc-macro2",
- "quote",
- "syn 2.0.77",
- "version_check",
- "yansi",
-]
-
 [[package]]
 name = "profiling"
 version = "1.0.15"
@@ -7162,7 +6075,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "8021cf59c8ec9c432cfc2526ac6b8aa508ecaf29cd415f271b8406c1b851c3fd"
 dependencies = [
  "quote",
- "syn 2.0.77",
+ "syn 2.0.75",
 ]
 
 [[package]]
@@ -7218,20 +6131,11 @@ dependencies = [
  "memchr",
 ]
 
-[[package]]
-name = "quick-xml"
-version = "0.36.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f7649a7b4df05aed9ea7ec6f628c67c9953a43869b8bc50929569b2999d443fe"
-dependencies = [
- "memchr",
-]
-
 [[package]]
 name = "quote"
-version = "1.0.37"
+version = "1.0.36"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af"
+checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7"
 dependencies = [
  "proc-macro2",
 ]
@@ -7323,44 +6227,6 @@ dependencies = [
  "rand_core 0.5.1",
 ]
 
-[[package]]
-name = "rasn"
-version = "0.12.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cf9b0d03fbc7d2dcfdd35086c43ce30ac5ff62ed7eff4397e4f4f2995a2b0e2a"
-dependencies = [
- "arrayvec 0.7.6",
- "bitvec",
- "bitvec-nom2",
- "bytes",
- "chrono",
- "either",
- "jzon",
- "konst",
- "nom",
- "num-bigint",
- "num-integer",
- "num-traits",
- "once_cell",
- "rasn-derive",
- "snafu",
-]
-
-[[package]]
-name = "rasn-derive"
-version = "0.12.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cbaf7105cd254b632f4732fbcc243ce750cef87d8335826125ef6df5733b5a0c"
-dependencies = [
- "either",
- "itertools 0.10.5",
- "proc-macro2",
- "quote",
- "rayon",
- "syn 1.0.109",
- "uuid",
-]
-
 [[package]]
 name = "rav1e"
 version = "0.7.1"
@@ -7436,15 +6302,6 @@ dependencies = [
  "crossbeam-utils",
 ]
 
-[[package]]
-name = "rc2"
-version = "0.8.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "62c64daa8e9438b84aaae55010a93f396f8e60e3911590fcba770d04643fc1dd"
-dependencies = [
- "cipher",
-]
-
 [[package]]
 name = "redox_syscall"
 version = "0.1.57"
@@ -7473,9 +6330,9 @@ dependencies = [
 
 [[package]]
 name = "redox_users"
-version = "0.4.6"
+version = "0.4.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43"
+checksum = "bd283d9651eeda4b2a83a43c1c91b266c40fd76ecd39a50a8c630ae69dc72891"
 dependencies = [
  "getrandom 0.2.15",
  "libredox",
@@ -7490,8 +6347,17 @@ checksum = "4219d74c6b67a3654a9fbebc4b419e22126d13d2f3c4a07ee0cb61ff79a79619"
 dependencies = [
  "aho-corasick",
  "memchr",
- "regex-automata",
- "regex-syntax",
+ "regex-automata 0.4.7",
+ "regex-syntax 0.8.4",
+]
+
+[[package]]
+name = "regex-automata"
+version = "0.1.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132"
+dependencies = [
+ "regex-syntax 0.6.29",
 ]
 
 [[package]]
@@ -7502,14 +6368,14 @@ checksum = "38caf58cc5ef2fed281f89292ef23f6365465ed9a41b7a7754eb4e26496c92df"
 dependencies = [
  "aho-corasick",
  "memchr",
- "regex-syntax",
+ "regex-syntax 0.8.4",
 ]
 
 [[package]]
-name = "regex-lite"
-version = "0.1.6"
+name = "regex-syntax"
+version = "0.6.29"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "53a49587ad06b26609c52e423de037e7f57f20d53535d66e08c695f347df952a"
+checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1"
 
 [[package]]
 name = "regex-syntax"
@@ -7528,51 +6394,9 @@ dependencies = [
 
 [[package]]
 name = "reqwest"
-version = "0.11.27"
+version = "0.12.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "dd67538700a17451e7cba03ac727fb961abb7607553461627b97de0b89cf4a62"
-dependencies = [
- "base64 0.21.7",
- "bytes",
- "encoding_rs",
- "futures-core",
- "futures-util",
- "h2 0.3.26",
- "http 0.2.12",
- "http-body 0.4.6",
- "hyper 0.14.30",
- "hyper-rustls 0.24.2",
- "ipnet",
- "js-sys",
- "log",
- "mime",
- "once_cell",
- "percent-encoding",
- "pin-project-lite",
- "rustls 0.21.12",
- "rustls-native-certs 0.6.3",
- "rustls-pemfile 1.0.4",
- "serde",
- "serde_json",
- "serde_urlencoded",
- "sync_wrapper 0.1.2",
- "system-configuration 0.5.1",
- "tokio",
- "tokio-rustls 0.24.1",
- "tower-service",
- "url",
- "wasm-bindgen",
- "wasm-bindgen-futures",
- "web-sys",
- "webpki-roots 0.25.4",
- "winreg 0.50.0",
-]
-
-[[package]]
-name = "reqwest"
-version = "0.12.7"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f8f4955649ef5c38cc7f9e8aa41761d48fb9677197daea9984dc54f56aad5e63"
+checksum = "c7d6d2a27d57148378eb5e111173f4276ad26340ecc5c49a4a2152167a2d6a37"
 dependencies = [
  "base64 0.22.1",
  "bytes",
@@ -7580,12 +6404,12 @@ dependencies = [
  "futures-channel",
  "futures-core",
  "futures-util",
- "h2 0.4.6",
- "http 1.1.0",
- "http-body 1.0.1",
+ "h2",
+ "http",
+ "http-body",
  "http-body-util",
- "hyper 1.4.1",
- "hyper-rustls 0.27.3",
+ "hyper",
+ "hyper-rustls",
  "hyper-tls",
  "hyper-util",
  "ipnet",
@@ -7596,12 +6420,12 @@ dependencies = [
  "once_cell",
  "percent-encoding",
  "pin-project-lite",
- "rustls-pemfile 2.1.3",
+ "rustls-pemfile",
  "serde",
  "serde_json",
  "serde_urlencoded",
  "sync_wrapper 1.0.1",
- "system-configuration 0.6.1",
+ "system-configuration",
  "tokio",
  "tokio-native-tls",
  "tokio-util",
@@ -7611,7 +6435,7 @@ dependencies = [
  "wasm-bindgen-futures",
  "wasm-streams",
  "web-sys",
- "windows-registry",
+ "winreg",
 ]
 
 [[package]]
@@ -7631,17 +6455,6 @@ dependencies = [
  "zune-jpeg",
 ]
 
-[[package]]
-name = "rfc6979"
-version = "0.3.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7743f17af12fa0b03b803ba12cd6a8d9483a587e89c69445e3909655c0b9fabb"
-dependencies = [
- "crypto-bigint 0.4.9",
- "hmac",
- "zeroize",
-]
-
 [[package]]
 name = "rfc6979"
 version = "0.4.0"
@@ -7676,34 +6489,11 @@ dependencies = [
  "windows-sys 0.48.0",
 ]
 
-[[package]]
-name = "rfd"
-version = "0.15.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8af382a047821a08aa6bfc09ab0d80ff48d45d8726f7cd8e44891f7cb4a4278e"
-dependencies = [
- "ashpd 0.9.1",
- "block2",
- "glib-sys",
- "gobject-sys",
- "gtk-sys",
- "js-sys",
- "log",
- "objc2",
- "objc2-app-kit",
- "objc2-foundation",
- "raw-window-handle",
- "wasm-bindgen",
- "wasm-bindgen-futures",
- "web-sys",
- "windows-sys 0.48.0",
-]
-
 [[package]]
 name = "rgb"
-version = "0.8.50"
+version = "0.8.48"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "57397d16646700483b67d2dd6511d79318f9d057fdbd21a4066aeac8b41d310a"
+checksum = "0f86ae463694029097b846d8f99fd5536740602ae00022c0c50c5600720b2f71"
 dependencies = [
  "bytemuck",
 ]
@@ -7734,9 +6524,9 @@ dependencies = [
 
 [[package]]
 name = "rkyv"
-version = "0.7.45"
+version = "0.7.44"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9008cd6385b9e161d8229e1f6549dd23c3d022f132a2ea37ac3a10ac4935779b"
+checksum = "5cba464629b3394fc4dbc6f940ff8f5b4ff5c7aef40f29166fd4ad12acbc99c0"
 dependencies = [
  "bitvec",
  "bytecheck",
@@ -7752,9 +6542,9 @@ dependencies = [
 
 [[package]]
 name = "rkyv_derive"
-version = "0.7.45"
+version = "0.7.44"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "503d1d27590a2b0a3a4ca4c94755aa2875657196ecbf401a42eff41d7de532c0"
+checksum = "a7dddfff8de25e6f62b9d64e6e432bf1c6736c57d20323e15ee10435fbda7c65"
 dependencies = [
  "proc-macro2",
  "quote",
@@ -7826,10 +6616,10 @@ dependencies = [
  "num-integer",
  "num-traits",
  "pkcs1",
- "pkcs8 0.10.2",
+ "pkcs8",
  "rand_core 0.6.4",
- "signature 2.2.0",
- "spki 0.7.3",
+ "signature",
+ "spki",
  "subtle",
  "zeroize",
 ]
@@ -7868,9 +6658,9 @@ dependencies = [
 
 [[package]]
 name = "rust_decimal"
-version = "1.36.0"
+version = "1.35.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b082d80e3e3cc52b2ed634388d436fe1f4de6af5786cc2de9ba9737527bdf555"
+checksum = "1790d1c4c0ca81211399e0e0af16333276f375209e71a37b67698a373db5b47a"
 dependencies = [
  "arrayvec 0.7.6",
  "borsh",
@@ -7896,22 +6686,13 @@ checksum = "583034fd73374156e66797ed8e5b0d5690409c9226b22d87cb7f19821c05d152"
 
 [[package]]
 name = "rustc_version"
-version = "0.4.1"
+version = "0.4.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92"
+checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366"
 dependencies = [
  "semver",
 ]
 
-[[package]]
-name = "rusticata-macros"
-version = "4.1.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "faf0c4a6ece9950b9abdb62b1cfcf2a68b3b67a10ba445b3bb85be2a293d0632"
-dependencies = [
- "nom",
-]
-
 [[package]]
 name = "rustix"
 version = "0.37.27"
@@ -7928,9 +6709,9 @@ dependencies = [
 
 [[package]]
 name = "rustix"
-version = "0.38.35"
+version = "0.38.34"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a85d50532239da68e9addb745ba38ff4612a242c1c7ceea689c4bc7c2f43c36f"
+checksum = "70dc5ec042f7a43c4a73241207cecc9873a06d45debb38b329f8541d85c2730f"
 dependencies = [
  "bitflags 2.6.0",
  "errno",
@@ -7939,28 +6720,15 @@ dependencies = [
  "windows-sys 0.52.0",
 ]
 
-[[package]]
-name = "rustls"
-version = "0.21.12"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3f56a14d1f48b391359b22f731fd4bd7e43c97f3c50eee276f3aa09c94784d3e"
-dependencies = [
- "log",
- "ring",
- "rustls-webpki 0.101.7",
- "sct",
-]
-
 [[package]]
 name = "rustls"
 version = "0.22.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "bf4ef73721ac7bcd79b2b315da7779d8fc09718c6b3d2d1b2d94850eb8c18432"
 dependencies = [
- "log",
  "ring",
  "rustls-pki-types",
- "rustls-webpki 0.102.7",
+ "rustls-webpki",
  "subtle",
  "zeroize",
 ]
@@ -7975,45 +6743,11 @@ dependencies = [
  "once_cell",
  "ring",
  "rustls-pki-types",
- "rustls-webpki 0.102.7",
+ "rustls-webpki",
  "subtle",
  "zeroize",
 ]
 
-[[package]]
-name = "rustls-native-certs"
-version = "0.6.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a9aace74cb666635c918e9c12bc0d348266037aa8eb599b5cba565709a8dff00"
-dependencies = [
- "openssl-probe",
- "rustls-pemfile 1.0.4",
- "schannel",
- "security-framework",
-]
-
-[[package]]
-name = "rustls-native-certs"
-version = "0.7.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e5bfb394eeed242e909609f56089eecfe5fda225042e8b171791b9c95f5931e5"
-dependencies = [
- "openssl-probe",
- "rustls-pemfile 2.1.3",
- "rustls-pki-types",
- "schannel",
- "security-framework",
-]
-
-[[package]]
-name = "rustls-pemfile"
-version = "1.0.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c"
-dependencies = [
- "base64 0.21.7",
-]
-
 [[package]]
 name = "rustls-pemfile"
 version = "2.1.3"
@@ -8032,19 +6766,9 @@ checksum = "fc0a2ce646f8655401bb81e7927b812614bd5d91dbc968696be50603510fcaf0"
 
 [[package]]
 name = "rustls-webpki"
-version = "0.101.7"
+version = "0.102.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765"
-dependencies = [
- "ring",
- "untrusted",
-]
-
-[[package]]
-name = "rustls-webpki"
-version = "0.102.7"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "84678086bd54edf2b415183ed7a94d0efb049f1b646a33e22a36f3794be6ae56"
+checksum = "8e6b52d4fda176fd835fdc55a835d4a89b8499cad995885a21149d5ad62f852e"
 dependencies = [
  "ring",
  "rustls-pki-types",
@@ -8075,17 +6799,6 @@ dependencies = [
  "unicode-script",
 ]
 
-[[package]]
-name = "ruzstd"
-version = "0.5.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "58c4eb8a81997cf040a091d1f7e1938aeab6749d3a0dfa73af43cdc32393483d"
-dependencies = [
- "byteorder",
- "derive_more",
- "twox-hash",
-]
-
 [[package]]
 name = "ryu"
 version = "1.0.18"
@@ -8131,7 +6844,6 @@ dependencies = [
  "serde",
  "serde_json",
  "url",
- "uuid",
 ]
 
 [[package]]
@@ -8143,7 +6855,7 @@ dependencies = [
  "proc-macro2",
  "quote",
  "serde_derive_internals",
- "syn 2.0.77",
+ "syn 2.0.75",
 ]
 
 [[package]]
@@ -8158,26 +6870,6 @@ version = "1.2.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
 
-[[package]]
-name = "scroll"
-version = "0.12.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6ab8598aa408498679922eff7fa985c25d58a90771bd6be794434c5277eab1a6"
-dependencies = [
- "scroll_derive",
-]
-
-[[package]]
-name = "scroll_derive"
-version = "0.12.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7f81c2fde025af7e69b1d1420531c8a8811ca898919db177141a85313b1cb932"
-dependencies = [
- "proc-macro2",
- "quote",
- "syn 2.0.77",
-]
-
 [[package]]
 name = "scrypt"
 version = "0.11.0"
@@ -8189,46 +6881,22 @@ dependencies = [
  "sha2",
 ]
 
-[[package]]
-name = "sct"
-version = "0.7.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414"
-dependencies = [
- "ring",
- "untrusted",
-]
-
 [[package]]
 name = "seahash"
 version = "4.1.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b"
 
-[[package]]
-name = "sec1"
-version = "0.3.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3be24c1842290c45df0a7bf069e0c268a747ad05a192f2fd7dcfdbc1cba40928"
-dependencies = [
- "base16ct 0.1.1",
- "der 0.6.1",
- "generic-array",
- "pkcs8 0.9.0",
- "subtle",
- "zeroize",
-]
-
 [[package]]
 name = "sec1"
 version = "0.7.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc"
 dependencies = [
- "base16ct 0.2.0",
- "der 0.7.9",
+ "base16ct",
+ "der",
  "generic-array",
- "pkcs8 0.10.2",
+ "pkcs8",
  "subtle",
  "zeroize",
 ]
@@ -8293,9 +6961,9 @@ checksum = "a3f0bf26fd526d2a95683cd0f87bf103b8539e2ca1ef48ce002d67aad59aa0b4"
 
 [[package]]
 name = "serde"
-version = "1.0.209"
+version = "1.0.208"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "99fce0ffe7310761ca6bf9faf5115afbc19688edd00171d81b1bb1b116c63e09"
+checksum = "cff085d2cb684faa248efb494c39b68e522822ac0de72ccf08109abde717cfb2"
 dependencies = [
  "serde_derive",
 ]
@@ -8321,27 +6989,15 @@ dependencies = [
  "serde",
 ]
 
-[[package]]
-name = "serde-xml-rs"
-version = "0.6.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fb3aa78ecda1ebc9ec9847d5d3aba7d618823446a049ba2491940506da6e2782"
-dependencies = [
- "log",
- "serde",
- "thiserror",
- "xml-rs",
-]
-
 [[package]]
 name = "serde_derive"
-version = "1.0.209"
+version = "1.0.208"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a5831b979fd7b5439637af1752d535ff49f4860c0f341d1baeb6faf0f4242170"
+checksum = "24008e81ff7613ed8e5ba0cfaf24e2c2f1e5b8a0495711e44fcd4882fca62bcf"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.77",
+ "syn 2.0.75",
 ]
 
 [[package]]
@@ -8352,7 +7008,7 @@ checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.77",
+ "syn 2.0.75",
 ]
 
 [[package]]
@@ -8366,11 +7022,11 @@ dependencies = [
 
 [[package]]
 name = "serde_json"
-version = "1.0.128"
+version = "1.0.125"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6ff5456707a1de34e7e37f2a6fd3d3f808c318259cbd01ab6377795054b483d8"
+checksum = "83c8e735a073ccf5be70aa8066aa984eaf2fa000db6c8d0100ae605b366d31ed"
 dependencies = [
- "indexmap 2.5.0",
+ "indexmap 2.4.0",
  "itoa 1.0.11",
  "memchr",
  "ryu",
@@ -8395,7 +7051,7 @@ checksum = "6c64451ba24fc7a6a2d60fc75dd9c83c90903b19028d4eff35e88fc1e86564e9"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.77",
+ "syn 2.0.75",
 ]
 
 [[package]]
@@ -8429,7 +7085,7 @@ dependencies = [
  "chrono",
  "hex",
  "indexmap 1.9.3",
- "indexmap 2.5.0",
+ "indexmap 2.4.0",
  "serde",
  "serde_derive",
  "serde_json",
@@ -8446,20 +7102,7 @@ dependencies = [
  "darling 0.20.10",
  "proc-macro2",
  "quote",
- "syn 2.0.77",
-]
-
-[[package]]
-name = "serde_yaml"
-version = "0.9.34+deprecated"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47"
-dependencies = [
- "indexmap 2.5.0",
- "itoa 1.0.11",
- "ryu",
- "serde",
- "unsafe-libyaml",
+ "syn 2.0.75",
 ]
 
 [[package]]
@@ -8551,6 +7194,15 @@ dependencies = [
  "keccak",
 ]
 
+[[package]]
+name = "sharded-slab"
+version = "0.1.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6"
+dependencies = [
+ "lazy_static",
+]
+
 [[package]]
 name = "shared_child"
 version = "1.0.1"
@@ -8582,16 +7234,6 @@ dependencies = [
  "libc",
 ]
 
-[[package]]
-name = "signature"
-version = "1.6.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "74233d3b3b2f6d4b006dc19dee745e73e2a6bfb6f93607cd3b02bd5b00797d7c"
-dependencies = [
- "digest",
- "rand_core 0.6.4",
-]
-
 [[package]]
 name = "signature"
 version = "2.2.0"
@@ -8623,24 +7265,6 @@ version = "0.1.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "f27f6278552951f1f2b8cf9da965d10969b2efdea95a6ec47987ab46edfe263a"
 
-[[package]]
-name = "simple-file-manifest"
-version = "0.11.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5dd19be0257552dd56d1bb6946f89f193c6e5b9f13cc9327c4bc84a357507c74"
-
-[[package]]
-name = "simple_asn1"
-version = "0.6.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "adc4e5204eb1910f40f9cfa375f6f05b68c3abac4b6fd879c8ff5e7ae8a0a085"
-dependencies = [
- "num-bigint",
- "num-traits",
- "thiserror",
- "time",
-]
-
 [[package]]
 name = "simplecss"
 version = "0.2.1"
@@ -8692,29 +7316,6 @@ version = "0.3.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "b7c388c1b5e93756d0c740965c41e8822f866621d41acbdf6336a6a168f8840c"
 
-[[package]]
-name = "snafu"
-version = "0.7.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e4de37ad025c587a29e8f3f5605c00f70b98715ef90b9061a815b9e59e9042d6"
-dependencies = [
- "backtrace",
- "doc-comment",
- "snafu-derive",
-]
-
-[[package]]
-name = "snafu-derive"
-version = "0.7.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "990079665f075b699031e9c08fd3ab99be5029b96f3b78dc0709e8f77e4efebf"
-dependencies = [
- "heck 0.4.1",
- "proc-macro2",
- "quote",
- "syn 1.0.109",
-]
-
 [[package]]
 name = "socket2"
 version = "0.4.10"
@@ -8778,7 +7379,7 @@ dependencies = [
  "base64 0.22.1",
  "bytes",
  "futures",
- "http 1.1.0",
+ "http",
  "httparse",
  "log",
  "rand 0.8.5",
@@ -8811,18 +7412,6 @@ dependencies = [
  "system-deps",
 ]
 
-[[package]]
-name = "spake2"
-version = "0.4.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c5482afe85a0b6ce956c945401598dbc527593c77ba51d0a87a586938b1b893a"
-dependencies = [
- "curve25519-dalek",
- "hkdf",
- "rand_core 0.6.4",
- "sha2",
-]
-
 [[package]]
 name = "spin"
 version = "0.9.8"
@@ -8832,16 +7421,6 @@ dependencies = [
  "lock_api",
 ]
 
-[[package]]
-name = "spki"
-version = "0.6.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "67cf02bbac7a337dc36e4f5a693db6c21e7863f45070f7064577eb4367a3212b"
-dependencies = [
- "base64ct",
- "der 0.6.1",
-]
-
 [[package]]
 name = "spki"
 version = "0.7.3"
@@ -8849,7 +7428,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d"
 dependencies = [
  "base64ct",
- "der 0.7.9",
+ "der",
 ]
 
 [[package]]
@@ -8858,6 +7437,15 @@ version = "1.2.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3"
 
+[[package]]
+name = "state"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2b8c4a4445d81357df8b1a650d0d0d6fbbbfe99d064aa5e02f3e4022061476d8"
+dependencies = [
+ "loom",
+]
+
 [[package]]
 name = "static_assertions"
 version = "1.1.0"
@@ -9021,9 +7609,9 @@ dependencies = [
 
 [[package]]
 name = "svgtypes"
-version = "0.15.2"
+version = "0.15.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "794de53cc48eaabeed0ab6a3404a65f40b3e38c067e4435883a65d2aa4ca000e"
+checksum = "fae3064df9b89391c9a76a0425a69d124aee9c5c28455204709e72c39868a43c"
 dependencies = [
  "kurbo",
  "siphasher 1.0.1",
@@ -9053,9 +7641,9 @@ dependencies = [
 
 [[package]]
 name = "syn"
-version = "2.0.77"
+version = "2.0.75"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9f35bcdf61fd8e7be6caf75f429fdca8beb3ed76584befb503b1569faee373ed"
+checksum = "f6af063034fc1935ede7be0122941bafa9bacb949334d090b77ca98b5817c7d9"
 dependencies = [
  "proc-macro2",
  "quote",
@@ -9071,7 +7659,7 @@ dependencies = [
  "proc-macro-error",
  "proc-macro2",
  "quote",
- "syn 2.0.77",
+ "syn 2.0.75",
 ]
 
 [[package]]
@@ -9085,21 +7673,6 @@ name = "sync_wrapper"
 version = "1.0.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "a7065abeca94b6a8a577f9bd45aa0867a2238b74e8eb67cf10d492bc39351394"
-dependencies = [
- "futures-core",
-]
-
-[[package]]
-name = "synstructure"
-version = "0.12.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f36bdaa60a83aca3921b5259d5400cbf5e90fc51931376a9bd4a0eb79aa7210f"
-dependencies = [
- "proc-macro2",
- "quote",
- "syn 1.0.109",
- "unicode-xid",
-]
 
 [[package]]
 name = "sys-locale"
@@ -9131,18 +7704,7 @@ checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7"
 dependencies = [
  "bitflags 1.3.2",
  "core-foundation 0.9.4",
- "system-configuration-sys 0.5.0",
-]
-
-[[package]]
-name = "system-configuration"
-version = "0.6.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b"
-dependencies = [
- "bitflags 2.6.0",
- "core-foundation 0.9.4",
- "system-configuration-sys 0.6.0",
+ "system-configuration-sys",
 ]
 
 [[package]]
@@ -9155,16 +7717,6 @@ dependencies = [
  "libc",
 ]
 
-[[package]]
-name = "system-configuration-sys"
-version = "0.6.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4"
-dependencies = [
- "core-foundation-sys",
- "libc",
-]
-
 [[package]]
 name = "system-deps"
 version = "6.2.2"
@@ -9180,12 +7732,12 @@ dependencies = [
 
 [[package]]
 name = "tao"
-version = "0.30.3"
+version = "0.29.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a0dbbebe82d02044dfa481adca1550d6dd7bd16e086bc34fa0fbecceb5a63751"
+checksum = "d3a97abbc7d6cfd0720da3e06fcb1cf2ac87cbfdb5bbbce103a1279a211c4d81"
 dependencies = [
  "bitflags 2.6.0",
- "cocoa",
+ "cocoa 0.26.0",
  "core-foundation 0.10.0",
  "core-graphics 0.24.0",
  "crossbeam-channel",
@@ -9219,13 +7771,13 @@ dependencies = [
 
 [[package]]
 name = "tao-macros"
-version = "0.1.3"
+version = "0.1.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f4e16beb8b2ac17db28eab8bca40e62dbfbb34c0fcdc6d9826b11b7b5d047dfd"
+checksum = "ec114582505d158b669b136e6851f85840c109819d77c42bb7c0709f727d18c2"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.77",
+ "syn 1.0.109",
 ]
 
 [[package]]
@@ -9253,12 +7805,13 @@ checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1"
 
 [[package]]
 name = "tauri"
-version = "2.0.2"
+version = "2.0.0-rc.8"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5920aad0804ea5e86808d4b6e8753d3bcbae7efc8f4e41a4da00b45427559868"
+checksum = "e8345ccc676ef16e26b61fc0f5340b4e770678b1e1f53f08c69ebdac5e56b422"
 dependencies = [
  "anyhow",
  "bytes",
+ "cocoa 0.26.0",
  "dirs 5.0.1",
  "dunce",
  "embed_plist",
@@ -9267,30 +7820,28 @@ dependencies = [
  "glob",
  "gtk",
  "heck 0.5.0",
- "http 1.1.0",
- "image",
+ "http",
+ "image 0.24.9",
  "jni",
  "libc",
  "log",
  "mime",
  "muda",
- "objc2",
- "objc2-app-kit",
- "objc2-foundation",
+ "objc",
  "percent-encoding",
- "plist",
  "raw-window-handle",
- "reqwest 0.12.7",
+ "reqwest",
  "serde",
  "serde_json",
  "serde_repr",
  "serialize-to-javascript",
+ "state",
  "swift-rs",
  "tauri-build",
  "tauri-macros",
  "tauri-runtime",
  "tauri-runtime-wry",
- "tauri-utils 2.0.1",
+ "tauri-utils 2.0.0-rc.7",
  "thiserror",
  "tokio",
  "tray-icon",
@@ -9304,9 +7855,9 @@ dependencies = [
 
 [[package]]
 name = "tauri-build"
-version = "2.0.1"
+version = "2.0.0-rc.7"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "935f9b3c49b22b3e2e485a57f46d61cd1ae07b1cbb2ba87387a387caf2d8c4e7"
+checksum = "7d5ad5fcfaf02cf79aa6727f6c5df38567d8dce172b00b62690c6bc46c08b7ce"
 dependencies = [
  "anyhow",
  "cargo_toml",
@@ -9318,7 +7869,7 @@ dependencies = [
  "semver",
  "serde",
  "serde_json",
- "tauri-utils 2.0.1",
+ "tauri-utils 2.0.0-rc.7",
  "tauri-winres",
  "toml 0.8.19",
  "walkdir",
@@ -9326,9 +7877,9 @@ dependencies = [
 
 [[package]]
 name = "tauri-bundler"
-version = "2.0.2"
+version = "2.0.1-rc.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cc59028d72f54cd39aecd61c662fc0a879473962bedb3de98eb129f59ccb0207"
+checksum = "00a0e120d67416e774923fc07a7b531ba6898ed08f0d6e07fe79d7e3da7e0c04"
 dependencies = [
  "anyhow",
  "ar",
@@ -9340,7 +7891,7 @@ dependencies = [
  "handlebars",
  "heck 0.5.0",
  "hex",
- "image",
+ "image 0.25.2",
  "log",
  "md5",
  "os_pipe",
@@ -9356,31 +7907,29 @@ dependencies = [
  "tar",
  "tauri-icns",
  "tauri-macos-sign",
- "tauri-utils 2.0.1",
+ "tauri-utils 2.0.0-rc.7",
  "tempfile",
  "thiserror",
  "time",
  "ureq",
- "url",
  "uuid",
  "walkdir",
  "windows-registry",
  "windows-sys 0.59.0",
- "zip 2.2.0",
+ "zip",
 ]
 
 [[package]]
 name = "tauri-cli"
-version = "2.0.2"
+version = "2.0.0-rc.8"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1c793c2f9430eb95040abac4ac78cd5a955c7aae13213a7d54e3e209c161c369"
+checksum = "ddc42d318b25224c40f88550308f8cc8746a38534d450ce9c553f8c93f335499"
 dependencies = [
  "anyhow",
- "ar",
  "axum",
  "base64 0.22.1",
  "cargo-mobile2",
- "clap 4.5.17",
+ "clap 4.5.16",
  "clap_complete",
  "colored",
  "common-path",
@@ -9390,13 +7939,13 @@ dependencies = [
  "duct",
  "dunce",
  "elf",
- "env_logger 0.11.5",
+ "env_logger",
  "glob",
  "handlebars",
  "heck 0.5.0",
  "html5ever",
  "ignore",
- "image",
+ "image 0.25.2",
  "include_dir",
  "itertools 0.13.0",
  "json-patch 2.0.0",
@@ -9414,7 +7963,6 @@ dependencies = [
  "minisign",
  "notify",
  "notify-debouncer-mini",
- "object 0.36.4",
  "os_info",
  "os_pipe",
  "oxc_allocator",
@@ -9423,7 +7971,6 @@ dependencies = [
  "oxc_span",
  "phf 0.11.2",
  "plist",
- "rand 0.8.5",
  "regex",
  "resvg",
  "semver",
@@ -9436,23 +7983,22 @@ dependencies = [
  "tauri-icns",
  "tauri-macos-sign",
  "tauri-utils 1.6.0",
- "tauri-utils 2.0.1",
+ "tauri-utils 2.0.0-rc.7",
  "tempfile",
  "tokio",
  "toml 0.8.19",
  "toml_edit 0.22.20",
  "ureq",
  "url",
- "uuid",
  "walkdir",
  "windows-sys 0.59.0",
 ]
 
 [[package]]
 name = "tauri-codegen"
-version = "2.0.1"
+version = "2.0.0-rc.7"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "95d7443dd4f0b597704b6a14b964ee2ed16e99928d8e6292ae9825f09fbcd30e"
+checksum = "809ef6316726fc72593d296cf6f4e7461326e310c313d6a6c42b6e7f1e2671cf"
 dependencies = [
  "base64 0.22.1",
  "brotli",
@@ -9466,8 +8012,8 @@ dependencies = [
  "serde",
  "serde_json",
  "sha2",
- "syn 2.0.77",
- "tauri-utils 2.0.1",
+ "syn 2.0.75",
+ "tauri-utils 2.0.0-rc.7",
  "thiserror",
  "time",
  "url",
@@ -9487,18 +8033,15 @@ dependencies = [
 
 [[package]]
 name = "tauri-macos-sign"
-version = "2.0.1"
+version = "0.1.1-rc.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "be95e2d37d1e3605831da071234418c786684af48982509af1b7f50b31af4546"
+checksum = "cea6debc5d6182af338fb92bda2b399332d6495d3cbc1238e5cbbf82ad56c09b"
 dependencies = [
  "anyhow",
- "apple-codesign",
- "chrono",
  "dirs-next",
  "log",
  "once-cell-regex",
  "os_pipe",
- "p12",
  "plist",
  "rand 0.8.5",
  "serde",
@@ -9509,23 +8052,23 @@ dependencies = [
 
 [[package]]
 name = "tauri-macros"
-version = "2.0.1"
+version = "2.0.0-rc.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4d2c0963ccfc3f5194415f2cce7acc975942a8797fbabfb0aa1ed6f59326ae7f"
+checksum = "1359e8861d210d25731f8b1bfbb4d111dd06406cf73c59659366ef450364d811"
 dependencies = [
  "heck 0.5.0",
  "proc-macro2",
  "quote",
- "syn 2.0.77",
+ "syn 2.0.75",
  "tauri-codegen",
- "tauri-utils 2.0.1",
+ "tauri-utils 2.0.0-rc.7",
 ]
 
 [[package]]
 name = "tauri-plugin"
-version = "2.0.1"
+version = "2.0.0-rc.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b2e6660a409963e4d57b9bfab4addd141eeff41bd3a7fb14e13004a832cf7ef6"
+checksum = "ec01af01098a286d3e430c1fa947bfd77bc8011ecb209438af4444b02d82b29e"
 dependencies = [
  "anyhow",
  "glob",
@@ -9533,19 +8076,19 @@ dependencies = [
  "schemars",
  "serde",
  "serde_json",
- "tauri-utils 2.0.1",
+ "tauri-utils 2.0.0-rc.7",
  "toml 0.8.19",
  "walkdir",
 ]
 
 [[package]]
 name = "tauri-plugin-clipboard-manager"
-version = "2.0.1"
+version = "2.0.0-rc.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "78b7d556886c15849198c0948fd7f4c880492f0461539176da0a8a70272e2904"
+checksum = "6719e5f3fcc6c6f5afda68da944a44a50c28f30a3506c457ea7dbcc13377bfe0"
 dependencies = [
  "arboard",
- "image",
+ "image 0.24.9",
  "log",
  "serde",
  "serde_json",
@@ -9556,32 +8099,30 @@ dependencies = [
 
 [[package]]
 name = "tauri-plugin-dialog"
-version = "2.0.1"
+version = "2.0.0-rc.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ddb2fe88b602461c118722c574e2775ab26a4e68886680583874b2f6520608b7"
+checksum = "fcf99017391fdc40b6c8ae0dae8d970cc8151a8177d48b8805f320f52cac0e3c"
 dependencies = [
+ "dunce",
  "log",
  "raw-window-handle",
- "rfd 0.15.0",
+ "rfd",
  "serde",
  "serde_json",
  "tauri",
  "tauri-plugin",
  "tauri-plugin-fs",
  "thiserror",
- "url",
 ]
 
 [[package]]
 name = "tauri-plugin-fs"
-version = "2.0.1"
+version = "2.0.0-rc.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ab300488ebec3487ca5f56289692e7e45feb07eea8d5e1dba497f7dc9dd9c407"
+checksum = "5df6b25b1f2b7b61565e66c4dbee9eb39e5635d2a763206e380e07cc3f601a67"
 dependencies = [
  "anyhow",
- "dunce",
  "glob",
- "percent-encoding",
  "schemars",
  "serde",
  "serde_json",
@@ -9595,13 +8136,13 @@ dependencies = [
 
 [[package]]
 name = "tauri-plugin-log"
-version = "2.0.1"
+version = "2.0.0-rc.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9a49f2c05d15e6375ab7f7e528b3049150ba4dfafdf61f85e5178d0aef18e3f5"
+checksum = "380d27f23c39cde6a73024e65d8ec9b5b0af861e968dbe16b3aad86cd2c578e5"
 dependencies = [
  "android_logger",
  "byte-unit",
- "cocoa",
+ "cocoa 0.25.0",
  "fern",
  "log",
  "objc",
@@ -9617,9 +8158,9 @@ dependencies = [
 
 [[package]]
 name = "tauri-plugin-notification"
-version = "2.0.1"
+version = "2.0.0-rc.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ef492a2d19b6376bb4c9e0c4fab3f3bf8a220ea112d24f35027b737ff55de20c"
+checksum = "35677cdcdb4dc3f3ef6891f31b8ea314045064752912d66e676a4f1577b57ffa"
 dependencies = [
  "log",
  "notify-rust",
@@ -9636,11 +8177,11 @@ dependencies = [
 
 [[package]]
 name = "tauri-plugin-os"
-version = "2.0.1"
+version = "2.0.0-rc.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fbc5f23a86f37687c7f4fecfdc706b279087bc44f7a46702f7307ff1551ee03a"
+checksum = "6b54cfeb26356822d3be3db4282041b03552f573a694b6b28aded7d95c62a039"
 dependencies = [
- "gethostname 0.5.0",
+ "gethostname",
  "log",
  "os_info",
  "serde",
@@ -9654,9 +8195,9 @@ dependencies = [
 
 [[package]]
 name = "tauri-plugin-shell"
-version = "2.0.1"
+version = "2.0.0-rc.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "371fb9aca2823990a2d0db7970573be5fdf07881fcaa2b835b29631feb84aec1"
+checksum = "2133e5c6fe2ae0263ff5920feed477d3b1413f89033f537966831b0cb6f61f8e"
 dependencies = [
  "encoding_rs",
  "log",
@@ -9675,24 +8216,24 @@ dependencies = [
 
 [[package]]
 name = "tauri-plugin-single-instance"
-version = "2.0.1"
+version = "2.0.0-rc.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a25ac834491d089699a2bc9266a662faf373c9f779f05a2235bc6e4d9e61769a"
+checksum = "de552151b4c9ba9ff72c7244dccaadd47f88d1f0d5caa2603c5c1c12b7636edc"
 dependencies = [
  "log",
  "serde",
  "serde_json",
  "tauri",
  "thiserror",
- "windows-sys 0.59.0",
+ "windows-sys 0.52.0",
  "zbus 4.4.0",
 ]
 
 [[package]]
 name = "tauri-plugin-window-state"
-version = "2.0.1"
+version = "2.0.0-rc.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fd1cef203a15b4772898e7bc8e57c1f34696e39848987dfcd294d51ba0525650"
+checksum = "eb6839228cbd225b95681c766cc51113e9dad62c4b3f6ebb102234413ba85ee2"
 dependencies = [
  "bitflags 2.6.0",
  "log",
@@ -9705,18 +8246,18 @@ dependencies = [
 
 [[package]]
 name = "tauri-runtime"
-version = "2.0.1"
+version = "2.0.0-rc.7"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "af12ad1af974b274ef1d32a94e6eba27a312b429ef28fcb98abc710df7f9151d"
+checksum = "75c72b844f387bfc3341c355f3e16b8cbf4161848fa4e348670effb222cd3ba5"
 dependencies = [
  "dpi",
  "gtk",
- "http 1.1.0",
+ "http",
  "jni",
  "raw-window-handle",
  "serde",
  "serde_json",
- "tauri-utils 2.0.1",
+ "tauri-utils 2.0.0-rc.7",
  "thiserror",
  "url",
  "windows 0.58.0",
@@ -9724,23 +8265,21 @@ dependencies = [
 
 [[package]]
 name = "tauri-runtime-wry"
-version = "2.0.1"
+version = "2.0.0-rc.7"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e45e88aa0b11b302d836e6ea3e507a6359044c4a8bc86b865ba99868c695753d"
+checksum = "73accf936a7cd01d1382de7850726fdf6c1f6ab3b01ccb7a0950cb852e332596"
 dependencies = [
+ "cocoa 0.26.0",
  "gtk",
- "http 1.1.0",
+ "http",
  "jni",
  "log",
- "objc2",
- "objc2-app-kit",
- "objc2-foundation",
  "percent-encoding",
  "raw-window-handle",
  "softbuffer",
  "tao",
  "tauri-runtime",
- "tauri-utils 2.0.1",
+ "tauri-utils 2.0.0-rc.7",
  "url",
  "webkit2gtk",
  "webview2-com",
@@ -9781,9 +8320,9 @@ dependencies = [
 
 [[package]]
 name = "tauri-utils"
-version = "2.0.1"
+version = "2.0.0-rc.7"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c38b0230d6880cf6dd07b6d7dd7789a0869f98ac12146e0d18d1c1049215a045"
+checksum = "d53d9fe87e985b273696ae22ce2b9f099a8f1b44bc8fb127467bda5fcb3e4371"
 dependencies = [
  "aes-gcm",
  "brotli",
@@ -9793,7 +8332,7 @@ dependencies = [
  "getrandom 0.2.15",
  "glob",
  "html5ever",
- "infer 0.16.0",
+ "infer 0.15.0",
  "json-patch 2.0.0",
  "json5",
  "kuchikiki",
@@ -9815,7 +8354,6 @@ dependencies = [
  "toml 0.8.19",
  "url",
  "urlpattern",
- "uuid",
  "walkdir",
 ]
 
@@ -9858,9 +8396,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "04cbcdd0c794ebb0d4cf35e88edd2f7d2c4c3e9a5a6dab322839b321c6a87a64"
 dependencies = [
  "cfg-if",
- "fastrand 2.1.1",
+ "fastrand 2.1.0",
  "once_cell",
- "rustix 0.38.35",
+ "rustix 0.38.34",
  "windows-sys 0.59.0",
 ]
 
@@ -9875,15 +8413,6 @@ dependencies = [
  "utf-8",
 ]
 
-[[package]]
-name = "termcolor"
-version = "1.4.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755"
-dependencies = [
- "winapi-util",
-]
-
 [[package]]
 name = "terminal_size"
 version = "0.2.6"
@@ -9938,7 +8467,17 @@ checksum = "a4558b58466b9ad7ca0f102865eccc95938dca1a74a856f2b57b6629050da261"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.77",
+ "syn 2.0.75",
+]
+
+[[package]]
+name = "thread_local"
+version = "1.1.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c"
+dependencies = [
+ "cfg-if",
+ "once_cell",
 ]
 
 [[package]]
@@ -10037,9 +8576,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
 
 [[package]]
 name = "tokio"
-version = "1.40.0"
+version = "1.39.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e2b070231665d27ad9ec9b8df639893f46727666c6767db40317fbe920a5d998"
+checksum = "9babc99b9923bfa4804bd74722ff02c0381021eafa4db9949217e3be8e84fff5"
 dependencies = [
  "backtrace",
  "bytes",
@@ -10061,7 +8600,7 @@ checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.77",
+ "syn 2.0.75",
 ]
 
 [[package]]
@@ -10074,16 +8613,6 @@ dependencies = [
  "tokio",
 ]
 
-[[package]]
-name = "tokio-rustls"
-version = "0.24.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081"
-dependencies = [
- "rustls 0.21.12",
- "tokio",
-]
-
 [[package]]
 name = "tokio-rustls"
 version = "0.25.0"
@@ -10132,9 +8661,9 @@ dependencies = [
 
 [[package]]
 name = "tokio-util"
-version = "0.7.12"
+version = "0.7.11"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "61e7c3654c13bcd040d4a03abee2c75b1d14a37b423cf5a813ceae1cc903ec6a"
+checksum = "9cf6b47b3771c49ac75ad09a6162f53ad4b8088b76ac60e8ec1455b31a189fe1"
 dependencies = [
  "bytes",
  "futures-core",
@@ -10154,7 +8683,7 @@ dependencies = [
  "bytes",
  "futures-core",
  "futures-sink",
- "http 1.1.0",
+ "http",
  "httparse",
  "rand 0.8.5",
  "ring",
@@ -10182,7 +8711,7 @@ version = "0.8.19"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "a1ed1f98e3fdc28d6d910e6737ae6ab1a93bf1985935a1193e68f93eeb68d24e"
 dependencies = [
- "indexmap 2.5.0",
+ "indexmap 2.4.0",
  "serde",
  "serde_spanned",
  "toml_datetime",
@@ -10204,7 +8733,7 @@ version = "0.19.15"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421"
 dependencies = [
- "indexmap 2.5.0",
+ "indexmap 2.4.0",
  "serde",
  "serde_spanned",
  "toml_datetime",
@@ -10217,7 +8746,18 @@ version = "0.20.7"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "70f427fce4d84c72b5b732388bf4a9f4531b53f74e2887e3ecb2481f68f66d81"
 dependencies = [
- "indexmap 2.5.0",
+ "indexmap 2.4.0",
+ "toml_datetime",
+ "winnow 0.5.40",
+]
+
+[[package]]
+name = "toml_edit"
+version = "0.21.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6a8534fd7f78b5405e860340ad6575217ce99f38d4d5c8f2442cb5ecb50090e1"
+dependencies = [
+ "indexmap 2.4.0",
  "toml_datetime",
  "winnow 0.5.40",
 ]
@@ -10228,7 +8768,7 @@ version = "0.22.20"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "583c44c02ad26b0c3f3066fe629275e50627026c51ac2e595cca4c230ce1ce1d"
 dependencies = [
- "indexmap 2.5.0",
+ "indexmap 2.4.0",
  "serde",
  "serde_spanned",
  "toml_datetime",
@@ -10283,7 +8823,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.77",
+ "syn 2.0.75",
 ]
 
 [[package]]
@@ -10293,13 +8833,43 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54"
 dependencies = [
  "once_cell",
+ "valuable",
+]
+
+[[package]]
+name = "tracing-log"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3"
+dependencies = [
+ "log",
+ "once_cell",
+ "tracing-core",
+]
+
+[[package]]
+name = "tracing-subscriber"
+version = "0.3.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ad0f048c97dbd9faa9b7df56362b8ebcaa52adb06b498c050d2f4e32f90a7a8b"
+dependencies = [
+ "matchers",
+ "nu-ansi-term",
+ "once_cell",
+ "regex",
+ "sharded-slab",
+ "smallvec",
+ "thread_local",
+ "tracing",
+ "tracing-core",
+ "tracing-log",
 ]
 
 [[package]]
 name = "tray-icon"
-version = "0.19.0"
+version = "0.16.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "533fc2d4105e0e3d96ce1c71f2d308c9fbbe2ef9c587cab63dd627ab5bde218f"
+checksum = "131a65b2cef2081bc14dbcd414c906edbfa3bb5323dd7e748cc298614681196b"
 dependencies = [
  "core-graphics 0.24.0",
  "crossbeam-channel",
@@ -10340,13 +8910,10 @@ dependencies = [
  "byteorder",
  "bytes",
  "data-encoding",
- "http 1.1.0",
+ "http",
  "httparse",
  "log",
  "rand 0.8.5",
- "rustls 0.22.4",
- "rustls-native-certs 0.7.3",
- "rustls-pki-types",
  "sha1",
  "thiserror",
  "url",
@@ -10362,16 +8929,6 @@ dependencies = [
  "cipher",
 ]
 
-[[package]]
-name = "twox-hash"
-version = "1.6.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "97fee6b57c6a41524a810daee9286c02d7752c4253064d0b05472833a438f675"
-dependencies = [
- "cfg-if",
- "static_assertions",
-]
-
 [[package]]
 name = "typeid"
 version = "1.0.2"
@@ -10384,21 +8941,6 @@ version = "1.17.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825"
 
-[[package]]
-name = "typewit"
-version = "1.9.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c6fb9ae6a3cafaf0a5d14c2302ca525f9ae8e07a0f0e6949de88d882c37a6e24"
-dependencies = [
- "typewit_proc_macros",
-]
-
-[[package]]
-name = "typewit_proc_macros"
-version = "1.8.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e36a83ea2b3c704935a01b4642946aadd445cea40b10935e3f8bd8052b8193d6"
-
 [[package]]
 name = "ucd-trie"
 version = "0.1.6"
@@ -10425,15 +8967,6 @@ dependencies = [
  "libc",
 ]
 
-[[package]]
-name = "uncased"
-version = "0.9.10"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e1b88fcfe09e89d3866a5c11019378088af2d24c3fbd4f0543f96b479ec90697"
-dependencies = [
- "version_check",
-]
-
 [[package]]
 name = "unic-char-property"
 version = "0.9.0"
@@ -10522,9 +9055,9 @@ dependencies = [
 
 [[package]]
 name = "unicode-properties"
-version = "0.1.2"
+version = "0.1.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "52ea75f83c0137a9b98608359a5f1af8144876eb67bcb1ce837368e906a9f524"
+checksum = "e4259d9d4425d9f0661581b804cb85fe66a4c631cadd8f490d1c13a35d5d9291"
 
 [[package]]
 name = "unicode-script"
@@ -10550,12 +9083,6 @@ version = "0.1.13"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "0336d538f7abc86d282a4189614dfaa90810dfc2c6f6427eaf88e16311dd225d"
 
-[[package]]
-name = "unicode-xid"
-version = "0.2.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
-
 [[package]]
 name = "universal-hash"
 version = "0.5.1"
@@ -10566,12 +9093,6 @@ dependencies = [
  "subtle",
 ]
 
-[[package]]
-name = "unsafe-libyaml"
-version = "0.2.11"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861"
-
 [[package]]
 name = "untrusted"
 version = "0.9.0"
@@ -10592,7 +9113,7 @@ dependencies = [
  "rustls-pki-types",
  "socks",
  "url",
- "webpki-roots 0.26.5",
+ "webpki-roots",
 ]
 
 [[package]]
@@ -10607,18 +9128,13 @@ dependencies = [
  "serde",
 ]
 
-[[package]]
-name = "urlencoding"
-version = "2.1.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da"
-
 [[package]]
 name = "urlpattern"
-version = "0.3.0"
+version = "0.2.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "70acd30e3aa1450bc2eece896ce2ad0d178e9c079493819301573dae3c37ba6d"
+checksum = "f9bd5ff03aea02fa45b13a7980151fe45009af1980ba69f651ec367121a31609"
 dependencies = [
+ "derive_more",
  "regex",
  "serde",
  "unic-ucd-ident",
@@ -10677,7 +9193,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "81dfa00651efa65069b0b6b651f4aaa31ba9e3c3ce0137aaad053604ee7e0314"
 dependencies = [
  "getrandom 0.2.15",
- "serde",
  "sha1_smol",
 ]
 
@@ -10692,6 +9207,12 @@ dependencies = [
  "wasm-bindgen",
 ]
 
+[[package]]
+name = "valuable"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d"
+
 [[package]]
 name = "value-bag"
 version = "1.9.0"
@@ -10758,12 +9279,6 @@ version = "0.5.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "65dd7eed29412da847b0f78bcec0ac98588165988a8cfe41d4ea1d429f8ccfff"
 
-[[package]]
-name = "vsimd"
-version = "0.8.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5c3082ca00d5a5ef149bb8b555a72ae84c9c59f7250f013ac822ac2e49b19c64"
-
 [[package]]
 name = "vswhom"
 version = "0.1.0"
@@ -10843,7 +9358,7 @@ dependencies = [
  "once_cell",
  "proc-macro2",
  "quote",
- "syn 2.0.77",
+ "syn 2.0.75",
  "wasm-bindgen-shared",
 ]
 
@@ -10877,7 +9392,7 @@ checksum = "afc340c74d9005395cf9dd098506f7f44e38f2b4a21c6aaacf9a105ea5e1e836"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.77",
+ "syn 2.0.75",
  "wasm-bindgen-backend",
  "wasm-bindgen-shared",
 ]
@@ -10901,66 +9416,6 @@ dependencies = [
  "web-sys",
 ]
 
-[[package]]
-name = "wayland-backend"
-version = "0.3.7"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "056535ced7a150d45159d3a8dc30f91a2e2d588ca0b23f70e56033622b8016f6"
-dependencies = [
- "cc",
- "downcast-rs",
- "rustix 0.38.35",
- "scoped-tls",
- "smallvec",
- "wayland-sys",
-]
-
-[[package]]
-name = "wayland-client"
-version = "0.31.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e3f45d1222915ef1fd2057220c1d9d9624b7654443ea35c3877f7a52bd0a5a2d"
-dependencies = [
- "bitflags 2.6.0",
- "rustix 0.38.35",
- "wayland-backend",
- "wayland-scanner",
-]
-
-[[package]]
-name = "wayland-protocols"
-version = "0.32.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2b5755d77ae9040bb872a25026555ce4cb0ae75fd923e90d25fba07d81057de0"
-dependencies = [
- "bitflags 2.6.0",
- "wayland-backend",
- "wayland-client",
- "wayland-scanner",
-]
-
-[[package]]
-name = "wayland-scanner"
-version = "0.31.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "597f2001b2e5fc1121e3d5b9791d3e78f05ba6bfa4641053846248e3a13661c3"
-dependencies = [
- "proc-macro2",
- "quick-xml 0.36.2",
- "quote",
-]
-
-[[package]]
-name = "wayland-sys"
-version = "0.31.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "efa8ac0d8e8ed3e3b5c9fc92c7881406a268e11555abe36493efabe649a29e09"
-dependencies = [
- "dlib",
- "log",
- "pkg-config",
-]
-
 [[package]]
 name = "web-sys"
 version = "0.3.70"
@@ -11017,15 +9472,9 @@ dependencies = [
 
 [[package]]
 name = "webpki-roots"
-version = "0.25.4"
+version = "0.26.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5f20c57d8d7db6d3b86154206ae5d8fba62dd39573114de97c2cb0578251f8e1"
-
-[[package]]
-name = "webpki-roots"
-version = "0.26.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0bd24728e5af82c6c4ec1b66ac4844bdf8156257fccda846ec58b42cd0cdbe6a"
+checksum = "bd7c23921eeb1713a4e851530e9b9756e4fb0e89978582942612524cf09f01cd"
 dependencies = [
  "rustls-pki-types",
 ]
@@ -11052,7 +9501,7 @@ checksum = "1d228f15bba3b9d56dde8bddbee66fa24545bd17b48d5128ccf4a8742b18e431"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.77",
+ "syn 2.0.75",
 ]
 
 [[package]]
@@ -11080,7 +9529,7 @@ checksum = "b4ee928febd44d98f2f459a4a79bd4d928591333a494a10a868418ac1b39cf1f"
 dependencies = [
  "either",
  "home",
- "rustix 0.38.35",
+ "rustix 0.38.34",
  "winsafe",
 ]
 
@@ -11127,13 +9576,22 @@ version = "0.5.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "d8cdd6999298d969289d8078dae02ce798ad23452075985cccba8b6326711ecf"
 dependencies = [
- "cocoa",
+ "cocoa 0.26.0",
  "objc",
  "raw-window-handle",
  "windows-sys 0.59.0",
  "windows-version",
 ]
 
+[[package]]
+name = "windows"
+version = "0.48.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e686886bc078bc1b0b600cac0147aadb815089b6e4da64016cbd754b6342700f"
+dependencies = [
+ "windows-targets 0.48.5",
+]
+
 [[package]]
 name = "windows"
 version = "0.54.0"
@@ -11216,7 +9674,7 @@ checksum = "f6fc35f58ecd95a9b71c4f2329b911016e6bec66b3f2e6a4aad86bd2e99e2f9b"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.77",
+ "syn 2.0.75",
 ]
 
 [[package]]
@@ -11227,7 +9685,7 @@ checksum = "2bbd5b46c938e506ecbce286b6628a02171d56153ba733b6c741fc627ec9579b"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.77",
+ "syn 2.0.75",
 ]
 
 [[package]]
@@ -11238,7 +9696,7 @@ checksum = "08990546bf4edef8f431fa6326e032865f27138718c587dc21bc0265bbcb57cc"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.77",
+ "syn 2.0.75",
 ]
 
 [[package]]
@@ -11249,7 +9707,7 @@ checksum = "053c4c462dc91d3b1504c6fe5a726dd15e216ba718e84a0e46a88fbe5ded3515"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.77",
+ "syn 2.0.75",
 ]
 
 [[package]]
@@ -11543,16 +10001,6 @@ dependencies = [
  "memchr",
 ]
 
-[[package]]
-name = "winreg"
-version = "0.50.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1"
-dependencies = [
- "cfg-if",
- "windows-sys 0.48.0",
-]
-
 [[package]]
 name = "winreg"
 version = "0.52.0"
@@ -11571,13 +10019,13 @@ checksum = "d135d17ab770252ad95e9a872d365cf3090e3be864a34ab46f48555993efc904"
 
 [[package]]
 name = "wry"
-version = "0.44.1"
+version = "0.42.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "440600584cfbd8b0d28eace95c1f2c253db05dae43780b79380aa1e868f04c73"
+checksum = "49b8049c8f239cdbfaaea4bacb9646f6b208938ceec0acd5b3e99cd05f70903f"
 dependencies = [
  "base64 0.22.1",
  "block",
- "cocoa",
+ "cocoa 0.26.0",
  "core-graphics 0.24.0",
  "crossbeam-channel",
  "dpi",
@@ -11585,7 +10033,7 @@ dependencies = [
  "gdkx11",
  "gtk",
  "html5ever",
- "http 1.1.0",
+ "http",
  "javascriptcore-rs",
  "jni",
  "kuchikiki",
@@ -11645,8 +10093,8 @@ version = "0.13.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "5d91ffca73ee7f68ce055750bf9f6eca0780b8c85eff9bc046a3b0da41755e12"
 dependencies = [
- "gethostname 0.4.3",
- "rustix 0.38.35",
+ "gethostname",
+ "rustix 0.38.34",
  "x11rb-protocol",
 ]
 
@@ -11668,16 +10116,6 @@ dependencies = [
  "zeroize",
 ]
 
-[[package]]
-name = "x509"
-version = "0.2.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ca3cec94c3999f31341553f358ef55f65fc031291a022cd42ec0ce7219560c76"
-dependencies = [
- "chrono",
- "cookie-factory",
-]
-
 [[package]]
 name = "x509-certificate"
 version = "0.23.1"
@@ -11687,12 +10125,12 @@ dependencies = [
  "bcder",
  "bytes",
  "chrono",
- "der 0.7.9",
+ "der",
  "hex",
  "pem",
  "ring",
- "signature 2.2.0",
- "spki 0.7.3",
+ "signature",
+ "spki",
  "thiserror",
  "zeroize",
 ]
@@ -11705,7 +10143,7 @@ checksum = "8da84f1a25939b27f6820d92aed108f83ff920fdf11a7b19366c27c4cda81d4f"
 dependencies = [
  "libc",
  "linux-raw-sys 0.4.14",
- "rustix 0.38.35",
+ "rustix 0.38.34",
 ]
 
 [[package]]
@@ -11724,18 +10162,6 @@ dependencies = [
  "windows-sys 0.59.0",
 ]
 
-[[package]]
-name = "xml-rs"
-version = "0.8.22"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "af4e2e2f7cba5a093896c1e150fbfe177d1883e7448200efb81d40b9d339ef26"
-
-[[package]]
-name = "xmlparser"
-version = "0.13.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "66fee0b777b0f5ac1c69bb06d361268faafa61cd4682ae064a171c16c433e9e4"
-
 [[package]]
 name = "xmlwriter"
 version = "0.1.0"
@@ -11751,18 +10177,6 @@ dependencies = [
  "lzma-sys",
 ]
 
-[[package]]
-name = "yansi"
-version = "1.0.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049"
-
-[[package]]
-name = "yasna"
-version = "0.5.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e17bb3549cc1321ae1296b9cdc2698e2b6cb1992adfa19a8c72e5b7a738f44cd"
-
 [[package]]
 name = "zbus"
 version = "3.15.2"
@@ -11863,10 +10277,10 @@ version = "4.4.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "267db9407081e90bbfa46d841d3cbc60f59c0351838c4bc65199ecd79ab1983e"
 dependencies = [
- "proc-macro-crate 3.2.0",
+ "proc-macro-crate 3.1.0",
  "proc-macro2",
  "quote",
- "syn 2.0.77",
+ "syn 2.0.75",
  "zvariant_utils 2.1.0",
 ]
 
@@ -11910,7 +10324,7 @@ checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.77",
+ "syn 2.0.75",
 ]
 
 [[package]]
@@ -11930,49 +10344,26 @@ checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.77",
+ "syn 2.0.75",
 ]
 
 [[package]]
 name = "zip"
-version = "0.6.6"
+version = "2.1.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "760394e246e4c28189f19d488c058bf16f564016aefac5d32bb1f3b51d5e9261"
-dependencies = [
- "byteorder",
- "crc32fast",
- "crossbeam-utils",
- "flate2",
-]
-
-[[package]]
-name = "zip"
-version = "2.2.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "dc5e4288ea4057ae23afc69a4472434a87a2495cafce6632fd1c4ec9f5cf3494"
+checksum = "40dd8c92efc296286ce1fbd16657c5dbefff44f1b4ca01cc5f517d8b7b3d3e2e"
 dependencies = [
  "arbitrary",
  "crc32fast",
  "crossbeam-utils",
  "displaydoc",
  "flate2",
- "indexmap 2.5.0",
+ "indexmap 2.4.0",
  "memchr",
  "thiserror",
  "zopfli",
 ]
 
-[[package]]
-name = "zip_structs"
-version = "0.2.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ce824a6bfffe8942820fa36d24973b7c83a40896749a42e33de0abdd11750ee5"
-dependencies = [
- "byteorder",
- "bytesize",
- "thiserror",
-]
-
 [[package]]
 name = "zopfli"
 version = "0.8.1"
@@ -12087,10 +10478,10 @@ version = "4.2.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "73e2ba546bda683a90652bac4a279bc146adad1386f25379cf73200d2002c449"
 dependencies = [
- "proc-macro-crate 3.2.0",
+ "proc-macro-crate 3.1.0",
  "proc-macro2",
  "quote",
- "syn 2.0.77",
+ "syn 2.0.75",
  "zvariant_utils 2.1.0",
 ]
 
@@ -12113,5 +10504,5 @@ checksum = "c51bcff7cc3dbb5055396bcf774748c3dab426b4b8659046963523cee4808340"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.77",
+ "syn 2.0.75",
 ]
diff --git a/desktop/tauri/src-tauri/Cargo.toml b/desktop/tauri/src-tauri/Cargo.toml
index 78f87926..db19639b 100644
--- a/desktop/tauri/src-tauri/Cargo.toml
+++ b/desktop/tauri/src-tauri/Cargo.toml
@@ -1,33 +1,33 @@
 [package]
-name = "portmaster"
+name = "app"
 version = "0.1.0"
 description = "Portmaster UI"
 authors = ["Safing"]
 license = ""
 repository = ""
-default-run = "portmaster"
+default-run = "app"
 edition = "2021"
-rust-version = "1.64"
+rust-version = "1.60"
 
 # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
 
 [build-dependencies]
-tauri-build = { version = "2.0.1", features = [] }
+tauri-build = { version = "2.0.0-rc.7", features = [] }
 
 [dependencies]
 # Tauri
-tauri = { version = "2.0.1", features = ["tray-icon", "image-png", "config-json5", "devtools"] }
-tauri-plugin-shell = "2.0.1"
-tauri-plugin-dialog = "2.0.1"
-tauri-plugin-clipboard-manager = "2.0.1"
-tauri-plugin-os = "2.0.1"
-tauri-plugin-single-instance = "2.0.1"
-tauri-plugin-notification = "2.0.1"
-tauri-plugin-log = "2.0.1"
-tauri-plugin-window-state = "2.0.1"
+tauri = { version = "2.0.0-rc.8", features = ["tray-icon", "image-png", "config-json5", "devtools"] }
+tauri-plugin-shell = "2.0.0-rc"
+tauri-plugin-dialog = "2.0.0-rc"
+tauri-plugin-clipboard-manager = "2.0.0-rc"
+tauri-plugin-os = "2.0.0-rc"
+tauri-plugin-single-instance = "2.0.0-rc"
+tauri-plugin-notification = "2.0.0-rc"
+tauri-plugin-log = "2.0.0-rc"
+tauri-plugin-window-state = "2.0.0-rc"
 
-tauri-cli = "2.0.1"
-clap_lex = "0.7.2"
+tauri-cli = "2.0.0-rc.8"
+clap = { version = "4" }
 
 # General
 serde_json = "1.0"
@@ -80,6 +80,3 @@ ctor = "0.2.6"
 # If you use cargo directly instead of tauri's cli you can use this feature flag to switch between tauri's `dev` and `build` modes.
 # DO NOT REMOVE!!
 custom-protocol = [ "tauri/custom-protocol" ]
-
-[package.metadata.clippy]
-allow = ["clippy::collapsible_else_if"]
\ No newline at end of file
diff --git a/desktop/tauri/src-tauri/README.md b/desktop/tauri/src-tauri/README.md
deleted file mode 100644
index a057e285..00000000
--- a/desktop/tauri/src-tauri/README.md
+++ /dev/null
@@ -1,41 +0,0 @@
-# Update Tauri guide
-
-Check latest versions of tauri packages and update them accordingly:
-```toml
-[build-dependencies]
-tauri-build = { version = "2.0.0-beta.19", features = [] } # Update to latest
-
-[dependencies]
-# Tauri
-tauri = { version = "2.0.0-beta.24", features = ["tray-icon", "image-png", "config-json5", "devtools"] } # Update to latest
-tauri-plugin-shell = "2.0.0-beta"
-tauri-plugin-dialog = "2.0.0-beta"
-tauri-plugin-clipboard-manager = "2.0.0-beta"
-tauri-plugin-os = "2.0.0-beta"
-tauri-plugin-single-instance = "2.0.0-beta"
-tauri-plugin-cli = "2.0.0-beta"
-tauri-plugin-notification = "2.0.0-beta"
-tauri-plugin-log = "2.0.0-beta"
-tauri-plugin-window-state = "2.0.0-beta"
-
-tauri-cli = "2.0.0-beta.21" # Update to latest
-```
-
-> The plugins will be auto updated based on tauri version.
-
-Run:
-```sh
-cargo update
-```
-
-Update WIX installer template:
-1. Get the latests [main.wxs](https://github.com/tauri-apps/tauri/blob/dev/tooling/bundler/src/bundle/windows/templates/main.wxs) template from the repository.
-2. Replace the contents of `templates/main_original.wxs` with the repository version.
-3. Replace the contents of `templates/main.wsx` and add the fallowing lines at the end of the file, inside the `Product` tag. 
-```xml
-    <!-- Service fragments -->
-    <CustomActionRef Id='InstallPortmasterService' />
-    <CustomActionRef Id='StopPortmasterService' />
-    <CustomActionRef Id='DeletePortmasterService' />
-    <!-- End Service fragments -->
-```
diff --git a/desktop/tauri/src-tauri/gen/schemas/acl-manifests.json b/desktop/tauri/src-tauri/gen/schemas/acl-manifests.json
index 1d974eb0..233ccc01 100644
--- a/desktop/tauri/src-tauri/gen/schemas/acl-manifests.json
+++ b/desktop/tauri/src-tauri/gen/schemas/acl-manifests.json
@@ -1 +1 @@
-{"clipboard-manager":{"default_permission":{"identifier":"default","description":"No features are enabled by default, as we believe\nthe clipboard can be inherently dangerous and it is \napplication specific if read and/or write access is needed.\n\nClipboard interaction needs to be explicitly enabled.\n","permissions":[]},"permissions":{"allow-clear":{"identifier":"allow-clear","description":"Enables the clear command without any pre-configured scope.","commands":{"allow":["clear"],"deny":[]}},"allow-read-image":{"identifier":"allow-read-image","description":"Enables the read_image command without any pre-configured scope.","commands":{"allow":["read_image"],"deny":[]}},"allow-read-text":{"identifier":"allow-read-text","description":"Enables the read_text command without any pre-configured scope.","commands":{"allow":["read_text"],"deny":[]}},"allow-write-html":{"identifier":"allow-write-html","description":"Enables the write_html command without any pre-configured scope.","commands":{"allow":["write_html"],"deny":[]}},"allow-write-image":{"identifier":"allow-write-image","description":"Enables the write_image command without any pre-configured scope.","commands":{"allow":["write_image"],"deny":[]}},"allow-write-text":{"identifier":"allow-write-text","description":"Enables the write_text command without any pre-configured scope.","commands":{"allow":["write_text"],"deny":[]}},"deny-clear":{"identifier":"deny-clear","description":"Denies the clear command without any pre-configured scope.","commands":{"allow":[],"deny":["clear"]}},"deny-read-image":{"identifier":"deny-read-image","description":"Denies the read_image command without any pre-configured scope.","commands":{"allow":[],"deny":["read_image"]}},"deny-read-text":{"identifier":"deny-read-text","description":"Denies the read_text command without any pre-configured scope.","commands":{"allow":[],"deny":["read_text"]}},"deny-write-html":{"identifier":"deny-write-html","description":"Denies the write_html command without any pre-configured scope.","commands":{"allow":[],"deny":["write_html"]}},"deny-write-image":{"identifier":"deny-write-image","description":"Denies the write_image command without any pre-configured scope.","commands":{"allow":[],"deny":["write_image"]}},"deny-write-text":{"identifier":"deny-write-text","description":"Denies the write_text command without any pre-configured scope.","commands":{"allow":[],"deny":["write_text"]}}},"permission_sets":{},"global_scope_schema":null},"core":{"default_permission":{"identifier":"default","description":"Default core plugins set which includes:\n- 'core:path:default'\n- 'core:event:default'\n- 'core:window:default'\n- 'core:webview:default'\n- 'core:app:default'\n- 'core:image:default'\n- 'core:resources:default'\n- 'core:menu:default'\n- 'core:tray:default'\n","permissions":["core:path:default","core:event:default","core:window:default","core:webview:default","core:app:default","core:image:default","core:resources:default","core:menu:default","core:tray:default"]},"permissions":{},"permission_sets":{},"global_scope_schema":null},"core:app":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin.","permissions":["allow-version","allow-name","allow-tauri-version"]},"permissions":{"allow-app-hide":{"identifier":"allow-app-hide","description":"Enables the app_hide command without any pre-configured scope.","commands":{"allow":["app_hide"],"deny":[]}},"allow-app-show":{"identifier":"allow-app-show","description":"Enables the app_show command without any pre-configured scope.","commands":{"allow":["app_show"],"deny":[]}},"allow-default-window-icon":{"identifier":"allow-default-window-icon","description":"Enables the default_window_icon command without any pre-configured scope.","commands":{"allow":["default_window_icon"],"deny":[]}},"allow-name":{"identifier":"allow-name","description":"Enables the name command without any pre-configured scope.","commands":{"allow":["name"],"deny":[]}},"allow-set-app-theme":{"identifier":"allow-set-app-theme","description":"Enables the set_app_theme command without any pre-configured scope.","commands":{"allow":["set_app_theme"],"deny":[]}},"allow-tauri-version":{"identifier":"allow-tauri-version","description":"Enables the tauri_version command without any pre-configured scope.","commands":{"allow":["tauri_version"],"deny":[]}},"allow-version":{"identifier":"allow-version","description":"Enables the version command without any pre-configured scope.","commands":{"allow":["version"],"deny":[]}},"deny-app-hide":{"identifier":"deny-app-hide","description":"Denies the app_hide command without any pre-configured scope.","commands":{"allow":[],"deny":["app_hide"]}},"deny-app-show":{"identifier":"deny-app-show","description":"Denies the app_show command without any pre-configured scope.","commands":{"allow":[],"deny":["app_show"]}},"deny-default-window-icon":{"identifier":"deny-default-window-icon","description":"Denies the default_window_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["default_window_icon"]}},"deny-name":{"identifier":"deny-name","description":"Denies the name command without any pre-configured scope.","commands":{"allow":[],"deny":["name"]}},"deny-set-app-theme":{"identifier":"deny-set-app-theme","description":"Denies the set_app_theme command without any pre-configured scope.","commands":{"allow":[],"deny":["set_app_theme"]}},"deny-tauri-version":{"identifier":"deny-tauri-version","description":"Denies the tauri_version command without any pre-configured scope.","commands":{"allow":[],"deny":["tauri_version"]}},"deny-version":{"identifier":"deny-version","description":"Denies the version command without any pre-configured scope.","commands":{"allow":[],"deny":["version"]}}},"permission_sets":{},"global_scope_schema":null},"core:event":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin.","permissions":["allow-listen","allow-unlisten","allow-emit","allow-emit-to"]},"permissions":{"allow-emit":{"identifier":"allow-emit","description":"Enables the emit command without any pre-configured scope.","commands":{"allow":["emit"],"deny":[]}},"allow-emit-to":{"identifier":"allow-emit-to","description":"Enables the emit_to command without any pre-configured scope.","commands":{"allow":["emit_to"],"deny":[]}},"allow-listen":{"identifier":"allow-listen","description":"Enables the listen command without any pre-configured scope.","commands":{"allow":["listen"],"deny":[]}},"allow-unlisten":{"identifier":"allow-unlisten","description":"Enables the unlisten command without any pre-configured scope.","commands":{"allow":["unlisten"],"deny":[]}},"deny-emit":{"identifier":"deny-emit","description":"Denies the emit command without any pre-configured scope.","commands":{"allow":[],"deny":["emit"]}},"deny-emit-to":{"identifier":"deny-emit-to","description":"Denies the emit_to command without any pre-configured scope.","commands":{"allow":[],"deny":["emit_to"]}},"deny-listen":{"identifier":"deny-listen","description":"Denies the listen command without any pre-configured scope.","commands":{"allow":[],"deny":["listen"]}},"deny-unlisten":{"identifier":"deny-unlisten","description":"Denies the unlisten command without any pre-configured scope.","commands":{"allow":[],"deny":["unlisten"]}}},"permission_sets":{},"global_scope_schema":null},"core:image":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin.","permissions":["allow-new","allow-from-bytes","allow-from-path","allow-rgba","allow-size"]},"permissions":{"allow-from-bytes":{"identifier":"allow-from-bytes","description":"Enables the from_bytes command without any pre-configured scope.","commands":{"allow":["from_bytes"],"deny":[]}},"allow-from-path":{"identifier":"allow-from-path","description":"Enables the from_path command without any pre-configured scope.","commands":{"allow":["from_path"],"deny":[]}},"allow-new":{"identifier":"allow-new","description":"Enables the new command without any pre-configured scope.","commands":{"allow":["new"],"deny":[]}},"allow-rgba":{"identifier":"allow-rgba","description":"Enables the rgba command without any pre-configured scope.","commands":{"allow":["rgba"],"deny":[]}},"allow-size":{"identifier":"allow-size","description":"Enables the size command without any pre-configured scope.","commands":{"allow":["size"],"deny":[]}},"deny-from-bytes":{"identifier":"deny-from-bytes","description":"Denies the from_bytes command without any pre-configured scope.","commands":{"allow":[],"deny":["from_bytes"]}},"deny-from-path":{"identifier":"deny-from-path","description":"Denies the from_path command without any pre-configured scope.","commands":{"allow":[],"deny":["from_path"]}},"deny-new":{"identifier":"deny-new","description":"Denies the new command without any pre-configured scope.","commands":{"allow":[],"deny":["new"]}},"deny-rgba":{"identifier":"deny-rgba","description":"Denies the rgba command without any pre-configured scope.","commands":{"allow":[],"deny":["rgba"]}},"deny-size":{"identifier":"deny-size","description":"Denies the size command without any pre-configured scope.","commands":{"allow":[],"deny":["size"]}}},"permission_sets":{},"global_scope_schema":null},"core:menu":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin.","permissions":["allow-new","allow-append","allow-prepend","allow-insert","allow-remove","allow-remove-at","allow-items","allow-get","allow-popup","allow-create-default","allow-set-as-app-menu","allow-set-as-window-menu","allow-text","allow-set-text","allow-is-enabled","allow-set-enabled","allow-set-accelerator","allow-set-as-windows-menu-for-nsapp","allow-set-as-help-menu-for-nsapp","allow-is-checked","allow-set-checked","allow-set-icon"]},"permissions":{"allow-append":{"identifier":"allow-append","description":"Enables the append command without any pre-configured scope.","commands":{"allow":["append"],"deny":[]}},"allow-create-default":{"identifier":"allow-create-default","description":"Enables the create_default command without any pre-configured scope.","commands":{"allow":["create_default"],"deny":[]}},"allow-get":{"identifier":"allow-get","description":"Enables the get command without any pre-configured scope.","commands":{"allow":["get"],"deny":[]}},"allow-insert":{"identifier":"allow-insert","description":"Enables the insert command without any pre-configured scope.","commands":{"allow":["insert"],"deny":[]}},"allow-is-checked":{"identifier":"allow-is-checked","description":"Enables the is_checked command without any pre-configured scope.","commands":{"allow":["is_checked"],"deny":[]}},"allow-is-enabled":{"identifier":"allow-is-enabled","description":"Enables the is_enabled command without any pre-configured scope.","commands":{"allow":["is_enabled"],"deny":[]}},"allow-items":{"identifier":"allow-items","description":"Enables the items command without any pre-configured scope.","commands":{"allow":["items"],"deny":[]}},"allow-new":{"identifier":"allow-new","description":"Enables the new command without any pre-configured scope.","commands":{"allow":["new"],"deny":[]}},"allow-popup":{"identifier":"allow-popup","description":"Enables the popup command without any pre-configured scope.","commands":{"allow":["popup"],"deny":[]}},"allow-prepend":{"identifier":"allow-prepend","description":"Enables the prepend command without any pre-configured scope.","commands":{"allow":["prepend"],"deny":[]}},"allow-remove":{"identifier":"allow-remove","description":"Enables the remove command without any pre-configured scope.","commands":{"allow":["remove"],"deny":[]}},"allow-remove-at":{"identifier":"allow-remove-at","description":"Enables the remove_at command without any pre-configured scope.","commands":{"allow":["remove_at"],"deny":[]}},"allow-set-accelerator":{"identifier":"allow-set-accelerator","description":"Enables the set_accelerator command without any pre-configured scope.","commands":{"allow":["set_accelerator"],"deny":[]}},"allow-set-as-app-menu":{"identifier":"allow-set-as-app-menu","description":"Enables the set_as_app_menu command without any pre-configured scope.","commands":{"allow":["set_as_app_menu"],"deny":[]}},"allow-set-as-help-menu-for-nsapp":{"identifier":"allow-set-as-help-menu-for-nsapp","description":"Enables the set_as_help_menu_for_nsapp command without any pre-configured scope.","commands":{"allow":["set_as_help_menu_for_nsapp"],"deny":[]}},"allow-set-as-window-menu":{"identifier":"allow-set-as-window-menu","description":"Enables the set_as_window_menu command without any pre-configured scope.","commands":{"allow":["set_as_window_menu"],"deny":[]}},"allow-set-as-windows-menu-for-nsapp":{"identifier":"allow-set-as-windows-menu-for-nsapp","description":"Enables the set_as_windows_menu_for_nsapp command without any pre-configured scope.","commands":{"allow":["set_as_windows_menu_for_nsapp"],"deny":[]}},"allow-set-checked":{"identifier":"allow-set-checked","description":"Enables the set_checked command without any pre-configured scope.","commands":{"allow":["set_checked"],"deny":[]}},"allow-set-enabled":{"identifier":"allow-set-enabled","description":"Enables the set_enabled command without any pre-configured scope.","commands":{"allow":["set_enabled"],"deny":[]}},"allow-set-icon":{"identifier":"allow-set-icon","description":"Enables the set_icon command without any pre-configured scope.","commands":{"allow":["set_icon"],"deny":[]}},"allow-set-text":{"identifier":"allow-set-text","description":"Enables the set_text command without any pre-configured scope.","commands":{"allow":["set_text"],"deny":[]}},"allow-text":{"identifier":"allow-text","description":"Enables the text command without any pre-configured scope.","commands":{"allow":["text"],"deny":[]}},"deny-append":{"identifier":"deny-append","description":"Denies the append command without any pre-configured scope.","commands":{"allow":[],"deny":["append"]}},"deny-create-default":{"identifier":"deny-create-default","description":"Denies the create_default command without any pre-configured scope.","commands":{"allow":[],"deny":["create_default"]}},"deny-get":{"identifier":"deny-get","description":"Denies the get command without any pre-configured scope.","commands":{"allow":[],"deny":["get"]}},"deny-insert":{"identifier":"deny-insert","description":"Denies the insert command without any pre-configured scope.","commands":{"allow":[],"deny":["insert"]}},"deny-is-checked":{"identifier":"deny-is-checked","description":"Denies the is_checked command without any pre-configured scope.","commands":{"allow":[],"deny":["is_checked"]}},"deny-is-enabled":{"identifier":"deny-is-enabled","description":"Denies the is_enabled command without any pre-configured scope.","commands":{"allow":[],"deny":["is_enabled"]}},"deny-items":{"identifier":"deny-items","description":"Denies the items command without any pre-configured scope.","commands":{"allow":[],"deny":["items"]}},"deny-new":{"identifier":"deny-new","description":"Denies the new command without any pre-configured scope.","commands":{"allow":[],"deny":["new"]}},"deny-popup":{"identifier":"deny-popup","description":"Denies the popup command without any pre-configured scope.","commands":{"allow":[],"deny":["popup"]}},"deny-prepend":{"identifier":"deny-prepend","description":"Denies the prepend command without any pre-configured scope.","commands":{"allow":[],"deny":["prepend"]}},"deny-remove":{"identifier":"deny-remove","description":"Denies the remove command without any pre-configured scope.","commands":{"allow":[],"deny":["remove"]}},"deny-remove-at":{"identifier":"deny-remove-at","description":"Denies the remove_at command without any pre-configured scope.","commands":{"allow":[],"deny":["remove_at"]}},"deny-set-accelerator":{"identifier":"deny-set-accelerator","description":"Denies the set_accelerator command without any pre-configured scope.","commands":{"allow":[],"deny":["set_accelerator"]}},"deny-set-as-app-menu":{"identifier":"deny-set-as-app-menu","description":"Denies the set_as_app_menu command without any pre-configured scope.","commands":{"allow":[],"deny":["set_as_app_menu"]}},"deny-set-as-help-menu-for-nsapp":{"identifier":"deny-set-as-help-menu-for-nsapp","description":"Denies the set_as_help_menu_for_nsapp command without any pre-configured scope.","commands":{"allow":[],"deny":["set_as_help_menu_for_nsapp"]}},"deny-set-as-window-menu":{"identifier":"deny-set-as-window-menu","description":"Denies the set_as_window_menu command without any pre-configured scope.","commands":{"allow":[],"deny":["set_as_window_menu"]}},"deny-set-as-windows-menu-for-nsapp":{"identifier":"deny-set-as-windows-menu-for-nsapp","description":"Denies the set_as_windows_menu_for_nsapp command without any pre-configured scope.","commands":{"allow":[],"deny":["set_as_windows_menu_for_nsapp"]}},"deny-set-checked":{"identifier":"deny-set-checked","description":"Denies the set_checked command without any pre-configured scope.","commands":{"allow":[],"deny":["set_checked"]}},"deny-set-enabled":{"identifier":"deny-set-enabled","description":"Denies the set_enabled command without any pre-configured scope.","commands":{"allow":[],"deny":["set_enabled"]}},"deny-set-icon":{"identifier":"deny-set-icon","description":"Denies the set_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["set_icon"]}},"deny-set-text":{"identifier":"deny-set-text","description":"Denies the set_text command without any pre-configured scope.","commands":{"allow":[],"deny":["set_text"]}},"deny-text":{"identifier":"deny-text","description":"Denies the text command without any pre-configured scope.","commands":{"allow":[],"deny":["text"]}}},"permission_sets":{},"global_scope_schema":null},"core:path":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin.","permissions":["allow-resolve-directory","allow-resolve","allow-normalize","allow-join","allow-dirname","allow-extname","allow-basename","allow-is-absolute"]},"permissions":{"allow-basename":{"identifier":"allow-basename","description":"Enables the basename command without any pre-configured scope.","commands":{"allow":["basename"],"deny":[]}},"allow-dirname":{"identifier":"allow-dirname","description":"Enables the dirname command without any pre-configured scope.","commands":{"allow":["dirname"],"deny":[]}},"allow-extname":{"identifier":"allow-extname","description":"Enables the extname command without any pre-configured scope.","commands":{"allow":["extname"],"deny":[]}},"allow-is-absolute":{"identifier":"allow-is-absolute","description":"Enables the is_absolute command without any pre-configured scope.","commands":{"allow":["is_absolute"],"deny":[]}},"allow-join":{"identifier":"allow-join","description":"Enables the join command without any pre-configured scope.","commands":{"allow":["join"],"deny":[]}},"allow-normalize":{"identifier":"allow-normalize","description":"Enables the normalize command without any pre-configured scope.","commands":{"allow":["normalize"],"deny":[]}},"allow-resolve":{"identifier":"allow-resolve","description":"Enables the resolve command without any pre-configured scope.","commands":{"allow":["resolve"],"deny":[]}},"allow-resolve-directory":{"identifier":"allow-resolve-directory","description":"Enables the resolve_directory command without any pre-configured scope.","commands":{"allow":["resolve_directory"],"deny":[]}},"deny-basename":{"identifier":"deny-basename","description":"Denies the basename command without any pre-configured scope.","commands":{"allow":[],"deny":["basename"]}},"deny-dirname":{"identifier":"deny-dirname","description":"Denies the dirname command without any pre-configured scope.","commands":{"allow":[],"deny":["dirname"]}},"deny-extname":{"identifier":"deny-extname","description":"Denies the extname command without any pre-configured scope.","commands":{"allow":[],"deny":["extname"]}},"deny-is-absolute":{"identifier":"deny-is-absolute","description":"Denies the is_absolute command without any pre-configured scope.","commands":{"allow":[],"deny":["is_absolute"]}},"deny-join":{"identifier":"deny-join","description":"Denies the join command without any pre-configured scope.","commands":{"allow":[],"deny":["join"]}},"deny-normalize":{"identifier":"deny-normalize","description":"Denies the normalize command without any pre-configured scope.","commands":{"allow":[],"deny":["normalize"]}},"deny-resolve":{"identifier":"deny-resolve","description":"Denies the resolve command without any pre-configured scope.","commands":{"allow":[],"deny":["resolve"]}},"deny-resolve-directory":{"identifier":"deny-resolve-directory","description":"Denies the resolve_directory command without any pre-configured scope.","commands":{"allow":[],"deny":["resolve_directory"]}}},"permission_sets":{},"global_scope_schema":null},"core:resources":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin.","permissions":["allow-close"]},"permissions":{"allow-close":{"identifier":"allow-close","description":"Enables the close command without any pre-configured scope.","commands":{"allow":["close"],"deny":[]}},"deny-close":{"identifier":"deny-close","description":"Denies the close command without any pre-configured scope.","commands":{"allow":[],"deny":["close"]}}},"permission_sets":{},"global_scope_schema":null},"core:tray":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin.","permissions":["allow-new","allow-get-by-id","allow-remove-by-id","allow-set-icon","allow-set-menu","allow-set-tooltip","allow-set-title","allow-set-visible","allow-set-temp-dir-path","allow-set-icon-as-template","allow-set-show-menu-on-left-click"]},"permissions":{"allow-get-by-id":{"identifier":"allow-get-by-id","description":"Enables the get_by_id command without any pre-configured scope.","commands":{"allow":["get_by_id"],"deny":[]}},"allow-new":{"identifier":"allow-new","description":"Enables the new command without any pre-configured scope.","commands":{"allow":["new"],"deny":[]}},"allow-remove-by-id":{"identifier":"allow-remove-by-id","description":"Enables the remove_by_id command without any pre-configured scope.","commands":{"allow":["remove_by_id"],"deny":[]}},"allow-set-icon":{"identifier":"allow-set-icon","description":"Enables the set_icon command without any pre-configured scope.","commands":{"allow":["set_icon"],"deny":[]}},"allow-set-icon-as-template":{"identifier":"allow-set-icon-as-template","description":"Enables the set_icon_as_template command without any pre-configured scope.","commands":{"allow":["set_icon_as_template"],"deny":[]}},"allow-set-menu":{"identifier":"allow-set-menu","description":"Enables the set_menu command without any pre-configured scope.","commands":{"allow":["set_menu"],"deny":[]}},"allow-set-show-menu-on-left-click":{"identifier":"allow-set-show-menu-on-left-click","description":"Enables the set_show_menu_on_left_click command without any pre-configured scope.","commands":{"allow":["set_show_menu_on_left_click"],"deny":[]}},"allow-set-temp-dir-path":{"identifier":"allow-set-temp-dir-path","description":"Enables the set_temp_dir_path command without any pre-configured scope.","commands":{"allow":["set_temp_dir_path"],"deny":[]}},"allow-set-title":{"identifier":"allow-set-title","description":"Enables the set_title command without any pre-configured scope.","commands":{"allow":["set_title"],"deny":[]}},"allow-set-tooltip":{"identifier":"allow-set-tooltip","description":"Enables the set_tooltip command without any pre-configured scope.","commands":{"allow":["set_tooltip"],"deny":[]}},"allow-set-visible":{"identifier":"allow-set-visible","description":"Enables the set_visible command without any pre-configured scope.","commands":{"allow":["set_visible"],"deny":[]}},"deny-get-by-id":{"identifier":"deny-get-by-id","description":"Denies the get_by_id command without any pre-configured scope.","commands":{"allow":[],"deny":["get_by_id"]}},"deny-new":{"identifier":"deny-new","description":"Denies the new command without any pre-configured scope.","commands":{"allow":[],"deny":["new"]}},"deny-remove-by-id":{"identifier":"deny-remove-by-id","description":"Denies the remove_by_id command without any pre-configured scope.","commands":{"allow":[],"deny":["remove_by_id"]}},"deny-set-icon":{"identifier":"deny-set-icon","description":"Denies the set_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["set_icon"]}},"deny-set-icon-as-template":{"identifier":"deny-set-icon-as-template","description":"Denies the set_icon_as_template command without any pre-configured scope.","commands":{"allow":[],"deny":["set_icon_as_template"]}},"deny-set-menu":{"identifier":"deny-set-menu","description":"Denies the set_menu command without any pre-configured scope.","commands":{"allow":[],"deny":["set_menu"]}},"deny-set-show-menu-on-left-click":{"identifier":"deny-set-show-menu-on-left-click","description":"Denies the set_show_menu_on_left_click command without any pre-configured scope.","commands":{"allow":[],"deny":["set_show_menu_on_left_click"]}},"deny-set-temp-dir-path":{"identifier":"deny-set-temp-dir-path","description":"Denies the set_temp_dir_path command without any pre-configured scope.","commands":{"allow":[],"deny":["set_temp_dir_path"]}},"deny-set-title":{"identifier":"deny-set-title","description":"Denies the set_title command without any pre-configured scope.","commands":{"allow":[],"deny":["set_title"]}},"deny-set-tooltip":{"identifier":"deny-set-tooltip","description":"Denies the set_tooltip command without any pre-configured scope.","commands":{"allow":[],"deny":["set_tooltip"]}},"deny-set-visible":{"identifier":"deny-set-visible","description":"Denies the set_visible command without any pre-configured scope.","commands":{"allow":[],"deny":["set_visible"]}}},"permission_sets":{},"global_scope_schema":null},"core:webview":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin.","permissions":["allow-get-all-webviews","allow-webview-position","allow-webview-size","allow-internal-toggle-devtools"]},"permissions":{"allow-clear-all-browsing-data":{"identifier":"allow-clear-all-browsing-data","description":"Enables the clear_all_browsing_data command without any pre-configured scope.","commands":{"allow":["clear_all_browsing_data"],"deny":[]}},"allow-create-webview":{"identifier":"allow-create-webview","description":"Enables the create_webview command without any pre-configured scope.","commands":{"allow":["create_webview"],"deny":[]}},"allow-create-webview-window":{"identifier":"allow-create-webview-window","description":"Enables the create_webview_window command without any pre-configured scope.","commands":{"allow":["create_webview_window"],"deny":[]}},"allow-get-all-webviews":{"identifier":"allow-get-all-webviews","description":"Enables the get_all_webviews command without any pre-configured scope.","commands":{"allow":["get_all_webviews"],"deny":[]}},"allow-internal-toggle-devtools":{"identifier":"allow-internal-toggle-devtools","description":"Enables the internal_toggle_devtools command without any pre-configured scope.","commands":{"allow":["internal_toggle_devtools"],"deny":[]}},"allow-print":{"identifier":"allow-print","description":"Enables the print command without any pre-configured scope.","commands":{"allow":["print"],"deny":[]}},"allow-reparent":{"identifier":"allow-reparent","description":"Enables the reparent command without any pre-configured scope.","commands":{"allow":["reparent"],"deny":[]}},"allow-set-webview-focus":{"identifier":"allow-set-webview-focus","description":"Enables the set_webview_focus command without any pre-configured scope.","commands":{"allow":["set_webview_focus"],"deny":[]}},"allow-set-webview-position":{"identifier":"allow-set-webview-position","description":"Enables the set_webview_position command without any pre-configured scope.","commands":{"allow":["set_webview_position"],"deny":[]}},"allow-set-webview-size":{"identifier":"allow-set-webview-size","description":"Enables the set_webview_size command without any pre-configured scope.","commands":{"allow":["set_webview_size"],"deny":[]}},"allow-set-webview-zoom":{"identifier":"allow-set-webview-zoom","description":"Enables the set_webview_zoom command without any pre-configured scope.","commands":{"allow":["set_webview_zoom"],"deny":[]}},"allow-webview-close":{"identifier":"allow-webview-close","description":"Enables the webview_close command without any pre-configured scope.","commands":{"allow":["webview_close"],"deny":[]}},"allow-webview-hide":{"identifier":"allow-webview-hide","description":"Enables the webview_hide command without any pre-configured scope.","commands":{"allow":["webview_hide"],"deny":[]}},"allow-webview-position":{"identifier":"allow-webview-position","description":"Enables the webview_position command without any pre-configured scope.","commands":{"allow":["webview_position"],"deny":[]}},"allow-webview-show":{"identifier":"allow-webview-show","description":"Enables the webview_show command without any pre-configured scope.","commands":{"allow":["webview_show"],"deny":[]}},"allow-webview-size":{"identifier":"allow-webview-size","description":"Enables the webview_size command without any pre-configured scope.","commands":{"allow":["webview_size"],"deny":[]}},"deny-clear-all-browsing-data":{"identifier":"deny-clear-all-browsing-data","description":"Denies the clear_all_browsing_data command without any pre-configured scope.","commands":{"allow":[],"deny":["clear_all_browsing_data"]}},"deny-create-webview":{"identifier":"deny-create-webview","description":"Denies the create_webview command without any pre-configured scope.","commands":{"allow":[],"deny":["create_webview"]}},"deny-create-webview-window":{"identifier":"deny-create-webview-window","description":"Denies the create_webview_window command without any pre-configured scope.","commands":{"allow":[],"deny":["create_webview_window"]}},"deny-get-all-webviews":{"identifier":"deny-get-all-webviews","description":"Denies the get_all_webviews command without any pre-configured scope.","commands":{"allow":[],"deny":["get_all_webviews"]}},"deny-internal-toggle-devtools":{"identifier":"deny-internal-toggle-devtools","description":"Denies the internal_toggle_devtools command without any pre-configured scope.","commands":{"allow":[],"deny":["internal_toggle_devtools"]}},"deny-print":{"identifier":"deny-print","description":"Denies the print command without any pre-configured scope.","commands":{"allow":[],"deny":["print"]}},"deny-reparent":{"identifier":"deny-reparent","description":"Denies the reparent command without any pre-configured scope.","commands":{"allow":[],"deny":["reparent"]}},"deny-set-webview-focus":{"identifier":"deny-set-webview-focus","description":"Denies the set_webview_focus command without any pre-configured scope.","commands":{"allow":[],"deny":["set_webview_focus"]}},"deny-set-webview-position":{"identifier":"deny-set-webview-position","description":"Denies the set_webview_position command without any pre-configured scope.","commands":{"allow":[],"deny":["set_webview_position"]}},"deny-set-webview-size":{"identifier":"deny-set-webview-size","description":"Denies the set_webview_size command without any pre-configured scope.","commands":{"allow":[],"deny":["set_webview_size"]}},"deny-set-webview-zoom":{"identifier":"deny-set-webview-zoom","description":"Denies the set_webview_zoom command without any pre-configured scope.","commands":{"allow":[],"deny":["set_webview_zoom"]}},"deny-webview-close":{"identifier":"deny-webview-close","description":"Denies the webview_close command without any pre-configured scope.","commands":{"allow":[],"deny":["webview_close"]}},"deny-webview-hide":{"identifier":"deny-webview-hide","description":"Denies the webview_hide command without any pre-configured scope.","commands":{"allow":[],"deny":["webview_hide"]}},"deny-webview-position":{"identifier":"deny-webview-position","description":"Denies the webview_position command without any pre-configured scope.","commands":{"allow":[],"deny":["webview_position"]}},"deny-webview-show":{"identifier":"deny-webview-show","description":"Denies the webview_show command without any pre-configured scope.","commands":{"allow":[],"deny":["webview_show"]}},"deny-webview-size":{"identifier":"deny-webview-size","description":"Denies the webview_size command without any pre-configured scope.","commands":{"allow":[],"deny":["webview_size"]}}},"permission_sets":{},"global_scope_schema":null},"core:window":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin.","permissions":["allow-get-all-windows","allow-scale-factor","allow-inner-position","allow-outer-position","allow-inner-size","allow-outer-size","allow-is-fullscreen","allow-is-minimized","allow-is-maximized","allow-is-focused","allow-is-decorated","allow-is-resizable","allow-is-maximizable","allow-is-minimizable","allow-is-closable","allow-is-visible","allow-is-enabled","allow-title","allow-current-monitor","allow-primary-monitor","allow-monitor-from-point","allow-available-monitors","allow-cursor-position","allow-theme","allow-internal-toggle-maximize"]},"permissions":{"allow-available-monitors":{"identifier":"allow-available-monitors","description":"Enables the available_monitors command without any pre-configured scope.","commands":{"allow":["available_monitors"],"deny":[]}},"allow-center":{"identifier":"allow-center","description":"Enables the center command without any pre-configured scope.","commands":{"allow":["center"],"deny":[]}},"allow-close":{"identifier":"allow-close","description":"Enables the close command without any pre-configured scope.","commands":{"allow":["close"],"deny":[]}},"allow-create":{"identifier":"allow-create","description":"Enables the create command without any pre-configured scope.","commands":{"allow":["create"],"deny":[]}},"allow-current-monitor":{"identifier":"allow-current-monitor","description":"Enables the current_monitor command without any pre-configured scope.","commands":{"allow":["current_monitor"],"deny":[]}},"allow-cursor-position":{"identifier":"allow-cursor-position","description":"Enables the cursor_position command without any pre-configured scope.","commands":{"allow":["cursor_position"],"deny":[]}},"allow-destroy":{"identifier":"allow-destroy","description":"Enables the destroy command without any pre-configured scope.","commands":{"allow":["destroy"],"deny":[]}},"allow-get-all-windows":{"identifier":"allow-get-all-windows","description":"Enables the get_all_windows command without any pre-configured scope.","commands":{"allow":["get_all_windows"],"deny":[]}},"allow-hide":{"identifier":"allow-hide","description":"Enables the hide command without any pre-configured scope.","commands":{"allow":["hide"],"deny":[]}},"allow-inner-position":{"identifier":"allow-inner-position","description":"Enables the inner_position command without any pre-configured scope.","commands":{"allow":["inner_position"],"deny":[]}},"allow-inner-size":{"identifier":"allow-inner-size","description":"Enables the inner_size command without any pre-configured scope.","commands":{"allow":["inner_size"],"deny":[]}},"allow-internal-toggle-maximize":{"identifier":"allow-internal-toggle-maximize","description":"Enables the internal_toggle_maximize command without any pre-configured scope.","commands":{"allow":["internal_toggle_maximize"],"deny":[]}},"allow-is-closable":{"identifier":"allow-is-closable","description":"Enables the is_closable command without any pre-configured scope.","commands":{"allow":["is_closable"],"deny":[]}},"allow-is-decorated":{"identifier":"allow-is-decorated","description":"Enables the is_decorated command without any pre-configured scope.","commands":{"allow":["is_decorated"],"deny":[]}},"allow-is-enabled":{"identifier":"allow-is-enabled","description":"Enables the is_enabled command without any pre-configured scope.","commands":{"allow":["is_enabled"],"deny":[]}},"allow-is-focused":{"identifier":"allow-is-focused","description":"Enables the is_focused command without any pre-configured scope.","commands":{"allow":["is_focused"],"deny":[]}},"allow-is-fullscreen":{"identifier":"allow-is-fullscreen","description":"Enables the is_fullscreen command without any pre-configured scope.","commands":{"allow":["is_fullscreen"],"deny":[]}},"allow-is-maximizable":{"identifier":"allow-is-maximizable","description":"Enables the is_maximizable command without any pre-configured scope.","commands":{"allow":["is_maximizable"],"deny":[]}},"allow-is-maximized":{"identifier":"allow-is-maximized","description":"Enables the is_maximized command without any pre-configured scope.","commands":{"allow":["is_maximized"],"deny":[]}},"allow-is-minimizable":{"identifier":"allow-is-minimizable","description":"Enables the is_minimizable command without any pre-configured scope.","commands":{"allow":["is_minimizable"],"deny":[]}},"allow-is-minimized":{"identifier":"allow-is-minimized","description":"Enables the is_minimized command without any pre-configured scope.","commands":{"allow":["is_minimized"],"deny":[]}},"allow-is-resizable":{"identifier":"allow-is-resizable","description":"Enables the is_resizable command without any pre-configured scope.","commands":{"allow":["is_resizable"],"deny":[]}},"allow-is-visible":{"identifier":"allow-is-visible","description":"Enables the is_visible command without any pre-configured scope.","commands":{"allow":["is_visible"],"deny":[]}},"allow-maximize":{"identifier":"allow-maximize","description":"Enables the maximize command without any pre-configured scope.","commands":{"allow":["maximize"],"deny":[]}},"allow-minimize":{"identifier":"allow-minimize","description":"Enables the minimize command without any pre-configured scope.","commands":{"allow":["minimize"],"deny":[]}},"allow-monitor-from-point":{"identifier":"allow-monitor-from-point","description":"Enables the monitor_from_point command without any pre-configured scope.","commands":{"allow":["monitor_from_point"],"deny":[]}},"allow-outer-position":{"identifier":"allow-outer-position","description":"Enables the outer_position command without any pre-configured scope.","commands":{"allow":["outer_position"],"deny":[]}},"allow-outer-size":{"identifier":"allow-outer-size","description":"Enables the outer_size command without any pre-configured scope.","commands":{"allow":["outer_size"],"deny":[]}},"allow-primary-monitor":{"identifier":"allow-primary-monitor","description":"Enables the primary_monitor command without any pre-configured scope.","commands":{"allow":["primary_monitor"],"deny":[]}},"allow-request-user-attention":{"identifier":"allow-request-user-attention","description":"Enables the request_user_attention command without any pre-configured scope.","commands":{"allow":["request_user_attention"],"deny":[]}},"allow-scale-factor":{"identifier":"allow-scale-factor","description":"Enables the scale_factor command without any pre-configured scope.","commands":{"allow":["scale_factor"],"deny":[]}},"allow-set-always-on-bottom":{"identifier":"allow-set-always-on-bottom","description":"Enables the set_always_on_bottom command without any pre-configured scope.","commands":{"allow":["set_always_on_bottom"],"deny":[]}},"allow-set-always-on-top":{"identifier":"allow-set-always-on-top","description":"Enables the set_always_on_top command without any pre-configured scope.","commands":{"allow":["set_always_on_top"],"deny":[]}},"allow-set-closable":{"identifier":"allow-set-closable","description":"Enables the set_closable command without any pre-configured scope.","commands":{"allow":["set_closable"],"deny":[]}},"allow-set-content-protected":{"identifier":"allow-set-content-protected","description":"Enables the set_content_protected command without any pre-configured scope.","commands":{"allow":["set_content_protected"],"deny":[]}},"allow-set-cursor-grab":{"identifier":"allow-set-cursor-grab","description":"Enables the set_cursor_grab command without any pre-configured scope.","commands":{"allow":["set_cursor_grab"],"deny":[]}},"allow-set-cursor-icon":{"identifier":"allow-set-cursor-icon","description":"Enables the set_cursor_icon command without any pre-configured scope.","commands":{"allow":["set_cursor_icon"],"deny":[]}},"allow-set-cursor-position":{"identifier":"allow-set-cursor-position","description":"Enables the set_cursor_position command without any pre-configured scope.","commands":{"allow":["set_cursor_position"],"deny":[]}},"allow-set-cursor-visible":{"identifier":"allow-set-cursor-visible","description":"Enables the set_cursor_visible command without any pre-configured scope.","commands":{"allow":["set_cursor_visible"],"deny":[]}},"allow-set-decorations":{"identifier":"allow-set-decorations","description":"Enables the set_decorations command without any pre-configured scope.","commands":{"allow":["set_decorations"],"deny":[]}},"allow-set-effects":{"identifier":"allow-set-effects","description":"Enables the set_effects command without any pre-configured scope.","commands":{"allow":["set_effects"],"deny":[]}},"allow-set-enabled":{"identifier":"allow-set-enabled","description":"Enables the set_enabled command without any pre-configured scope.","commands":{"allow":["set_enabled"],"deny":[]}},"allow-set-focus":{"identifier":"allow-set-focus","description":"Enables the set_focus command without any pre-configured scope.","commands":{"allow":["set_focus"],"deny":[]}},"allow-set-fullscreen":{"identifier":"allow-set-fullscreen","description":"Enables the set_fullscreen command without any pre-configured scope.","commands":{"allow":["set_fullscreen"],"deny":[]}},"allow-set-icon":{"identifier":"allow-set-icon","description":"Enables the set_icon command without any pre-configured scope.","commands":{"allow":["set_icon"],"deny":[]}},"allow-set-ignore-cursor-events":{"identifier":"allow-set-ignore-cursor-events","description":"Enables the set_ignore_cursor_events command without any pre-configured scope.","commands":{"allow":["set_ignore_cursor_events"],"deny":[]}},"allow-set-max-size":{"identifier":"allow-set-max-size","description":"Enables the set_max_size command without any pre-configured scope.","commands":{"allow":["set_max_size"],"deny":[]}},"allow-set-maximizable":{"identifier":"allow-set-maximizable","description":"Enables the set_maximizable command without any pre-configured scope.","commands":{"allow":["set_maximizable"],"deny":[]}},"allow-set-min-size":{"identifier":"allow-set-min-size","description":"Enables the set_min_size command without any pre-configured scope.","commands":{"allow":["set_min_size"],"deny":[]}},"allow-set-minimizable":{"identifier":"allow-set-minimizable","description":"Enables the set_minimizable command without any pre-configured scope.","commands":{"allow":["set_minimizable"],"deny":[]}},"allow-set-position":{"identifier":"allow-set-position","description":"Enables the set_position command without any pre-configured scope.","commands":{"allow":["set_position"],"deny":[]}},"allow-set-progress-bar":{"identifier":"allow-set-progress-bar","description":"Enables the set_progress_bar command without any pre-configured scope.","commands":{"allow":["set_progress_bar"],"deny":[]}},"allow-set-resizable":{"identifier":"allow-set-resizable","description":"Enables the set_resizable command without any pre-configured scope.","commands":{"allow":["set_resizable"],"deny":[]}},"allow-set-shadow":{"identifier":"allow-set-shadow","description":"Enables the set_shadow command without any pre-configured scope.","commands":{"allow":["set_shadow"],"deny":[]}},"allow-set-size":{"identifier":"allow-set-size","description":"Enables the set_size command without any pre-configured scope.","commands":{"allow":["set_size"],"deny":[]}},"allow-set-size-constraints":{"identifier":"allow-set-size-constraints","description":"Enables the set_size_constraints command without any pre-configured scope.","commands":{"allow":["set_size_constraints"],"deny":[]}},"allow-set-skip-taskbar":{"identifier":"allow-set-skip-taskbar","description":"Enables the set_skip_taskbar command without any pre-configured scope.","commands":{"allow":["set_skip_taskbar"],"deny":[]}},"allow-set-theme":{"identifier":"allow-set-theme","description":"Enables the set_theme command without any pre-configured scope.","commands":{"allow":["set_theme"],"deny":[]}},"allow-set-title":{"identifier":"allow-set-title","description":"Enables the set_title command without any pre-configured scope.","commands":{"allow":["set_title"],"deny":[]}},"allow-set-title-bar-style":{"identifier":"allow-set-title-bar-style","description":"Enables the set_title_bar_style command without any pre-configured scope.","commands":{"allow":["set_title_bar_style"],"deny":[]}},"allow-set-visible-on-all-workspaces":{"identifier":"allow-set-visible-on-all-workspaces","description":"Enables the set_visible_on_all_workspaces command without any pre-configured scope.","commands":{"allow":["set_visible_on_all_workspaces"],"deny":[]}},"allow-show":{"identifier":"allow-show","description":"Enables the show command without any pre-configured scope.","commands":{"allow":["show"],"deny":[]}},"allow-start-dragging":{"identifier":"allow-start-dragging","description":"Enables the start_dragging command without any pre-configured scope.","commands":{"allow":["start_dragging"],"deny":[]}},"allow-start-resize-dragging":{"identifier":"allow-start-resize-dragging","description":"Enables the start_resize_dragging command without any pre-configured scope.","commands":{"allow":["start_resize_dragging"],"deny":[]}},"allow-theme":{"identifier":"allow-theme","description":"Enables the theme command without any pre-configured scope.","commands":{"allow":["theme"],"deny":[]}},"allow-title":{"identifier":"allow-title","description":"Enables the title command without any pre-configured scope.","commands":{"allow":["title"],"deny":[]}},"allow-toggle-maximize":{"identifier":"allow-toggle-maximize","description":"Enables the toggle_maximize command without any pre-configured scope.","commands":{"allow":["toggle_maximize"],"deny":[]}},"allow-unmaximize":{"identifier":"allow-unmaximize","description":"Enables the unmaximize command without any pre-configured scope.","commands":{"allow":["unmaximize"],"deny":[]}},"allow-unminimize":{"identifier":"allow-unminimize","description":"Enables the unminimize command without any pre-configured scope.","commands":{"allow":["unminimize"],"deny":[]}},"deny-available-monitors":{"identifier":"deny-available-monitors","description":"Denies the available_monitors command without any pre-configured scope.","commands":{"allow":[],"deny":["available_monitors"]}},"deny-center":{"identifier":"deny-center","description":"Denies the center command without any pre-configured scope.","commands":{"allow":[],"deny":["center"]}},"deny-close":{"identifier":"deny-close","description":"Denies the close command without any pre-configured scope.","commands":{"allow":[],"deny":["close"]}},"deny-create":{"identifier":"deny-create","description":"Denies the create command without any pre-configured scope.","commands":{"allow":[],"deny":["create"]}},"deny-current-monitor":{"identifier":"deny-current-monitor","description":"Denies the current_monitor command without any pre-configured scope.","commands":{"allow":[],"deny":["current_monitor"]}},"deny-cursor-position":{"identifier":"deny-cursor-position","description":"Denies the cursor_position command without any pre-configured scope.","commands":{"allow":[],"deny":["cursor_position"]}},"deny-destroy":{"identifier":"deny-destroy","description":"Denies the destroy command without any pre-configured scope.","commands":{"allow":[],"deny":["destroy"]}},"deny-get-all-windows":{"identifier":"deny-get-all-windows","description":"Denies the get_all_windows command without any pre-configured scope.","commands":{"allow":[],"deny":["get_all_windows"]}},"deny-hide":{"identifier":"deny-hide","description":"Denies the hide command without any pre-configured scope.","commands":{"allow":[],"deny":["hide"]}},"deny-inner-position":{"identifier":"deny-inner-position","description":"Denies the inner_position command without any pre-configured scope.","commands":{"allow":[],"deny":["inner_position"]}},"deny-inner-size":{"identifier":"deny-inner-size","description":"Denies the inner_size command without any pre-configured scope.","commands":{"allow":[],"deny":["inner_size"]}},"deny-internal-toggle-maximize":{"identifier":"deny-internal-toggle-maximize","description":"Denies the internal_toggle_maximize command without any pre-configured scope.","commands":{"allow":[],"deny":["internal_toggle_maximize"]}},"deny-is-closable":{"identifier":"deny-is-closable","description":"Denies the is_closable command without any pre-configured scope.","commands":{"allow":[],"deny":["is_closable"]}},"deny-is-decorated":{"identifier":"deny-is-decorated","description":"Denies the is_decorated command without any pre-configured scope.","commands":{"allow":[],"deny":["is_decorated"]}},"deny-is-enabled":{"identifier":"deny-is-enabled","description":"Denies the is_enabled command without any pre-configured scope.","commands":{"allow":[],"deny":["is_enabled"]}},"deny-is-focused":{"identifier":"deny-is-focused","description":"Denies the is_focused command without any pre-configured scope.","commands":{"allow":[],"deny":["is_focused"]}},"deny-is-fullscreen":{"identifier":"deny-is-fullscreen","description":"Denies the is_fullscreen command without any pre-configured scope.","commands":{"allow":[],"deny":["is_fullscreen"]}},"deny-is-maximizable":{"identifier":"deny-is-maximizable","description":"Denies the is_maximizable command without any pre-configured scope.","commands":{"allow":[],"deny":["is_maximizable"]}},"deny-is-maximized":{"identifier":"deny-is-maximized","description":"Denies the is_maximized command without any pre-configured scope.","commands":{"allow":[],"deny":["is_maximized"]}},"deny-is-minimizable":{"identifier":"deny-is-minimizable","description":"Denies the is_minimizable command without any pre-configured scope.","commands":{"allow":[],"deny":["is_minimizable"]}},"deny-is-minimized":{"identifier":"deny-is-minimized","description":"Denies the is_minimized command without any pre-configured scope.","commands":{"allow":[],"deny":["is_minimized"]}},"deny-is-resizable":{"identifier":"deny-is-resizable","description":"Denies the is_resizable command without any pre-configured scope.","commands":{"allow":[],"deny":["is_resizable"]}},"deny-is-visible":{"identifier":"deny-is-visible","description":"Denies the is_visible command without any pre-configured scope.","commands":{"allow":[],"deny":["is_visible"]}},"deny-maximize":{"identifier":"deny-maximize","description":"Denies the maximize command without any pre-configured scope.","commands":{"allow":[],"deny":["maximize"]}},"deny-minimize":{"identifier":"deny-minimize","description":"Denies the minimize command without any pre-configured scope.","commands":{"allow":[],"deny":["minimize"]}},"deny-monitor-from-point":{"identifier":"deny-monitor-from-point","description":"Denies the monitor_from_point command without any pre-configured scope.","commands":{"allow":[],"deny":["monitor_from_point"]}},"deny-outer-position":{"identifier":"deny-outer-position","description":"Denies the outer_position command without any pre-configured scope.","commands":{"allow":[],"deny":["outer_position"]}},"deny-outer-size":{"identifier":"deny-outer-size","description":"Denies the outer_size command without any pre-configured scope.","commands":{"allow":[],"deny":["outer_size"]}},"deny-primary-monitor":{"identifier":"deny-primary-monitor","description":"Denies the primary_monitor command without any pre-configured scope.","commands":{"allow":[],"deny":["primary_monitor"]}},"deny-request-user-attention":{"identifier":"deny-request-user-attention","description":"Denies the request_user_attention command without any pre-configured scope.","commands":{"allow":[],"deny":["request_user_attention"]}},"deny-scale-factor":{"identifier":"deny-scale-factor","description":"Denies the scale_factor command without any pre-configured scope.","commands":{"allow":[],"deny":["scale_factor"]}},"deny-set-always-on-bottom":{"identifier":"deny-set-always-on-bottom","description":"Denies the set_always_on_bottom command without any pre-configured scope.","commands":{"allow":[],"deny":["set_always_on_bottom"]}},"deny-set-always-on-top":{"identifier":"deny-set-always-on-top","description":"Denies the set_always_on_top command without any pre-configured scope.","commands":{"allow":[],"deny":["set_always_on_top"]}},"deny-set-closable":{"identifier":"deny-set-closable","description":"Denies the set_closable command without any pre-configured scope.","commands":{"allow":[],"deny":["set_closable"]}},"deny-set-content-protected":{"identifier":"deny-set-content-protected","description":"Denies the set_content_protected command without any pre-configured scope.","commands":{"allow":[],"deny":["set_content_protected"]}},"deny-set-cursor-grab":{"identifier":"deny-set-cursor-grab","description":"Denies the set_cursor_grab command without any pre-configured scope.","commands":{"allow":[],"deny":["set_cursor_grab"]}},"deny-set-cursor-icon":{"identifier":"deny-set-cursor-icon","description":"Denies the set_cursor_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["set_cursor_icon"]}},"deny-set-cursor-position":{"identifier":"deny-set-cursor-position","description":"Denies the set_cursor_position command without any pre-configured scope.","commands":{"allow":[],"deny":["set_cursor_position"]}},"deny-set-cursor-visible":{"identifier":"deny-set-cursor-visible","description":"Denies the set_cursor_visible command without any pre-configured scope.","commands":{"allow":[],"deny":["set_cursor_visible"]}},"deny-set-decorations":{"identifier":"deny-set-decorations","description":"Denies the set_decorations command without any pre-configured scope.","commands":{"allow":[],"deny":["set_decorations"]}},"deny-set-effects":{"identifier":"deny-set-effects","description":"Denies the set_effects command without any pre-configured scope.","commands":{"allow":[],"deny":["set_effects"]}},"deny-set-enabled":{"identifier":"deny-set-enabled","description":"Denies the set_enabled command without any pre-configured scope.","commands":{"allow":[],"deny":["set_enabled"]}},"deny-set-focus":{"identifier":"deny-set-focus","description":"Denies the set_focus command without any pre-configured scope.","commands":{"allow":[],"deny":["set_focus"]}},"deny-set-fullscreen":{"identifier":"deny-set-fullscreen","description":"Denies the set_fullscreen command without any pre-configured scope.","commands":{"allow":[],"deny":["set_fullscreen"]}},"deny-set-icon":{"identifier":"deny-set-icon","description":"Denies the set_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["set_icon"]}},"deny-set-ignore-cursor-events":{"identifier":"deny-set-ignore-cursor-events","description":"Denies the set_ignore_cursor_events command without any pre-configured scope.","commands":{"allow":[],"deny":["set_ignore_cursor_events"]}},"deny-set-max-size":{"identifier":"deny-set-max-size","description":"Denies the set_max_size command without any pre-configured scope.","commands":{"allow":[],"deny":["set_max_size"]}},"deny-set-maximizable":{"identifier":"deny-set-maximizable","description":"Denies the set_maximizable command without any pre-configured scope.","commands":{"allow":[],"deny":["set_maximizable"]}},"deny-set-min-size":{"identifier":"deny-set-min-size","description":"Denies the set_min_size command without any pre-configured scope.","commands":{"allow":[],"deny":["set_min_size"]}},"deny-set-minimizable":{"identifier":"deny-set-minimizable","description":"Denies the set_minimizable command without any pre-configured scope.","commands":{"allow":[],"deny":["set_minimizable"]}},"deny-set-position":{"identifier":"deny-set-position","description":"Denies the set_position command without any pre-configured scope.","commands":{"allow":[],"deny":["set_position"]}},"deny-set-progress-bar":{"identifier":"deny-set-progress-bar","description":"Denies the set_progress_bar command without any pre-configured scope.","commands":{"allow":[],"deny":["set_progress_bar"]}},"deny-set-resizable":{"identifier":"deny-set-resizable","description":"Denies the set_resizable command without any pre-configured scope.","commands":{"allow":[],"deny":["set_resizable"]}},"deny-set-shadow":{"identifier":"deny-set-shadow","description":"Denies the set_shadow command without any pre-configured scope.","commands":{"allow":[],"deny":["set_shadow"]}},"deny-set-size":{"identifier":"deny-set-size","description":"Denies the set_size command without any pre-configured scope.","commands":{"allow":[],"deny":["set_size"]}},"deny-set-size-constraints":{"identifier":"deny-set-size-constraints","description":"Denies the set_size_constraints command without any pre-configured scope.","commands":{"allow":[],"deny":["set_size_constraints"]}},"deny-set-skip-taskbar":{"identifier":"deny-set-skip-taskbar","description":"Denies the set_skip_taskbar command without any pre-configured scope.","commands":{"allow":[],"deny":["set_skip_taskbar"]}},"deny-set-theme":{"identifier":"deny-set-theme","description":"Denies the set_theme command without any pre-configured scope.","commands":{"allow":[],"deny":["set_theme"]}},"deny-set-title":{"identifier":"deny-set-title","description":"Denies the set_title command without any pre-configured scope.","commands":{"allow":[],"deny":["set_title"]}},"deny-set-title-bar-style":{"identifier":"deny-set-title-bar-style","description":"Denies the set_title_bar_style command without any pre-configured scope.","commands":{"allow":[],"deny":["set_title_bar_style"]}},"deny-set-visible-on-all-workspaces":{"identifier":"deny-set-visible-on-all-workspaces","description":"Denies the set_visible_on_all_workspaces command without any pre-configured scope.","commands":{"allow":[],"deny":["set_visible_on_all_workspaces"]}},"deny-show":{"identifier":"deny-show","description":"Denies the show command without any pre-configured scope.","commands":{"allow":[],"deny":["show"]}},"deny-start-dragging":{"identifier":"deny-start-dragging","description":"Denies the start_dragging command without any pre-configured scope.","commands":{"allow":[],"deny":["start_dragging"]}},"deny-start-resize-dragging":{"identifier":"deny-start-resize-dragging","description":"Denies the start_resize_dragging command without any pre-configured scope.","commands":{"allow":[],"deny":["start_resize_dragging"]}},"deny-theme":{"identifier":"deny-theme","description":"Denies the theme command without any pre-configured scope.","commands":{"allow":[],"deny":["theme"]}},"deny-title":{"identifier":"deny-title","description":"Denies the title command without any pre-configured scope.","commands":{"allow":[],"deny":["title"]}},"deny-toggle-maximize":{"identifier":"deny-toggle-maximize","description":"Denies the toggle_maximize command without any pre-configured scope.","commands":{"allow":[],"deny":["toggle_maximize"]}},"deny-unmaximize":{"identifier":"deny-unmaximize","description":"Denies the unmaximize command without any pre-configured scope.","commands":{"allow":[],"deny":["unmaximize"]}},"deny-unminimize":{"identifier":"deny-unminimize","description":"Denies the unminimize command without any pre-configured scope.","commands":{"allow":[],"deny":["unminimize"]}}},"permission_sets":{},"global_scope_schema":null},"dialog":{"default_permission":{"identifier":"default","description":"This permission set configures the types of dialogs\navailable from the dialog plugin.\n\n#### Granted Permissions\n\nAll dialog types are enabled.\n\n\n","permissions":["allow-ask","allow-confirm","allow-message","allow-save","allow-open"]},"permissions":{"allow-ask":{"identifier":"allow-ask","description":"Enables the ask command without any pre-configured scope.","commands":{"allow":["ask"],"deny":[]}},"allow-confirm":{"identifier":"allow-confirm","description":"Enables the confirm command without any pre-configured scope.","commands":{"allow":["confirm"],"deny":[]}},"allow-message":{"identifier":"allow-message","description":"Enables the message command without any pre-configured scope.","commands":{"allow":["message"],"deny":[]}},"allow-open":{"identifier":"allow-open","description":"Enables the open command without any pre-configured scope.","commands":{"allow":["open"],"deny":[]}},"allow-save":{"identifier":"allow-save","description":"Enables the save command without any pre-configured scope.","commands":{"allow":["save"],"deny":[]}},"deny-ask":{"identifier":"deny-ask","description":"Denies the ask command without any pre-configured scope.","commands":{"allow":[],"deny":["ask"]}},"deny-confirm":{"identifier":"deny-confirm","description":"Denies the confirm command without any pre-configured scope.","commands":{"allow":[],"deny":["confirm"]}},"deny-message":{"identifier":"deny-message","description":"Denies the message command without any pre-configured scope.","commands":{"allow":[],"deny":["message"]}},"deny-open":{"identifier":"deny-open","description":"Denies the open command without any pre-configured scope.","commands":{"allow":[],"deny":["open"]}},"deny-save":{"identifier":"deny-save","description":"Denies the save command without any pre-configured scope.","commands":{"allow":[],"deny":["save"]}}},"permission_sets":{},"global_scope_schema":null},"log":{"default_permission":{"identifier":"default","description":"Allows the log command","permissions":["allow-log"]},"permissions":{"allow-log":{"identifier":"allow-log","description":"Enables the log command without any pre-configured scope.","commands":{"allow":["log"],"deny":[]}},"deny-log":{"identifier":"deny-log","description":"Denies the log command without any pre-configured scope.","commands":{"allow":[],"deny":["log"]}}},"permission_sets":{},"global_scope_schema":null},"notification":{"default_permission":{"identifier":"default","description":"This permission set configures which\nnotification features are by default exposed.\n\n#### Granted Permissions\n\nIt allows all notification related features.\n\n","permissions":["allow-is-permission-granted","allow-request-permission","allow-notify","allow-register-action-types","allow-register-listener","allow-cancel","allow-get-pending","allow-remove-active","allow-get-active","allow-check-permissions","allow-show","allow-batch","allow-list-channels","allow-delete-channel","allow-create-channel","allow-permission-state"]},"permissions":{"allow-batch":{"identifier":"allow-batch","description":"Enables the batch command without any pre-configured scope.","commands":{"allow":["batch"],"deny":[]}},"allow-cancel":{"identifier":"allow-cancel","description":"Enables the cancel command without any pre-configured scope.","commands":{"allow":["cancel"],"deny":[]}},"allow-check-permissions":{"identifier":"allow-check-permissions","description":"Enables the check_permissions command without any pre-configured scope.","commands":{"allow":["check_permissions"],"deny":[]}},"allow-create-channel":{"identifier":"allow-create-channel","description":"Enables the create_channel command without any pre-configured scope.","commands":{"allow":["create_channel"],"deny":[]}},"allow-delete-channel":{"identifier":"allow-delete-channel","description":"Enables the delete_channel command without any pre-configured scope.","commands":{"allow":["delete_channel"],"deny":[]}},"allow-get-active":{"identifier":"allow-get-active","description":"Enables the get_active command without any pre-configured scope.","commands":{"allow":["get_active"],"deny":[]}},"allow-get-pending":{"identifier":"allow-get-pending","description":"Enables the get_pending command without any pre-configured scope.","commands":{"allow":["get_pending"],"deny":[]}},"allow-is-permission-granted":{"identifier":"allow-is-permission-granted","description":"Enables the is_permission_granted command without any pre-configured scope.","commands":{"allow":["is_permission_granted"],"deny":[]}},"allow-list-channels":{"identifier":"allow-list-channels","description":"Enables the list_channels command without any pre-configured scope.","commands":{"allow":["list_channels"],"deny":[]}},"allow-notify":{"identifier":"allow-notify","description":"Enables the notify command without any pre-configured scope.","commands":{"allow":["notify"],"deny":[]}},"allow-permission-state":{"identifier":"allow-permission-state","description":"Enables the permission_state command without any pre-configured scope.","commands":{"allow":["permission_state"],"deny":[]}},"allow-register-action-types":{"identifier":"allow-register-action-types","description":"Enables the register_action_types command without any pre-configured scope.","commands":{"allow":["register_action_types"],"deny":[]}},"allow-register-listener":{"identifier":"allow-register-listener","description":"Enables the register_listener command without any pre-configured scope.","commands":{"allow":["register_listener"],"deny":[]}},"allow-remove-active":{"identifier":"allow-remove-active","description":"Enables the remove_active command without any pre-configured scope.","commands":{"allow":["remove_active"],"deny":[]}},"allow-request-permission":{"identifier":"allow-request-permission","description":"Enables the request_permission command without any pre-configured scope.","commands":{"allow":["request_permission"],"deny":[]}},"allow-show":{"identifier":"allow-show","description":"Enables the show command without any pre-configured scope.","commands":{"allow":["show"],"deny":[]}},"deny-batch":{"identifier":"deny-batch","description":"Denies the batch command without any pre-configured scope.","commands":{"allow":[],"deny":["batch"]}},"deny-cancel":{"identifier":"deny-cancel","description":"Denies the cancel command without any pre-configured scope.","commands":{"allow":[],"deny":["cancel"]}},"deny-check-permissions":{"identifier":"deny-check-permissions","description":"Denies the check_permissions command without any pre-configured scope.","commands":{"allow":[],"deny":["check_permissions"]}},"deny-create-channel":{"identifier":"deny-create-channel","description":"Denies the create_channel command without any pre-configured scope.","commands":{"allow":[],"deny":["create_channel"]}},"deny-delete-channel":{"identifier":"deny-delete-channel","description":"Denies the delete_channel command without any pre-configured scope.","commands":{"allow":[],"deny":["delete_channel"]}},"deny-get-active":{"identifier":"deny-get-active","description":"Denies the get_active command without any pre-configured scope.","commands":{"allow":[],"deny":["get_active"]}},"deny-get-pending":{"identifier":"deny-get-pending","description":"Denies the get_pending command without any pre-configured scope.","commands":{"allow":[],"deny":["get_pending"]}},"deny-is-permission-granted":{"identifier":"deny-is-permission-granted","description":"Denies the is_permission_granted command without any pre-configured scope.","commands":{"allow":[],"deny":["is_permission_granted"]}},"deny-list-channels":{"identifier":"deny-list-channels","description":"Denies the list_channels command without any pre-configured scope.","commands":{"allow":[],"deny":["list_channels"]}},"deny-notify":{"identifier":"deny-notify","description":"Denies the notify command without any pre-configured scope.","commands":{"allow":[],"deny":["notify"]}},"deny-permission-state":{"identifier":"deny-permission-state","description":"Denies the permission_state command without any pre-configured scope.","commands":{"allow":[],"deny":["permission_state"]}},"deny-register-action-types":{"identifier":"deny-register-action-types","description":"Denies the register_action_types command without any pre-configured scope.","commands":{"allow":[],"deny":["register_action_types"]}},"deny-register-listener":{"identifier":"deny-register-listener","description":"Denies the register_listener command without any pre-configured scope.","commands":{"allow":[],"deny":["register_listener"]}},"deny-remove-active":{"identifier":"deny-remove-active","description":"Denies the remove_active command without any pre-configured scope.","commands":{"allow":[],"deny":["remove_active"]}},"deny-request-permission":{"identifier":"deny-request-permission","description":"Denies the request_permission command without any pre-configured scope.","commands":{"allow":[],"deny":["request_permission"]}},"deny-show":{"identifier":"deny-show","description":"Denies the show command without any pre-configured scope.","commands":{"allow":[],"deny":["show"]}}},"permission_sets":{},"global_scope_schema":null},"os":{"default_permission":{"identifier":"default","description":"This permission set configures which\noperating system information are available\nto gather from the frontend.\n\n#### Granted Permissions\n\nAll information except the host name are available.\n\n","permissions":["allow-arch","allow-exe-extension","allow-family","allow-locale","allow-os-type","allow-platform","allow-version"]},"permissions":{"allow-arch":{"identifier":"allow-arch","description":"Enables the arch command without any pre-configured scope.","commands":{"allow":["arch"],"deny":[]}},"allow-exe-extension":{"identifier":"allow-exe-extension","description":"Enables the exe_extension command without any pre-configured scope.","commands":{"allow":["exe_extension"],"deny":[]}},"allow-family":{"identifier":"allow-family","description":"Enables the family command without any pre-configured scope.","commands":{"allow":["family"],"deny":[]}},"allow-hostname":{"identifier":"allow-hostname","description":"Enables the hostname command without any pre-configured scope.","commands":{"allow":["hostname"],"deny":[]}},"allow-locale":{"identifier":"allow-locale","description":"Enables the locale command without any pre-configured scope.","commands":{"allow":["locale"],"deny":[]}},"allow-os-type":{"identifier":"allow-os-type","description":"Enables the os_type command without any pre-configured scope.","commands":{"allow":["os_type"],"deny":[]}},"allow-platform":{"identifier":"allow-platform","description":"Enables the platform command without any pre-configured scope.","commands":{"allow":["platform"],"deny":[]}},"allow-version":{"identifier":"allow-version","description":"Enables the version command without any pre-configured scope.","commands":{"allow":["version"],"deny":[]}},"deny-arch":{"identifier":"deny-arch","description":"Denies the arch command without any pre-configured scope.","commands":{"allow":[],"deny":["arch"]}},"deny-exe-extension":{"identifier":"deny-exe-extension","description":"Denies the exe_extension command without any pre-configured scope.","commands":{"allow":[],"deny":["exe_extension"]}},"deny-family":{"identifier":"deny-family","description":"Denies the family command without any pre-configured scope.","commands":{"allow":[],"deny":["family"]}},"deny-hostname":{"identifier":"deny-hostname","description":"Denies the hostname command without any pre-configured scope.","commands":{"allow":[],"deny":["hostname"]}},"deny-locale":{"identifier":"deny-locale","description":"Denies the locale command without any pre-configured scope.","commands":{"allow":[],"deny":["locale"]}},"deny-os-type":{"identifier":"deny-os-type","description":"Denies the os_type command without any pre-configured scope.","commands":{"allow":[],"deny":["os_type"]}},"deny-platform":{"identifier":"deny-platform","description":"Denies the platform command without any pre-configured scope.","commands":{"allow":[],"deny":["platform"]}},"deny-version":{"identifier":"deny-version","description":"Denies the version command without any pre-configured scope.","commands":{"allow":[],"deny":["version"]}}},"permission_sets":{},"global_scope_schema":null},"shell":{"default_permission":{"identifier":"default","description":"This permission set configures which\nshell functionality is exposed by default.\n\n#### Granted Permissions\n\nIt allows to use the `open` functionality without any specific\nscope pre-configured. It will allow opening `http(s)://`,\n`tel:` and `mailto:` links.\n","permissions":["allow-open"]},"permissions":{"allow-execute":{"identifier":"allow-execute","description":"Enables the execute command without any pre-configured scope.","commands":{"allow":["execute"],"deny":[]}},"allow-kill":{"identifier":"allow-kill","description":"Enables the kill command without any pre-configured scope.","commands":{"allow":["kill"],"deny":[]}},"allow-open":{"identifier":"allow-open","description":"Enables the open command without any pre-configured scope.","commands":{"allow":["open"],"deny":[]}},"allow-spawn":{"identifier":"allow-spawn","description":"Enables the spawn command without any pre-configured scope.","commands":{"allow":["spawn"],"deny":[]}},"allow-stdin-write":{"identifier":"allow-stdin-write","description":"Enables the stdin_write command without any pre-configured scope.","commands":{"allow":["stdin_write"],"deny":[]}},"deny-execute":{"identifier":"deny-execute","description":"Denies the execute command without any pre-configured scope.","commands":{"allow":[],"deny":["execute"]}},"deny-kill":{"identifier":"deny-kill","description":"Denies the kill command without any pre-configured scope.","commands":{"allow":[],"deny":["kill"]}},"deny-open":{"identifier":"deny-open","description":"Denies the open command without any pre-configured scope.","commands":{"allow":[],"deny":["open"]}},"deny-spawn":{"identifier":"deny-spawn","description":"Denies the spawn command without any pre-configured scope.","commands":{"allow":[],"deny":["spawn"]}},"deny-stdin-write":{"identifier":"deny-stdin-write","description":"Denies the stdin_write command without any pre-configured scope.","commands":{"allow":[],"deny":["stdin_write"]}}},"permission_sets":{},"global_scope_schema":{"$schema":"http://json-schema.org/draft-07/schema#","anyOf":[{"additionalProperties":false,"properties":{"args":{"allOf":[{"$ref":"#/definitions/ShellScopeEntryAllowedArgs"}],"description":"The allowed arguments for the command execution."},"cmd":{"description":"The command name. It can start with a variable that resolves to a system base directory. The variables are: `$AUDIO`, `$CACHE`, `$CONFIG`, `$DATA`, `$LOCALDATA`, `$DESKTOP`, `$DOCUMENT`, `$DOWNLOAD`, `$EXE`, `$FONT`, `$HOME`, `$PICTURE`, `$PUBLIC`, `$RUNTIME`, `$TEMPLATE`, `$VIDEO`, `$RESOURCE`, `$LOG`, `$TEMP`, `$APPCONFIG`, `$APPDATA`, `$APPLOCALDATA`, `$APPCACHE`, `$APPLOG`.","type":"string"},"name":{"description":"The name for this allowed shell command configuration.\n\nThis name will be used inside of the webview API to call this command along with any specified arguments.","type":"string"}},"required":["cmd","name"],"type":"object"},{"additionalProperties":false,"properties":{"args":{"allOf":[{"$ref":"#/definitions/ShellScopeEntryAllowedArgs"}],"description":"The allowed arguments for the command execution."},"name":{"description":"The name for this allowed shell command configuration.\n\nThis name will be used inside of the webview API to call this command along with any specified arguments.","type":"string"},"sidecar":{"description":"If this command is a sidecar command.","type":"boolean"}},"required":["name","sidecar"],"type":"object"}],"definitions":{"ShellScopeEntryAllowedArg":{"anyOf":[{"description":"A non-configurable argument that is passed to the command in the order it was specified.","type":"string"},{"additionalProperties":false,"description":"A variable that is set while calling the command from the webview API.","properties":{"raw":{"default":false,"description":"Marks the validator as a raw regex, meaning the plugin should not make any modification at runtime.\n\nThis means the regex will not match on the entire string by default, which might be exploited if your regex allow unexpected input to be considered valid. When using this option, make sure your regex is correct.","type":"boolean"},"validator":{"description":"[regex] validator to require passed values to conform to an expected input.\n\nThis will require the argument value passed to this variable to match the `validator` regex before it will be executed.\n\nThe regex string is by default surrounded by `^...$` to match the full string. For example the `https?://\\w+` regex would be registered as `^https?://\\w+$`.\n\n[regex]: <https://docs.rs/regex/latest/regex/#syntax>","type":"string"}},"required":["validator"],"type":"object"}],"description":"A command argument allowed to be executed by the webview API."},"ShellScopeEntryAllowedArgs":{"anyOf":[{"description":"Use a simple boolean to allow all or disable all arguments to this command configuration.","type":"boolean"},{"description":"A specific set of [`ShellScopeEntryAllowedArg`] that are valid to call for the command configuration.","items":{"$ref":"#/definitions/ShellScopeEntryAllowedArg"},"type":"array"}],"description":"A set of command arguments allowed to be executed by the webview API.\n\nA value of `true` will allow any arguments to be passed to the command. `false` will disable all arguments. A list of [`ShellScopeEntryAllowedArg`] will set those arguments as the only valid arguments to be passed to the attached command configuration."}},"description":"Shell scope entry.","title":"ShellScopeEntry"}},"window-state":{"default_permission":{"identifier":"default","description":"This permission set configures what kind of\noperations are available from the window state plugin.\n\n#### Granted Permissions\n\nAll operations are enabled by default.\n\n","permissions":["allow-filename","allow-restore-state","allow-save-window-state"]},"permissions":{"allow-filename":{"identifier":"allow-filename","description":"Enables the filename command without any pre-configured scope.","commands":{"allow":["filename"],"deny":[]}},"allow-restore-state":{"identifier":"allow-restore-state","description":"Enables the restore_state command without any pre-configured scope.","commands":{"allow":["restore_state"],"deny":[]}},"allow-save-window-state":{"identifier":"allow-save-window-state","description":"Enables the save_window_state command without any pre-configured scope.","commands":{"allow":["save_window_state"],"deny":[]}},"deny-filename":{"identifier":"deny-filename","description":"Denies the filename command without any pre-configured scope.","commands":{"allow":[],"deny":["filename"]}},"deny-restore-state":{"identifier":"deny-restore-state","description":"Denies the restore_state command without any pre-configured scope.","commands":{"allow":[],"deny":["restore_state"]}},"deny-save-window-state":{"identifier":"deny-save-window-state","description":"Denies the save_window_state command without any pre-configured scope.","commands":{"allow":[],"deny":["save_window_state"]}}},"permission_sets":{},"global_scope_schema":null}}
\ No newline at end of file
+{"clipboard-manager":{"default_permission":{"identifier":"default","description":"No features are enabled by default, as we believe\nthe clipboard can be inherently dangerous and it is \napplication specific if read and/or write access is needed.\n\nClipboard interaction needs to be explicitly enabled.\n","permissions":[]},"permissions":{"allow-clear":{"identifier":"allow-clear","description":"Enables the clear command without any pre-configured scope.","commands":{"allow":["clear"],"deny":[]}},"allow-read-image":{"identifier":"allow-read-image","description":"Enables the read_image command without any pre-configured scope.","commands":{"allow":["read_image"],"deny":[]}},"allow-read-text":{"identifier":"allow-read-text","description":"Enables the read_text command without any pre-configured scope.","commands":{"allow":["read_text"],"deny":[]}},"allow-write-html":{"identifier":"allow-write-html","description":"Enables the write_html command without any pre-configured scope.","commands":{"allow":["write_html"],"deny":[]}},"allow-write-image":{"identifier":"allow-write-image","description":"Enables the write_image command without any pre-configured scope.","commands":{"allow":["write_image"],"deny":[]}},"allow-write-text":{"identifier":"allow-write-text","description":"Enables the write_text command without any pre-configured scope.","commands":{"allow":["write_text"],"deny":[]}},"deny-clear":{"identifier":"deny-clear","description":"Denies the clear command without any pre-configured scope.","commands":{"allow":[],"deny":["clear"]}},"deny-read-image":{"identifier":"deny-read-image","description":"Denies the read_image command without any pre-configured scope.","commands":{"allow":[],"deny":["read_image"]}},"deny-read-text":{"identifier":"deny-read-text","description":"Denies the read_text command without any pre-configured scope.","commands":{"allow":[],"deny":["read_text"]}},"deny-write-html":{"identifier":"deny-write-html","description":"Denies the write_html command without any pre-configured scope.","commands":{"allow":[],"deny":["write_html"]}},"deny-write-image":{"identifier":"deny-write-image","description":"Denies the write_image command without any pre-configured scope.","commands":{"allow":[],"deny":["write_image"]}},"deny-write-text":{"identifier":"deny-write-text","description":"Denies the write_text command without any pre-configured scope.","commands":{"allow":[],"deny":["write_text"]}}},"permission_sets":{},"global_scope_schema":null},"core:app":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin.","permissions":["allow-version","allow-name","allow-tauri-version"]},"permissions":{"allow-app-hide":{"identifier":"allow-app-hide","description":"Enables the app_hide command without any pre-configured scope.","commands":{"allow":["app_hide"],"deny":[]}},"allow-app-show":{"identifier":"allow-app-show","description":"Enables the app_show command without any pre-configured scope.","commands":{"allow":["app_show"],"deny":[]}},"allow-default-window-icon":{"identifier":"allow-default-window-icon","description":"Enables the default_window_icon command without any pre-configured scope.","commands":{"allow":["default_window_icon"],"deny":[]}},"allow-name":{"identifier":"allow-name","description":"Enables the name command without any pre-configured scope.","commands":{"allow":["name"],"deny":[]}},"allow-tauri-version":{"identifier":"allow-tauri-version","description":"Enables the tauri_version command without any pre-configured scope.","commands":{"allow":["tauri_version"],"deny":[]}},"allow-version":{"identifier":"allow-version","description":"Enables the version command without any pre-configured scope.","commands":{"allow":["version"],"deny":[]}},"deny-app-hide":{"identifier":"deny-app-hide","description":"Denies the app_hide command without any pre-configured scope.","commands":{"allow":[],"deny":["app_hide"]}},"deny-app-show":{"identifier":"deny-app-show","description":"Denies the app_show command without any pre-configured scope.","commands":{"allow":[],"deny":["app_show"]}},"deny-default-window-icon":{"identifier":"deny-default-window-icon","description":"Denies the default_window_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["default_window_icon"]}},"deny-name":{"identifier":"deny-name","description":"Denies the name command without any pre-configured scope.","commands":{"allow":[],"deny":["name"]}},"deny-tauri-version":{"identifier":"deny-tauri-version","description":"Denies the tauri_version command without any pre-configured scope.","commands":{"allow":[],"deny":["tauri_version"]}},"deny-version":{"identifier":"deny-version","description":"Denies the version command without any pre-configured scope.","commands":{"allow":[],"deny":["version"]}}},"permission_sets":{},"global_scope_schema":null},"core:event":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin.","permissions":["allow-listen","allow-unlisten","allow-emit","allow-emit-to"]},"permissions":{"allow-emit":{"identifier":"allow-emit","description":"Enables the emit command without any pre-configured scope.","commands":{"allow":["emit"],"deny":[]}},"allow-emit-to":{"identifier":"allow-emit-to","description":"Enables the emit_to command without any pre-configured scope.","commands":{"allow":["emit_to"],"deny":[]}},"allow-listen":{"identifier":"allow-listen","description":"Enables the listen command without any pre-configured scope.","commands":{"allow":["listen"],"deny":[]}},"allow-unlisten":{"identifier":"allow-unlisten","description":"Enables the unlisten command without any pre-configured scope.","commands":{"allow":["unlisten"],"deny":[]}},"deny-emit":{"identifier":"deny-emit","description":"Denies the emit command without any pre-configured scope.","commands":{"allow":[],"deny":["emit"]}},"deny-emit-to":{"identifier":"deny-emit-to","description":"Denies the emit_to command without any pre-configured scope.","commands":{"allow":[],"deny":["emit_to"]}},"deny-listen":{"identifier":"deny-listen","description":"Denies the listen command without any pre-configured scope.","commands":{"allow":[],"deny":["listen"]}},"deny-unlisten":{"identifier":"deny-unlisten","description":"Denies the unlisten command without any pre-configured scope.","commands":{"allow":[],"deny":["unlisten"]}}},"permission_sets":{},"global_scope_schema":null},"core:image":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin.","permissions":["allow-new","allow-from-bytes","allow-from-path","allow-rgba","allow-size"]},"permissions":{"allow-from-bytes":{"identifier":"allow-from-bytes","description":"Enables the from_bytes command without any pre-configured scope.","commands":{"allow":["from_bytes"],"deny":[]}},"allow-from-path":{"identifier":"allow-from-path","description":"Enables the from_path command without any pre-configured scope.","commands":{"allow":["from_path"],"deny":[]}},"allow-new":{"identifier":"allow-new","description":"Enables the new command without any pre-configured scope.","commands":{"allow":["new"],"deny":[]}},"allow-rgba":{"identifier":"allow-rgba","description":"Enables the rgba command without any pre-configured scope.","commands":{"allow":["rgba"],"deny":[]}},"allow-size":{"identifier":"allow-size","description":"Enables the size command without any pre-configured scope.","commands":{"allow":["size"],"deny":[]}},"deny-from-bytes":{"identifier":"deny-from-bytes","description":"Denies the from_bytes command without any pre-configured scope.","commands":{"allow":[],"deny":["from_bytes"]}},"deny-from-path":{"identifier":"deny-from-path","description":"Denies the from_path command without any pre-configured scope.","commands":{"allow":[],"deny":["from_path"]}},"deny-new":{"identifier":"deny-new","description":"Denies the new command without any pre-configured scope.","commands":{"allow":[],"deny":["new"]}},"deny-rgba":{"identifier":"deny-rgba","description":"Denies the rgba command without any pre-configured scope.","commands":{"allow":[],"deny":["rgba"]}},"deny-size":{"identifier":"deny-size","description":"Denies the size command without any pre-configured scope.","commands":{"allow":[],"deny":["size"]}}},"permission_sets":{},"global_scope_schema":null},"core:menu":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin.","permissions":["allow-new","allow-append","allow-prepend","allow-insert","allow-remove","allow-remove-at","allow-items","allow-get","allow-popup","allow-create-default","allow-set-as-app-menu","allow-set-as-window-menu","allow-text","allow-set-text","allow-is-enabled","allow-set-enabled","allow-set-accelerator","allow-set-as-windows-menu-for-nsapp","allow-set-as-help-menu-for-nsapp","allow-is-checked","allow-set-checked","allow-set-icon"]},"permissions":{"allow-append":{"identifier":"allow-append","description":"Enables the append command without any pre-configured scope.","commands":{"allow":["append"],"deny":[]}},"allow-create-default":{"identifier":"allow-create-default","description":"Enables the create_default command without any pre-configured scope.","commands":{"allow":["create_default"],"deny":[]}},"allow-get":{"identifier":"allow-get","description":"Enables the get command without any pre-configured scope.","commands":{"allow":["get"],"deny":[]}},"allow-insert":{"identifier":"allow-insert","description":"Enables the insert command without any pre-configured scope.","commands":{"allow":["insert"],"deny":[]}},"allow-is-checked":{"identifier":"allow-is-checked","description":"Enables the is_checked command without any pre-configured scope.","commands":{"allow":["is_checked"],"deny":[]}},"allow-is-enabled":{"identifier":"allow-is-enabled","description":"Enables the is_enabled command without any pre-configured scope.","commands":{"allow":["is_enabled"],"deny":[]}},"allow-items":{"identifier":"allow-items","description":"Enables the items command without any pre-configured scope.","commands":{"allow":["items"],"deny":[]}},"allow-new":{"identifier":"allow-new","description":"Enables the new command without any pre-configured scope.","commands":{"allow":["new"],"deny":[]}},"allow-popup":{"identifier":"allow-popup","description":"Enables the popup command without any pre-configured scope.","commands":{"allow":["popup"],"deny":[]}},"allow-prepend":{"identifier":"allow-prepend","description":"Enables the prepend command without any pre-configured scope.","commands":{"allow":["prepend"],"deny":[]}},"allow-remove":{"identifier":"allow-remove","description":"Enables the remove command without any pre-configured scope.","commands":{"allow":["remove"],"deny":[]}},"allow-remove-at":{"identifier":"allow-remove-at","description":"Enables the remove_at command without any pre-configured scope.","commands":{"allow":["remove_at"],"deny":[]}},"allow-set-accelerator":{"identifier":"allow-set-accelerator","description":"Enables the set_accelerator command without any pre-configured scope.","commands":{"allow":["set_accelerator"],"deny":[]}},"allow-set-as-app-menu":{"identifier":"allow-set-as-app-menu","description":"Enables the set_as_app_menu command without any pre-configured scope.","commands":{"allow":["set_as_app_menu"],"deny":[]}},"allow-set-as-help-menu-for-nsapp":{"identifier":"allow-set-as-help-menu-for-nsapp","description":"Enables the set_as_help_menu_for_nsapp command without any pre-configured scope.","commands":{"allow":["set_as_help_menu_for_nsapp"],"deny":[]}},"allow-set-as-window-menu":{"identifier":"allow-set-as-window-menu","description":"Enables the set_as_window_menu command without any pre-configured scope.","commands":{"allow":["set_as_window_menu"],"deny":[]}},"allow-set-as-windows-menu-for-nsapp":{"identifier":"allow-set-as-windows-menu-for-nsapp","description":"Enables the set_as_windows_menu_for_nsapp command without any pre-configured scope.","commands":{"allow":["set_as_windows_menu_for_nsapp"],"deny":[]}},"allow-set-checked":{"identifier":"allow-set-checked","description":"Enables the set_checked command without any pre-configured scope.","commands":{"allow":["set_checked"],"deny":[]}},"allow-set-enabled":{"identifier":"allow-set-enabled","description":"Enables the set_enabled command without any pre-configured scope.","commands":{"allow":["set_enabled"],"deny":[]}},"allow-set-icon":{"identifier":"allow-set-icon","description":"Enables the set_icon command without any pre-configured scope.","commands":{"allow":["set_icon"],"deny":[]}},"allow-set-text":{"identifier":"allow-set-text","description":"Enables the set_text command without any pre-configured scope.","commands":{"allow":["set_text"],"deny":[]}},"allow-text":{"identifier":"allow-text","description":"Enables the text command without any pre-configured scope.","commands":{"allow":["text"],"deny":[]}},"deny-append":{"identifier":"deny-append","description":"Denies the append command without any pre-configured scope.","commands":{"allow":[],"deny":["append"]}},"deny-create-default":{"identifier":"deny-create-default","description":"Denies the create_default command without any pre-configured scope.","commands":{"allow":[],"deny":["create_default"]}},"deny-get":{"identifier":"deny-get","description":"Denies the get command without any pre-configured scope.","commands":{"allow":[],"deny":["get"]}},"deny-insert":{"identifier":"deny-insert","description":"Denies the insert command without any pre-configured scope.","commands":{"allow":[],"deny":["insert"]}},"deny-is-checked":{"identifier":"deny-is-checked","description":"Denies the is_checked command without any pre-configured scope.","commands":{"allow":[],"deny":["is_checked"]}},"deny-is-enabled":{"identifier":"deny-is-enabled","description":"Denies the is_enabled command without any pre-configured scope.","commands":{"allow":[],"deny":["is_enabled"]}},"deny-items":{"identifier":"deny-items","description":"Denies the items command without any pre-configured scope.","commands":{"allow":[],"deny":["items"]}},"deny-new":{"identifier":"deny-new","description":"Denies the new command without any pre-configured scope.","commands":{"allow":[],"deny":["new"]}},"deny-popup":{"identifier":"deny-popup","description":"Denies the popup command without any pre-configured scope.","commands":{"allow":[],"deny":["popup"]}},"deny-prepend":{"identifier":"deny-prepend","description":"Denies the prepend command without any pre-configured scope.","commands":{"allow":[],"deny":["prepend"]}},"deny-remove":{"identifier":"deny-remove","description":"Denies the remove command without any pre-configured scope.","commands":{"allow":[],"deny":["remove"]}},"deny-remove-at":{"identifier":"deny-remove-at","description":"Denies the remove_at command without any pre-configured scope.","commands":{"allow":[],"deny":["remove_at"]}},"deny-set-accelerator":{"identifier":"deny-set-accelerator","description":"Denies the set_accelerator command without any pre-configured scope.","commands":{"allow":[],"deny":["set_accelerator"]}},"deny-set-as-app-menu":{"identifier":"deny-set-as-app-menu","description":"Denies the set_as_app_menu command without any pre-configured scope.","commands":{"allow":[],"deny":["set_as_app_menu"]}},"deny-set-as-help-menu-for-nsapp":{"identifier":"deny-set-as-help-menu-for-nsapp","description":"Denies the set_as_help_menu_for_nsapp command without any pre-configured scope.","commands":{"allow":[],"deny":["set_as_help_menu_for_nsapp"]}},"deny-set-as-window-menu":{"identifier":"deny-set-as-window-menu","description":"Denies the set_as_window_menu command without any pre-configured scope.","commands":{"allow":[],"deny":["set_as_window_menu"]}},"deny-set-as-windows-menu-for-nsapp":{"identifier":"deny-set-as-windows-menu-for-nsapp","description":"Denies the set_as_windows_menu_for_nsapp command without any pre-configured scope.","commands":{"allow":[],"deny":["set_as_windows_menu_for_nsapp"]}},"deny-set-checked":{"identifier":"deny-set-checked","description":"Denies the set_checked command without any pre-configured scope.","commands":{"allow":[],"deny":["set_checked"]}},"deny-set-enabled":{"identifier":"deny-set-enabled","description":"Denies the set_enabled command without any pre-configured scope.","commands":{"allow":[],"deny":["set_enabled"]}},"deny-set-icon":{"identifier":"deny-set-icon","description":"Denies the set_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["set_icon"]}},"deny-set-text":{"identifier":"deny-set-text","description":"Denies the set_text command without any pre-configured scope.","commands":{"allow":[],"deny":["set_text"]}},"deny-text":{"identifier":"deny-text","description":"Denies the text command without any pre-configured scope.","commands":{"allow":[],"deny":["text"]}}},"permission_sets":{},"global_scope_schema":null},"core:path":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin.","permissions":["allow-resolve-directory","allow-resolve","allow-normalize","allow-join","allow-dirname","allow-extname","allow-basename","allow-is-absolute"]},"permissions":{"allow-basename":{"identifier":"allow-basename","description":"Enables the basename command without any pre-configured scope.","commands":{"allow":["basename"],"deny":[]}},"allow-dirname":{"identifier":"allow-dirname","description":"Enables the dirname command without any pre-configured scope.","commands":{"allow":["dirname"],"deny":[]}},"allow-extname":{"identifier":"allow-extname","description":"Enables the extname command without any pre-configured scope.","commands":{"allow":["extname"],"deny":[]}},"allow-is-absolute":{"identifier":"allow-is-absolute","description":"Enables the is_absolute command without any pre-configured scope.","commands":{"allow":["is_absolute"],"deny":[]}},"allow-join":{"identifier":"allow-join","description":"Enables the join command without any pre-configured scope.","commands":{"allow":["join"],"deny":[]}},"allow-normalize":{"identifier":"allow-normalize","description":"Enables the normalize command without any pre-configured scope.","commands":{"allow":["normalize"],"deny":[]}},"allow-resolve":{"identifier":"allow-resolve","description":"Enables the resolve command without any pre-configured scope.","commands":{"allow":["resolve"],"deny":[]}},"allow-resolve-directory":{"identifier":"allow-resolve-directory","description":"Enables the resolve_directory command without any pre-configured scope.","commands":{"allow":["resolve_directory"],"deny":[]}},"deny-basename":{"identifier":"deny-basename","description":"Denies the basename command without any pre-configured scope.","commands":{"allow":[],"deny":["basename"]}},"deny-dirname":{"identifier":"deny-dirname","description":"Denies the dirname command without any pre-configured scope.","commands":{"allow":[],"deny":["dirname"]}},"deny-extname":{"identifier":"deny-extname","description":"Denies the extname command without any pre-configured scope.","commands":{"allow":[],"deny":["extname"]}},"deny-is-absolute":{"identifier":"deny-is-absolute","description":"Denies the is_absolute command without any pre-configured scope.","commands":{"allow":[],"deny":["is_absolute"]}},"deny-join":{"identifier":"deny-join","description":"Denies the join command without any pre-configured scope.","commands":{"allow":[],"deny":["join"]}},"deny-normalize":{"identifier":"deny-normalize","description":"Denies the normalize command without any pre-configured scope.","commands":{"allow":[],"deny":["normalize"]}},"deny-resolve":{"identifier":"deny-resolve","description":"Denies the resolve command without any pre-configured scope.","commands":{"allow":[],"deny":["resolve"]}},"deny-resolve-directory":{"identifier":"deny-resolve-directory","description":"Denies the resolve_directory command without any pre-configured scope.","commands":{"allow":[],"deny":["resolve_directory"]}}},"permission_sets":{},"global_scope_schema":null},"core:resources":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin.","permissions":["allow-close"]},"permissions":{"allow-close":{"identifier":"allow-close","description":"Enables the close command without any pre-configured scope.","commands":{"allow":["close"],"deny":[]}},"deny-close":{"identifier":"deny-close","description":"Denies the close command without any pre-configured scope.","commands":{"allow":[],"deny":["close"]}}},"permission_sets":{},"global_scope_schema":null},"core:tray":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin.","permissions":["allow-new","allow-get-by-id","allow-remove-by-id","allow-set-icon","allow-set-menu","allow-set-tooltip","allow-set-title","allow-set-visible","allow-set-temp-dir-path","allow-set-icon-as-template","allow-set-show-menu-on-left-click"]},"permissions":{"allow-get-by-id":{"identifier":"allow-get-by-id","description":"Enables the get_by_id command without any pre-configured scope.","commands":{"allow":["get_by_id"],"deny":[]}},"allow-new":{"identifier":"allow-new","description":"Enables the new command without any pre-configured scope.","commands":{"allow":["new"],"deny":[]}},"allow-remove-by-id":{"identifier":"allow-remove-by-id","description":"Enables the remove_by_id command without any pre-configured scope.","commands":{"allow":["remove_by_id"],"deny":[]}},"allow-set-icon":{"identifier":"allow-set-icon","description":"Enables the set_icon command without any pre-configured scope.","commands":{"allow":["set_icon"],"deny":[]}},"allow-set-icon-as-template":{"identifier":"allow-set-icon-as-template","description":"Enables the set_icon_as_template command without any pre-configured scope.","commands":{"allow":["set_icon_as_template"],"deny":[]}},"allow-set-menu":{"identifier":"allow-set-menu","description":"Enables the set_menu command without any pre-configured scope.","commands":{"allow":["set_menu"],"deny":[]}},"allow-set-show-menu-on-left-click":{"identifier":"allow-set-show-menu-on-left-click","description":"Enables the set_show_menu_on_left_click command without any pre-configured scope.","commands":{"allow":["set_show_menu_on_left_click"],"deny":[]}},"allow-set-temp-dir-path":{"identifier":"allow-set-temp-dir-path","description":"Enables the set_temp_dir_path command without any pre-configured scope.","commands":{"allow":["set_temp_dir_path"],"deny":[]}},"allow-set-title":{"identifier":"allow-set-title","description":"Enables the set_title command without any pre-configured scope.","commands":{"allow":["set_title"],"deny":[]}},"allow-set-tooltip":{"identifier":"allow-set-tooltip","description":"Enables the set_tooltip command without any pre-configured scope.","commands":{"allow":["set_tooltip"],"deny":[]}},"allow-set-visible":{"identifier":"allow-set-visible","description":"Enables the set_visible command without any pre-configured scope.","commands":{"allow":["set_visible"],"deny":[]}},"deny-get-by-id":{"identifier":"deny-get-by-id","description":"Denies the get_by_id command without any pre-configured scope.","commands":{"allow":[],"deny":["get_by_id"]}},"deny-new":{"identifier":"deny-new","description":"Denies the new command without any pre-configured scope.","commands":{"allow":[],"deny":["new"]}},"deny-remove-by-id":{"identifier":"deny-remove-by-id","description":"Denies the remove_by_id command without any pre-configured scope.","commands":{"allow":[],"deny":["remove_by_id"]}},"deny-set-icon":{"identifier":"deny-set-icon","description":"Denies the set_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["set_icon"]}},"deny-set-icon-as-template":{"identifier":"deny-set-icon-as-template","description":"Denies the set_icon_as_template command without any pre-configured scope.","commands":{"allow":[],"deny":["set_icon_as_template"]}},"deny-set-menu":{"identifier":"deny-set-menu","description":"Denies the set_menu command without any pre-configured scope.","commands":{"allow":[],"deny":["set_menu"]}},"deny-set-show-menu-on-left-click":{"identifier":"deny-set-show-menu-on-left-click","description":"Denies the set_show_menu_on_left_click command without any pre-configured scope.","commands":{"allow":[],"deny":["set_show_menu_on_left_click"]}},"deny-set-temp-dir-path":{"identifier":"deny-set-temp-dir-path","description":"Denies the set_temp_dir_path command without any pre-configured scope.","commands":{"allow":[],"deny":["set_temp_dir_path"]}},"deny-set-title":{"identifier":"deny-set-title","description":"Denies the set_title command without any pre-configured scope.","commands":{"allow":[],"deny":["set_title"]}},"deny-set-tooltip":{"identifier":"deny-set-tooltip","description":"Denies the set_tooltip command without any pre-configured scope.","commands":{"allow":[],"deny":["set_tooltip"]}},"deny-set-visible":{"identifier":"deny-set-visible","description":"Denies the set_visible command without any pre-configured scope.","commands":{"allow":[],"deny":["set_visible"]}}},"permission_sets":{},"global_scope_schema":null},"core:webview":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin.","permissions":["allow-get-all-webviews","allow-webview-position","allow-webview-size","allow-internal-toggle-devtools"]},"permissions":{"allow-create-webview":{"identifier":"allow-create-webview","description":"Enables the create_webview command without any pre-configured scope.","commands":{"allow":["create_webview"],"deny":[]}},"allow-create-webview-window":{"identifier":"allow-create-webview-window","description":"Enables the create_webview_window command without any pre-configured scope.","commands":{"allow":["create_webview_window"],"deny":[]}},"allow-get-all-webviews":{"identifier":"allow-get-all-webviews","description":"Enables the get_all_webviews command without any pre-configured scope.","commands":{"allow":["get_all_webviews"],"deny":[]}},"allow-internal-toggle-devtools":{"identifier":"allow-internal-toggle-devtools","description":"Enables the internal_toggle_devtools command without any pre-configured scope.","commands":{"allow":["internal_toggle_devtools"],"deny":[]}},"allow-print":{"identifier":"allow-print","description":"Enables the print command without any pre-configured scope.","commands":{"allow":["print"],"deny":[]}},"allow-reparent":{"identifier":"allow-reparent","description":"Enables the reparent command without any pre-configured scope.","commands":{"allow":["reparent"],"deny":[]}},"allow-set-webview-focus":{"identifier":"allow-set-webview-focus","description":"Enables the set_webview_focus command without any pre-configured scope.","commands":{"allow":["set_webview_focus"],"deny":[]}},"allow-set-webview-position":{"identifier":"allow-set-webview-position","description":"Enables the set_webview_position command without any pre-configured scope.","commands":{"allow":["set_webview_position"],"deny":[]}},"allow-set-webview-size":{"identifier":"allow-set-webview-size","description":"Enables the set_webview_size command without any pre-configured scope.","commands":{"allow":["set_webview_size"],"deny":[]}},"allow-set-webview-zoom":{"identifier":"allow-set-webview-zoom","description":"Enables the set_webview_zoom command without any pre-configured scope.","commands":{"allow":["set_webview_zoom"],"deny":[]}},"allow-webview-close":{"identifier":"allow-webview-close","description":"Enables the webview_close command without any pre-configured scope.","commands":{"allow":["webview_close"],"deny":[]}},"allow-webview-position":{"identifier":"allow-webview-position","description":"Enables the webview_position command without any pre-configured scope.","commands":{"allow":["webview_position"],"deny":[]}},"allow-webview-size":{"identifier":"allow-webview-size","description":"Enables the webview_size command without any pre-configured scope.","commands":{"allow":["webview_size"],"deny":[]}},"deny-create-webview":{"identifier":"deny-create-webview","description":"Denies the create_webview command without any pre-configured scope.","commands":{"allow":[],"deny":["create_webview"]}},"deny-create-webview-window":{"identifier":"deny-create-webview-window","description":"Denies the create_webview_window command without any pre-configured scope.","commands":{"allow":[],"deny":["create_webview_window"]}},"deny-get-all-webviews":{"identifier":"deny-get-all-webviews","description":"Denies the get_all_webviews command without any pre-configured scope.","commands":{"allow":[],"deny":["get_all_webviews"]}},"deny-internal-toggle-devtools":{"identifier":"deny-internal-toggle-devtools","description":"Denies the internal_toggle_devtools command without any pre-configured scope.","commands":{"allow":[],"deny":["internal_toggle_devtools"]}},"deny-print":{"identifier":"deny-print","description":"Denies the print command without any pre-configured scope.","commands":{"allow":[],"deny":["print"]}},"deny-reparent":{"identifier":"deny-reparent","description":"Denies the reparent command without any pre-configured scope.","commands":{"allow":[],"deny":["reparent"]}},"deny-set-webview-focus":{"identifier":"deny-set-webview-focus","description":"Denies the set_webview_focus command without any pre-configured scope.","commands":{"allow":[],"deny":["set_webview_focus"]}},"deny-set-webview-position":{"identifier":"deny-set-webview-position","description":"Denies the set_webview_position command without any pre-configured scope.","commands":{"allow":[],"deny":["set_webview_position"]}},"deny-set-webview-size":{"identifier":"deny-set-webview-size","description":"Denies the set_webview_size command without any pre-configured scope.","commands":{"allow":[],"deny":["set_webview_size"]}},"deny-set-webview-zoom":{"identifier":"deny-set-webview-zoom","description":"Denies the set_webview_zoom command without any pre-configured scope.","commands":{"allow":[],"deny":["set_webview_zoom"]}},"deny-webview-close":{"identifier":"deny-webview-close","description":"Denies the webview_close command without any pre-configured scope.","commands":{"allow":[],"deny":["webview_close"]}},"deny-webview-position":{"identifier":"deny-webview-position","description":"Denies the webview_position command without any pre-configured scope.","commands":{"allow":[],"deny":["webview_position"]}},"deny-webview-size":{"identifier":"deny-webview-size","description":"Denies the webview_size command without any pre-configured scope.","commands":{"allow":[],"deny":["webview_size"]}}},"permission_sets":{},"global_scope_schema":null},"core:window":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin.","permissions":["allow-get-all-windows","allow-scale-factor","allow-inner-position","allow-outer-position","allow-inner-size","allow-outer-size","allow-is-fullscreen","allow-is-minimized","allow-is-maximized","allow-is-focused","allow-is-decorated","allow-is-resizable","allow-is-maximizable","allow-is-minimizable","allow-is-closable","allow-is-visible","allow-title","allow-current-monitor","allow-primary-monitor","allow-monitor-from-point","allow-available-monitors","allow-cursor-position","allow-theme","allow-internal-toggle-maximize"]},"permissions":{"allow-available-monitors":{"identifier":"allow-available-monitors","description":"Enables the available_monitors command without any pre-configured scope.","commands":{"allow":["available_monitors"],"deny":[]}},"allow-center":{"identifier":"allow-center","description":"Enables the center command without any pre-configured scope.","commands":{"allow":["center"],"deny":[]}},"allow-close":{"identifier":"allow-close","description":"Enables the close command without any pre-configured scope.","commands":{"allow":["close"],"deny":[]}},"allow-create":{"identifier":"allow-create","description":"Enables the create command without any pre-configured scope.","commands":{"allow":["create"],"deny":[]}},"allow-current-monitor":{"identifier":"allow-current-monitor","description":"Enables the current_monitor command without any pre-configured scope.","commands":{"allow":["current_monitor"],"deny":[]}},"allow-cursor-position":{"identifier":"allow-cursor-position","description":"Enables the cursor_position command without any pre-configured scope.","commands":{"allow":["cursor_position"],"deny":[]}},"allow-destroy":{"identifier":"allow-destroy","description":"Enables the destroy command without any pre-configured scope.","commands":{"allow":["destroy"],"deny":[]}},"allow-get-all-windows":{"identifier":"allow-get-all-windows","description":"Enables the get_all_windows command without any pre-configured scope.","commands":{"allow":["get_all_windows"],"deny":[]}},"allow-hide":{"identifier":"allow-hide","description":"Enables the hide command without any pre-configured scope.","commands":{"allow":["hide"],"deny":[]}},"allow-inner-position":{"identifier":"allow-inner-position","description":"Enables the inner_position command without any pre-configured scope.","commands":{"allow":["inner_position"],"deny":[]}},"allow-inner-size":{"identifier":"allow-inner-size","description":"Enables the inner_size command without any pre-configured scope.","commands":{"allow":["inner_size"],"deny":[]}},"allow-internal-toggle-maximize":{"identifier":"allow-internal-toggle-maximize","description":"Enables the internal_toggle_maximize command without any pre-configured scope.","commands":{"allow":["internal_toggle_maximize"],"deny":[]}},"allow-is-closable":{"identifier":"allow-is-closable","description":"Enables the is_closable command without any pre-configured scope.","commands":{"allow":["is_closable"],"deny":[]}},"allow-is-decorated":{"identifier":"allow-is-decorated","description":"Enables the is_decorated command without any pre-configured scope.","commands":{"allow":["is_decorated"],"deny":[]}},"allow-is-focused":{"identifier":"allow-is-focused","description":"Enables the is_focused command without any pre-configured scope.","commands":{"allow":["is_focused"],"deny":[]}},"allow-is-fullscreen":{"identifier":"allow-is-fullscreen","description":"Enables the is_fullscreen command without any pre-configured scope.","commands":{"allow":["is_fullscreen"],"deny":[]}},"allow-is-maximizable":{"identifier":"allow-is-maximizable","description":"Enables the is_maximizable command without any pre-configured scope.","commands":{"allow":["is_maximizable"],"deny":[]}},"allow-is-maximized":{"identifier":"allow-is-maximized","description":"Enables the is_maximized command without any pre-configured scope.","commands":{"allow":["is_maximized"],"deny":[]}},"allow-is-minimizable":{"identifier":"allow-is-minimizable","description":"Enables the is_minimizable command without any pre-configured scope.","commands":{"allow":["is_minimizable"],"deny":[]}},"allow-is-minimized":{"identifier":"allow-is-minimized","description":"Enables the is_minimized command without any pre-configured scope.","commands":{"allow":["is_minimized"],"deny":[]}},"allow-is-resizable":{"identifier":"allow-is-resizable","description":"Enables the is_resizable command without any pre-configured scope.","commands":{"allow":["is_resizable"],"deny":[]}},"allow-is-visible":{"identifier":"allow-is-visible","description":"Enables the is_visible command without any pre-configured scope.","commands":{"allow":["is_visible"],"deny":[]}},"allow-maximize":{"identifier":"allow-maximize","description":"Enables the maximize command without any pre-configured scope.","commands":{"allow":["maximize"],"deny":[]}},"allow-minimize":{"identifier":"allow-minimize","description":"Enables the minimize command without any pre-configured scope.","commands":{"allow":["minimize"],"deny":[]}},"allow-monitor-from-point":{"identifier":"allow-monitor-from-point","description":"Enables the monitor_from_point command without any pre-configured scope.","commands":{"allow":["monitor_from_point"],"deny":[]}},"allow-outer-position":{"identifier":"allow-outer-position","description":"Enables the outer_position command without any pre-configured scope.","commands":{"allow":["outer_position"],"deny":[]}},"allow-outer-size":{"identifier":"allow-outer-size","description":"Enables the outer_size command without any pre-configured scope.","commands":{"allow":["outer_size"],"deny":[]}},"allow-primary-monitor":{"identifier":"allow-primary-monitor","description":"Enables the primary_monitor command without any pre-configured scope.","commands":{"allow":["primary_monitor"],"deny":[]}},"allow-request-user-attention":{"identifier":"allow-request-user-attention","description":"Enables the request_user_attention command without any pre-configured scope.","commands":{"allow":["request_user_attention"],"deny":[]}},"allow-scale-factor":{"identifier":"allow-scale-factor","description":"Enables the scale_factor command without any pre-configured scope.","commands":{"allow":["scale_factor"],"deny":[]}},"allow-set-always-on-bottom":{"identifier":"allow-set-always-on-bottom","description":"Enables the set_always_on_bottom command without any pre-configured scope.","commands":{"allow":["set_always_on_bottom"],"deny":[]}},"allow-set-always-on-top":{"identifier":"allow-set-always-on-top","description":"Enables the set_always_on_top command without any pre-configured scope.","commands":{"allow":["set_always_on_top"],"deny":[]}},"allow-set-closable":{"identifier":"allow-set-closable","description":"Enables the set_closable command without any pre-configured scope.","commands":{"allow":["set_closable"],"deny":[]}},"allow-set-content-protected":{"identifier":"allow-set-content-protected","description":"Enables the set_content_protected command without any pre-configured scope.","commands":{"allow":["set_content_protected"],"deny":[]}},"allow-set-cursor-grab":{"identifier":"allow-set-cursor-grab","description":"Enables the set_cursor_grab command without any pre-configured scope.","commands":{"allow":["set_cursor_grab"],"deny":[]}},"allow-set-cursor-icon":{"identifier":"allow-set-cursor-icon","description":"Enables the set_cursor_icon command without any pre-configured scope.","commands":{"allow":["set_cursor_icon"],"deny":[]}},"allow-set-cursor-position":{"identifier":"allow-set-cursor-position","description":"Enables the set_cursor_position command without any pre-configured scope.","commands":{"allow":["set_cursor_position"],"deny":[]}},"allow-set-cursor-visible":{"identifier":"allow-set-cursor-visible","description":"Enables the set_cursor_visible command without any pre-configured scope.","commands":{"allow":["set_cursor_visible"],"deny":[]}},"allow-set-decorations":{"identifier":"allow-set-decorations","description":"Enables the set_decorations command without any pre-configured scope.","commands":{"allow":["set_decorations"],"deny":[]}},"allow-set-effects":{"identifier":"allow-set-effects","description":"Enables the set_effects command without any pre-configured scope.","commands":{"allow":["set_effects"],"deny":[]}},"allow-set-focus":{"identifier":"allow-set-focus","description":"Enables the set_focus command without any pre-configured scope.","commands":{"allow":["set_focus"],"deny":[]}},"allow-set-fullscreen":{"identifier":"allow-set-fullscreen","description":"Enables the set_fullscreen command without any pre-configured scope.","commands":{"allow":["set_fullscreen"],"deny":[]}},"allow-set-icon":{"identifier":"allow-set-icon","description":"Enables the set_icon command without any pre-configured scope.","commands":{"allow":["set_icon"],"deny":[]}},"allow-set-ignore-cursor-events":{"identifier":"allow-set-ignore-cursor-events","description":"Enables the set_ignore_cursor_events command without any pre-configured scope.","commands":{"allow":["set_ignore_cursor_events"],"deny":[]}},"allow-set-max-size":{"identifier":"allow-set-max-size","description":"Enables the set_max_size command without any pre-configured scope.","commands":{"allow":["set_max_size"],"deny":[]}},"allow-set-maximizable":{"identifier":"allow-set-maximizable","description":"Enables the set_maximizable command without any pre-configured scope.","commands":{"allow":["set_maximizable"],"deny":[]}},"allow-set-min-size":{"identifier":"allow-set-min-size","description":"Enables the set_min_size command without any pre-configured scope.","commands":{"allow":["set_min_size"],"deny":[]}},"allow-set-minimizable":{"identifier":"allow-set-minimizable","description":"Enables the set_minimizable command without any pre-configured scope.","commands":{"allow":["set_minimizable"],"deny":[]}},"allow-set-position":{"identifier":"allow-set-position","description":"Enables the set_position command without any pre-configured scope.","commands":{"allow":["set_position"],"deny":[]}},"allow-set-progress-bar":{"identifier":"allow-set-progress-bar","description":"Enables the set_progress_bar command without any pre-configured scope.","commands":{"allow":["set_progress_bar"],"deny":[]}},"allow-set-resizable":{"identifier":"allow-set-resizable","description":"Enables the set_resizable command without any pre-configured scope.","commands":{"allow":["set_resizable"],"deny":[]}},"allow-set-shadow":{"identifier":"allow-set-shadow","description":"Enables the set_shadow command without any pre-configured scope.","commands":{"allow":["set_shadow"],"deny":[]}},"allow-set-size":{"identifier":"allow-set-size","description":"Enables the set_size command without any pre-configured scope.","commands":{"allow":["set_size"],"deny":[]}},"allow-set-size-constraints":{"identifier":"allow-set-size-constraints","description":"Enables the set_size_constraints command without any pre-configured scope.","commands":{"allow":["set_size_constraints"],"deny":[]}},"allow-set-skip-taskbar":{"identifier":"allow-set-skip-taskbar","description":"Enables the set_skip_taskbar command without any pre-configured scope.","commands":{"allow":["set_skip_taskbar"],"deny":[]}},"allow-set-title":{"identifier":"allow-set-title","description":"Enables the set_title command without any pre-configured scope.","commands":{"allow":["set_title"],"deny":[]}},"allow-set-title-bar-style":{"identifier":"allow-set-title-bar-style","description":"Enables the set_title_bar_style command without any pre-configured scope.","commands":{"allow":["set_title_bar_style"],"deny":[]}},"allow-set-visible-on-all-workspaces":{"identifier":"allow-set-visible-on-all-workspaces","description":"Enables the set_visible_on_all_workspaces command without any pre-configured scope.","commands":{"allow":["set_visible_on_all_workspaces"],"deny":[]}},"allow-show":{"identifier":"allow-show","description":"Enables the show command without any pre-configured scope.","commands":{"allow":["show"],"deny":[]}},"allow-start-dragging":{"identifier":"allow-start-dragging","description":"Enables the start_dragging command without any pre-configured scope.","commands":{"allow":["start_dragging"],"deny":[]}},"allow-start-resize-dragging":{"identifier":"allow-start-resize-dragging","description":"Enables the start_resize_dragging command without any pre-configured scope.","commands":{"allow":["start_resize_dragging"],"deny":[]}},"allow-theme":{"identifier":"allow-theme","description":"Enables the theme command without any pre-configured scope.","commands":{"allow":["theme"],"deny":[]}},"allow-title":{"identifier":"allow-title","description":"Enables the title command without any pre-configured scope.","commands":{"allow":["title"],"deny":[]}},"allow-toggle-maximize":{"identifier":"allow-toggle-maximize","description":"Enables the toggle_maximize command without any pre-configured scope.","commands":{"allow":["toggle_maximize"],"deny":[]}},"allow-unmaximize":{"identifier":"allow-unmaximize","description":"Enables the unmaximize command without any pre-configured scope.","commands":{"allow":["unmaximize"],"deny":[]}},"allow-unminimize":{"identifier":"allow-unminimize","description":"Enables the unminimize command without any pre-configured scope.","commands":{"allow":["unminimize"],"deny":[]}},"deny-available-monitors":{"identifier":"deny-available-monitors","description":"Denies the available_monitors command without any pre-configured scope.","commands":{"allow":[],"deny":["available_monitors"]}},"deny-center":{"identifier":"deny-center","description":"Denies the center command without any pre-configured scope.","commands":{"allow":[],"deny":["center"]}},"deny-close":{"identifier":"deny-close","description":"Denies the close command without any pre-configured scope.","commands":{"allow":[],"deny":["close"]}},"deny-create":{"identifier":"deny-create","description":"Denies the create command without any pre-configured scope.","commands":{"allow":[],"deny":["create"]}},"deny-current-monitor":{"identifier":"deny-current-monitor","description":"Denies the current_monitor command without any pre-configured scope.","commands":{"allow":[],"deny":["current_monitor"]}},"deny-cursor-position":{"identifier":"deny-cursor-position","description":"Denies the cursor_position command without any pre-configured scope.","commands":{"allow":[],"deny":["cursor_position"]}},"deny-destroy":{"identifier":"deny-destroy","description":"Denies the destroy command without any pre-configured scope.","commands":{"allow":[],"deny":["destroy"]}},"deny-get-all-windows":{"identifier":"deny-get-all-windows","description":"Denies the get_all_windows command without any pre-configured scope.","commands":{"allow":[],"deny":["get_all_windows"]}},"deny-hide":{"identifier":"deny-hide","description":"Denies the hide command without any pre-configured scope.","commands":{"allow":[],"deny":["hide"]}},"deny-inner-position":{"identifier":"deny-inner-position","description":"Denies the inner_position command without any pre-configured scope.","commands":{"allow":[],"deny":["inner_position"]}},"deny-inner-size":{"identifier":"deny-inner-size","description":"Denies the inner_size command without any pre-configured scope.","commands":{"allow":[],"deny":["inner_size"]}},"deny-internal-toggle-maximize":{"identifier":"deny-internal-toggle-maximize","description":"Denies the internal_toggle_maximize command without any pre-configured scope.","commands":{"allow":[],"deny":["internal_toggle_maximize"]}},"deny-is-closable":{"identifier":"deny-is-closable","description":"Denies the is_closable command without any pre-configured scope.","commands":{"allow":[],"deny":["is_closable"]}},"deny-is-decorated":{"identifier":"deny-is-decorated","description":"Denies the is_decorated command without any pre-configured scope.","commands":{"allow":[],"deny":["is_decorated"]}},"deny-is-focused":{"identifier":"deny-is-focused","description":"Denies the is_focused command without any pre-configured scope.","commands":{"allow":[],"deny":["is_focused"]}},"deny-is-fullscreen":{"identifier":"deny-is-fullscreen","description":"Denies the is_fullscreen command without any pre-configured scope.","commands":{"allow":[],"deny":["is_fullscreen"]}},"deny-is-maximizable":{"identifier":"deny-is-maximizable","description":"Denies the is_maximizable command without any pre-configured scope.","commands":{"allow":[],"deny":["is_maximizable"]}},"deny-is-maximized":{"identifier":"deny-is-maximized","description":"Denies the is_maximized command without any pre-configured scope.","commands":{"allow":[],"deny":["is_maximized"]}},"deny-is-minimizable":{"identifier":"deny-is-minimizable","description":"Denies the is_minimizable command without any pre-configured scope.","commands":{"allow":[],"deny":["is_minimizable"]}},"deny-is-minimized":{"identifier":"deny-is-minimized","description":"Denies the is_minimized command without any pre-configured scope.","commands":{"allow":[],"deny":["is_minimized"]}},"deny-is-resizable":{"identifier":"deny-is-resizable","description":"Denies the is_resizable command without any pre-configured scope.","commands":{"allow":[],"deny":["is_resizable"]}},"deny-is-visible":{"identifier":"deny-is-visible","description":"Denies the is_visible command without any pre-configured scope.","commands":{"allow":[],"deny":["is_visible"]}},"deny-maximize":{"identifier":"deny-maximize","description":"Denies the maximize command without any pre-configured scope.","commands":{"allow":[],"deny":["maximize"]}},"deny-minimize":{"identifier":"deny-minimize","description":"Denies the minimize command without any pre-configured scope.","commands":{"allow":[],"deny":["minimize"]}},"deny-monitor-from-point":{"identifier":"deny-monitor-from-point","description":"Denies the monitor_from_point command without any pre-configured scope.","commands":{"allow":[],"deny":["monitor_from_point"]}},"deny-outer-position":{"identifier":"deny-outer-position","description":"Denies the outer_position command without any pre-configured scope.","commands":{"allow":[],"deny":["outer_position"]}},"deny-outer-size":{"identifier":"deny-outer-size","description":"Denies the outer_size command without any pre-configured scope.","commands":{"allow":[],"deny":["outer_size"]}},"deny-primary-monitor":{"identifier":"deny-primary-monitor","description":"Denies the primary_monitor command without any pre-configured scope.","commands":{"allow":[],"deny":["primary_monitor"]}},"deny-request-user-attention":{"identifier":"deny-request-user-attention","description":"Denies the request_user_attention command without any pre-configured scope.","commands":{"allow":[],"deny":["request_user_attention"]}},"deny-scale-factor":{"identifier":"deny-scale-factor","description":"Denies the scale_factor command without any pre-configured scope.","commands":{"allow":[],"deny":["scale_factor"]}},"deny-set-always-on-bottom":{"identifier":"deny-set-always-on-bottom","description":"Denies the set_always_on_bottom command without any pre-configured scope.","commands":{"allow":[],"deny":["set_always_on_bottom"]}},"deny-set-always-on-top":{"identifier":"deny-set-always-on-top","description":"Denies the set_always_on_top command without any pre-configured scope.","commands":{"allow":[],"deny":["set_always_on_top"]}},"deny-set-closable":{"identifier":"deny-set-closable","description":"Denies the set_closable command without any pre-configured scope.","commands":{"allow":[],"deny":["set_closable"]}},"deny-set-content-protected":{"identifier":"deny-set-content-protected","description":"Denies the set_content_protected command without any pre-configured scope.","commands":{"allow":[],"deny":["set_content_protected"]}},"deny-set-cursor-grab":{"identifier":"deny-set-cursor-grab","description":"Denies the set_cursor_grab command without any pre-configured scope.","commands":{"allow":[],"deny":["set_cursor_grab"]}},"deny-set-cursor-icon":{"identifier":"deny-set-cursor-icon","description":"Denies the set_cursor_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["set_cursor_icon"]}},"deny-set-cursor-position":{"identifier":"deny-set-cursor-position","description":"Denies the set_cursor_position command without any pre-configured scope.","commands":{"allow":[],"deny":["set_cursor_position"]}},"deny-set-cursor-visible":{"identifier":"deny-set-cursor-visible","description":"Denies the set_cursor_visible command without any pre-configured scope.","commands":{"allow":[],"deny":["set_cursor_visible"]}},"deny-set-decorations":{"identifier":"deny-set-decorations","description":"Denies the set_decorations command without any pre-configured scope.","commands":{"allow":[],"deny":["set_decorations"]}},"deny-set-effects":{"identifier":"deny-set-effects","description":"Denies the set_effects command without any pre-configured scope.","commands":{"allow":[],"deny":["set_effects"]}},"deny-set-focus":{"identifier":"deny-set-focus","description":"Denies the set_focus command without any pre-configured scope.","commands":{"allow":[],"deny":["set_focus"]}},"deny-set-fullscreen":{"identifier":"deny-set-fullscreen","description":"Denies the set_fullscreen command without any pre-configured scope.","commands":{"allow":[],"deny":["set_fullscreen"]}},"deny-set-icon":{"identifier":"deny-set-icon","description":"Denies the set_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["set_icon"]}},"deny-set-ignore-cursor-events":{"identifier":"deny-set-ignore-cursor-events","description":"Denies the set_ignore_cursor_events command without any pre-configured scope.","commands":{"allow":[],"deny":["set_ignore_cursor_events"]}},"deny-set-max-size":{"identifier":"deny-set-max-size","description":"Denies the set_max_size command without any pre-configured scope.","commands":{"allow":[],"deny":["set_max_size"]}},"deny-set-maximizable":{"identifier":"deny-set-maximizable","description":"Denies the set_maximizable command without any pre-configured scope.","commands":{"allow":[],"deny":["set_maximizable"]}},"deny-set-min-size":{"identifier":"deny-set-min-size","description":"Denies the set_min_size command without any pre-configured scope.","commands":{"allow":[],"deny":["set_min_size"]}},"deny-set-minimizable":{"identifier":"deny-set-minimizable","description":"Denies the set_minimizable command without any pre-configured scope.","commands":{"allow":[],"deny":["set_minimizable"]}},"deny-set-position":{"identifier":"deny-set-position","description":"Denies the set_position command without any pre-configured scope.","commands":{"allow":[],"deny":["set_position"]}},"deny-set-progress-bar":{"identifier":"deny-set-progress-bar","description":"Denies the set_progress_bar command without any pre-configured scope.","commands":{"allow":[],"deny":["set_progress_bar"]}},"deny-set-resizable":{"identifier":"deny-set-resizable","description":"Denies the set_resizable command without any pre-configured scope.","commands":{"allow":[],"deny":["set_resizable"]}},"deny-set-shadow":{"identifier":"deny-set-shadow","description":"Denies the set_shadow command without any pre-configured scope.","commands":{"allow":[],"deny":["set_shadow"]}},"deny-set-size":{"identifier":"deny-set-size","description":"Denies the set_size command without any pre-configured scope.","commands":{"allow":[],"deny":["set_size"]}},"deny-set-size-constraints":{"identifier":"deny-set-size-constraints","description":"Denies the set_size_constraints command without any pre-configured scope.","commands":{"allow":[],"deny":["set_size_constraints"]}},"deny-set-skip-taskbar":{"identifier":"deny-set-skip-taskbar","description":"Denies the set_skip_taskbar command without any pre-configured scope.","commands":{"allow":[],"deny":["set_skip_taskbar"]}},"deny-set-title":{"identifier":"deny-set-title","description":"Denies the set_title command without any pre-configured scope.","commands":{"allow":[],"deny":["set_title"]}},"deny-set-title-bar-style":{"identifier":"deny-set-title-bar-style","description":"Denies the set_title_bar_style command without any pre-configured scope.","commands":{"allow":[],"deny":["set_title_bar_style"]}},"deny-set-visible-on-all-workspaces":{"identifier":"deny-set-visible-on-all-workspaces","description":"Denies the set_visible_on_all_workspaces command without any pre-configured scope.","commands":{"allow":[],"deny":["set_visible_on_all_workspaces"]}},"deny-show":{"identifier":"deny-show","description":"Denies the show command without any pre-configured scope.","commands":{"allow":[],"deny":["show"]}},"deny-start-dragging":{"identifier":"deny-start-dragging","description":"Denies the start_dragging command without any pre-configured scope.","commands":{"allow":[],"deny":["start_dragging"]}},"deny-start-resize-dragging":{"identifier":"deny-start-resize-dragging","description":"Denies the start_resize_dragging command without any pre-configured scope.","commands":{"allow":[],"deny":["start_resize_dragging"]}},"deny-theme":{"identifier":"deny-theme","description":"Denies the theme command without any pre-configured scope.","commands":{"allow":[],"deny":["theme"]}},"deny-title":{"identifier":"deny-title","description":"Denies the title command without any pre-configured scope.","commands":{"allow":[],"deny":["title"]}},"deny-toggle-maximize":{"identifier":"deny-toggle-maximize","description":"Denies the toggle_maximize command without any pre-configured scope.","commands":{"allow":[],"deny":["toggle_maximize"]}},"deny-unmaximize":{"identifier":"deny-unmaximize","description":"Denies the unmaximize command without any pre-configured scope.","commands":{"allow":[],"deny":["unmaximize"]}},"deny-unminimize":{"identifier":"deny-unminimize","description":"Denies the unminimize command without any pre-configured scope.","commands":{"allow":[],"deny":["unminimize"]}}},"permission_sets":{},"global_scope_schema":null},"dialog":{"default_permission":{"identifier":"default","description":"This permission set configures the types of dialogs\navailable from the dialog plugin.\n\n#### Granted Permissions\n\nAll dialog types are enabled.\n\n\n","permissions":["allow-ask","allow-confirm","allow-message","allow-save","allow-open"]},"permissions":{"allow-ask":{"identifier":"allow-ask","description":"Enables the ask command without any pre-configured scope.","commands":{"allow":["ask"],"deny":[]}},"allow-confirm":{"identifier":"allow-confirm","description":"Enables the confirm command without any pre-configured scope.","commands":{"allow":["confirm"],"deny":[]}},"allow-message":{"identifier":"allow-message","description":"Enables the message command without any pre-configured scope.","commands":{"allow":["message"],"deny":[]}},"allow-open":{"identifier":"allow-open","description":"Enables the open command without any pre-configured scope.","commands":{"allow":["open"],"deny":[]}},"allow-save":{"identifier":"allow-save","description":"Enables the save command without any pre-configured scope.","commands":{"allow":["save"],"deny":[]}},"deny-ask":{"identifier":"deny-ask","description":"Denies the ask command without any pre-configured scope.","commands":{"allow":[],"deny":["ask"]}},"deny-confirm":{"identifier":"deny-confirm","description":"Denies the confirm command without any pre-configured scope.","commands":{"allow":[],"deny":["confirm"]}},"deny-message":{"identifier":"deny-message","description":"Denies the message command without any pre-configured scope.","commands":{"allow":[],"deny":["message"]}},"deny-open":{"identifier":"deny-open","description":"Denies the open command without any pre-configured scope.","commands":{"allow":[],"deny":["open"]}},"deny-save":{"identifier":"deny-save","description":"Denies the save command without any pre-configured scope.","commands":{"allow":[],"deny":["save"]}}},"permission_sets":{},"global_scope_schema":null},"log":{"default_permission":{"identifier":"default","description":"Allows the log command","permissions":["allow-log"]},"permissions":{"allow-log":{"identifier":"allow-log","description":"Enables the log command without any pre-configured scope.","commands":{"allow":["log"],"deny":[]}},"deny-log":{"identifier":"deny-log","description":"Denies the log command without any pre-configured scope.","commands":{"allow":[],"deny":["log"]}}},"permission_sets":{},"global_scope_schema":null},"notification":{"default_permission":{"identifier":"default","description":"This permission set configures which\nnotification features are by default exposed.\n\n#### Granted Permissions\n\nIt allows all notification related features.\n\n","permissions":["allow-is-permission-granted","allow-request-permission","allow-notify","allow-register-action-types","allow-register-listener","allow-cancel","allow-get-pending","allow-remove-active","allow-get-active","allow-check-permissions","allow-show","allow-batch","allow-list-channels","allow-delete-channel","allow-create-channel","allow-permission-state"]},"permissions":{"allow-batch":{"identifier":"allow-batch","description":"Enables the batch command without any pre-configured scope.","commands":{"allow":["batch"],"deny":[]}},"allow-cancel":{"identifier":"allow-cancel","description":"Enables the cancel command without any pre-configured scope.","commands":{"allow":["cancel"],"deny":[]}},"allow-check-permissions":{"identifier":"allow-check-permissions","description":"Enables the check_permissions command without any pre-configured scope.","commands":{"allow":["check_permissions"],"deny":[]}},"allow-create-channel":{"identifier":"allow-create-channel","description":"Enables the create_channel command without any pre-configured scope.","commands":{"allow":["create_channel"],"deny":[]}},"allow-delete-channel":{"identifier":"allow-delete-channel","description":"Enables the delete_channel command without any pre-configured scope.","commands":{"allow":["delete_channel"],"deny":[]}},"allow-get-active":{"identifier":"allow-get-active","description":"Enables the get_active command without any pre-configured scope.","commands":{"allow":["get_active"],"deny":[]}},"allow-get-pending":{"identifier":"allow-get-pending","description":"Enables the get_pending command without any pre-configured scope.","commands":{"allow":["get_pending"],"deny":[]}},"allow-is-permission-granted":{"identifier":"allow-is-permission-granted","description":"Enables the is_permission_granted command without any pre-configured scope.","commands":{"allow":["is_permission_granted"],"deny":[]}},"allow-list-channels":{"identifier":"allow-list-channels","description":"Enables the list_channels command without any pre-configured scope.","commands":{"allow":["list_channels"],"deny":[]}},"allow-notify":{"identifier":"allow-notify","description":"Enables the notify command without any pre-configured scope.","commands":{"allow":["notify"],"deny":[]}},"allow-permission-state":{"identifier":"allow-permission-state","description":"Enables the permission_state command without any pre-configured scope.","commands":{"allow":["permission_state"],"deny":[]}},"allow-register-action-types":{"identifier":"allow-register-action-types","description":"Enables the register_action_types command without any pre-configured scope.","commands":{"allow":["register_action_types"],"deny":[]}},"allow-register-listener":{"identifier":"allow-register-listener","description":"Enables the register_listener command without any pre-configured scope.","commands":{"allow":["register_listener"],"deny":[]}},"allow-remove-active":{"identifier":"allow-remove-active","description":"Enables the remove_active command without any pre-configured scope.","commands":{"allow":["remove_active"],"deny":[]}},"allow-request-permission":{"identifier":"allow-request-permission","description":"Enables the request_permission command without any pre-configured scope.","commands":{"allow":["request_permission"],"deny":[]}},"allow-show":{"identifier":"allow-show","description":"Enables the show command without any pre-configured scope.","commands":{"allow":["show"],"deny":[]}},"deny-batch":{"identifier":"deny-batch","description":"Denies the batch command without any pre-configured scope.","commands":{"allow":[],"deny":["batch"]}},"deny-cancel":{"identifier":"deny-cancel","description":"Denies the cancel command without any pre-configured scope.","commands":{"allow":[],"deny":["cancel"]}},"deny-check-permissions":{"identifier":"deny-check-permissions","description":"Denies the check_permissions command without any pre-configured scope.","commands":{"allow":[],"deny":["check_permissions"]}},"deny-create-channel":{"identifier":"deny-create-channel","description":"Denies the create_channel command without any pre-configured scope.","commands":{"allow":[],"deny":["create_channel"]}},"deny-delete-channel":{"identifier":"deny-delete-channel","description":"Denies the delete_channel command without any pre-configured scope.","commands":{"allow":[],"deny":["delete_channel"]}},"deny-get-active":{"identifier":"deny-get-active","description":"Denies the get_active command without any pre-configured scope.","commands":{"allow":[],"deny":["get_active"]}},"deny-get-pending":{"identifier":"deny-get-pending","description":"Denies the get_pending command without any pre-configured scope.","commands":{"allow":[],"deny":["get_pending"]}},"deny-is-permission-granted":{"identifier":"deny-is-permission-granted","description":"Denies the is_permission_granted command without any pre-configured scope.","commands":{"allow":[],"deny":["is_permission_granted"]}},"deny-list-channels":{"identifier":"deny-list-channels","description":"Denies the list_channels command without any pre-configured scope.","commands":{"allow":[],"deny":["list_channels"]}},"deny-notify":{"identifier":"deny-notify","description":"Denies the notify command without any pre-configured scope.","commands":{"allow":[],"deny":["notify"]}},"deny-permission-state":{"identifier":"deny-permission-state","description":"Denies the permission_state command without any pre-configured scope.","commands":{"allow":[],"deny":["permission_state"]}},"deny-register-action-types":{"identifier":"deny-register-action-types","description":"Denies the register_action_types command without any pre-configured scope.","commands":{"allow":[],"deny":["register_action_types"]}},"deny-register-listener":{"identifier":"deny-register-listener","description":"Denies the register_listener command without any pre-configured scope.","commands":{"allow":[],"deny":["register_listener"]}},"deny-remove-active":{"identifier":"deny-remove-active","description":"Denies the remove_active command without any pre-configured scope.","commands":{"allow":[],"deny":["remove_active"]}},"deny-request-permission":{"identifier":"deny-request-permission","description":"Denies the request_permission command without any pre-configured scope.","commands":{"allow":[],"deny":["request_permission"]}},"deny-show":{"identifier":"deny-show","description":"Denies the show command without any pre-configured scope.","commands":{"allow":[],"deny":["show"]}}},"permission_sets":{},"global_scope_schema":null},"os":{"default_permission":{"identifier":"default","description":"This permission set configures which\noperating system information are available\nto gather from the frontend.\n\n#### Granted Permissions\n\nAll information except the host name are available.\n\n","permissions":["allow-arch","allow-exe-extension","allow-family","allow-locale","allow-os-type","allow-platform","allow-version"]},"permissions":{"allow-arch":{"identifier":"allow-arch","description":"Enables the arch command without any pre-configured scope.","commands":{"allow":["arch"],"deny":[]}},"allow-exe-extension":{"identifier":"allow-exe-extension","description":"Enables the exe_extension command without any pre-configured scope.","commands":{"allow":["exe_extension"],"deny":[]}},"allow-family":{"identifier":"allow-family","description":"Enables the family command without any pre-configured scope.","commands":{"allow":["family"],"deny":[]}},"allow-hostname":{"identifier":"allow-hostname","description":"Enables the hostname command without any pre-configured scope.","commands":{"allow":["hostname"],"deny":[]}},"allow-locale":{"identifier":"allow-locale","description":"Enables the locale command without any pre-configured scope.","commands":{"allow":["locale"],"deny":[]}},"allow-os-type":{"identifier":"allow-os-type","description":"Enables the os_type command without any pre-configured scope.","commands":{"allow":["os_type"],"deny":[]}},"allow-platform":{"identifier":"allow-platform","description":"Enables the platform command without any pre-configured scope.","commands":{"allow":["platform"],"deny":[]}},"allow-version":{"identifier":"allow-version","description":"Enables the version command without any pre-configured scope.","commands":{"allow":["version"],"deny":[]}},"deny-arch":{"identifier":"deny-arch","description":"Denies the arch command without any pre-configured scope.","commands":{"allow":[],"deny":["arch"]}},"deny-exe-extension":{"identifier":"deny-exe-extension","description":"Denies the exe_extension command without any pre-configured scope.","commands":{"allow":[],"deny":["exe_extension"]}},"deny-family":{"identifier":"deny-family","description":"Denies the family command without any pre-configured scope.","commands":{"allow":[],"deny":["family"]}},"deny-hostname":{"identifier":"deny-hostname","description":"Denies the hostname command without any pre-configured scope.","commands":{"allow":[],"deny":["hostname"]}},"deny-locale":{"identifier":"deny-locale","description":"Denies the locale command without any pre-configured scope.","commands":{"allow":[],"deny":["locale"]}},"deny-os-type":{"identifier":"deny-os-type","description":"Denies the os_type command without any pre-configured scope.","commands":{"allow":[],"deny":["os_type"]}},"deny-platform":{"identifier":"deny-platform","description":"Denies the platform command without any pre-configured scope.","commands":{"allow":[],"deny":["platform"]}},"deny-version":{"identifier":"deny-version","description":"Denies the version command without any pre-configured scope.","commands":{"allow":[],"deny":["version"]}}},"permission_sets":{},"global_scope_schema":null},"shell":{"default_permission":{"identifier":"default","description":"This permission set configures which\nshell functionality is exposed by default.\n\n#### Granted Permissions\n\nIt allows to use the `open` functionality without any specific\nscope pre-configured. It will allow opening `http(s)://`,\n`tel:` and `mailto:` links.\n","permissions":["allow-open"]},"permissions":{"allow-execute":{"identifier":"allow-execute","description":"Enables the execute command without any pre-configured scope.","commands":{"allow":["execute"],"deny":[]}},"allow-kill":{"identifier":"allow-kill","description":"Enables the kill command without any pre-configured scope.","commands":{"allow":["kill"],"deny":[]}},"allow-open":{"identifier":"allow-open","description":"Enables the open command without any pre-configured scope.","commands":{"allow":["open"],"deny":[]}},"allow-spawn":{"identifier":"allow-spawn","description":"Enables the spawn command without any pre-configured scope.","commands":{"allow":["spawn"],"deny":[]}},"allow-stdin-write":{"identifier":"allow-stdin-write","description":"Enables the stdin_write command without any pre-configured scope.","commands":{"allow":["stdin_write"],"deny":[]}},"deny-execute":{"identifier":"deny-execute","description":"Denies the execute command without any pre-configured scope.","commands":{"allow":[],"deny":["execute"]}},"deny-kill":{"identifier":"deny-kill","description":"Denies the kill command without any pre-configured scope.","commands":{"allow":[],"deny":["kill"]}},"deny-open":{"identifier":"deny-open","description":"Denies the open command without any pre-configured scope.","commands":{"allow":[],"deny":["open"]}},"deny-spawn":{"identifier":"deny-spawn","description":"Denies the spawn command without any pre-configured scope.","commands":{"allow":[],"deny":["spawn"]}},"deny-stdin-write":{"identifier":"deny-stdin-write","description":"Denies the stdin_write command without any pre-configured scope.","commands":{"allow":[],"deny":["stdin_write"]}}},"permission_sets":{},"global_scope_schema":{"$schema":"http://json-schema.org/draft-07/schema#","definitions":{"ShellAllowedArg":{"anyOf":[{"description":"A non-configurable argument that is passed to the command in the order it was specified.","type":"string"},{"additionalProperties":false,"description":"A variable that is set while calling the command from the webview API.","properties":{"raw":{"default":false,"description":"Marks the validator as a raw regex, meaning the plugin should not make any modification at runtime.\n\nThis means the regex will not match on the entire string by default, which might be exploited if your regex allow unexpected input to be considered valid. When using this option, make sure your regex is correct.","type":"boolean"},"validator":{"description":"[regex] validator to require passed values to conform to an expected input.\n\nThis will require the argument value passed to this variable to match the `validator` regex before it will be executed.\n\nThe regex string is by default surrounded by `^...$` to match the full string. For example the `https?://\\w+` regex would be registered as `^https?://\\w+$`.\n\n[regex]: <https://docs.rs/regex/latest/regex/#syntax>","type":"string"}},"required":["validator"],"type":"object"}],"description":"A command argument allowed to be executed by the webview API."},"ShellAllowedArgs":{"anyOf":[{"description":"Use a simple boolean to allow all or disable all arguments to this command configuration.","type":"boolean"},{"description":"A specific set of [`ShellAllowedArg`] that are valid to call for the command configuration.","items":{"$ref":"#/definitions/ShellAllowedArg"},"type":"array"}],"description":"A set of command arguments allowed to be executed by the webview API.\n\nA value of `true` will allow any arguments to be passed to the command. `false` will disable all arguments. A list of [`ShellAllowedArg`] will set those arguments as the only valid arguments to be passed to the attached command configuration."}},"description":"A command allowed to be executed by the webview API.","properties":{"args":{"allOf":[{"$ref":"#/definitions/ShellAllowedArgs"}],"description":"The allowed arguments for the command execution."},"cmd":{"description":"The command name. It can start with a variable that resolves to a system base directory. The variables are: `$AUDIO`, `$CACHE`, `$CONFIG`, `$DATA`, `$LOCALDATA`, `$DESKTOP`, `$DOCUMENT`, `$DOWNLOAD`, `$EXE`, `$FONT`, `$HOME`, `$PICTURE`, `$PUBLIC`, `$RUNTIME`, `$TEMPLATE`, `$VIDEO`, `$RESOURCE`, `$APP`, `$LOG`, `$TEMP`, `$APPCONFIG`, `$APPDATA`, `$APPLOCALDATA`, `$APPCACHE`, `$APPLOG`.","type":"string"},"name":{"description":"The name for this allowed shell command configuration.\n\nThis name will be used inside of the webview API to call this command along with any specified arguments.","type":"string"},"sidecar":{"description":"If this command is a sidecar command.","type":"boolean"}},"required":["args","cmd","name","sidecar"],"title":"Entry","type":"object"}},"window-state":{"default_permission":{"identifier":"default","description":"This permission set configures what kind of\noperations are available from the window state plugin.\n\n#### Granted Permissions\n\nAll operations are enabled by default.\n\n","permissions":["allow-filename","allow-restore-state","allow-save-window-state"]},"permissions":{"allow-filename":{"identifier":"allow-filename","description":"Enables the filename command without any pre-configured scope.","commands":{"allow":["filename"],"deny":[]}},"allow-restore-state":{"identifier":"allow-restore-state","description":"Enables the restore_state command without any pre-configured scope.","commands":{"allow":["restore_state"],"deny":[]}},"allow-save-window-state":{"identifier":"allow-save-window-state","description":"Enables the save_window_state command without any pre-configured scope.","commands":{"allow":["save_window_state"],"deny":[]}},"deny-filename":{"identifier":"deny-filename","description":"Denies the filename command without any pre-configured scope.","commands":{"allow":[],"deny":["filename"]}},"deny-restore-state":{"identifier":"deny-restore-state","description":"Denies the restore_state command without any pre-configured scope.","commands":{"allow":[],"deny":["restore_state"]}},"deny-save-window-state":{"identifier":"deny-save-window-state","description":"Denies the save_window_state command without any pre-configured scope.","commands":{"allow":[],"deny":["save_window_state"]}}},"permission_sets":{},"global_scope_schema":null}}
\ No newline at end of file
diff --git a/desktop/tauri/src-tauri/gen/schemas/desktop-schema.json b/desktop/tauri/src-tauri/gen/schemas/desktop-schema.json
index 10fb08fb..797ccb5c 100644
--- a/desktop/tauri/src-tauri/gen/schemas/desktop-schema.json
+++ b/desktop/tauri/src-tauri/gen/schemas/desktop-schema.json
@@ -133,2202 +133,2803 @@
         {
           "description": "Reference a permission or permission set by identifier and extends its scope.",
           "type": "object",
-          "allOf": [
+          "oneOf": [
             {
-              "if": {
-                "properties": {
-                  "identifier": {
-                    "anyOf": [
-                      {
-                        "description": "This permission set configures which\nshell functionality is exposed by default.\n\n#### Granted Permissions\n\nIt allows to use the `open` functionality without any specific\nscope pre-configured. It will allow opening `http(s)://`,\n`tel:` and `mailto:` links.\n",
-                        "type": "string",
-                        "const": "shell:default"
-                      },
-                      {
-                        "description": "Enables the execute command without any pre-configured scope.",
-                        "type": "string",
-                        "const": "shell:allow-execute"
-                      },
-                      {
-                        "description": "Enables the kill command without any pre-configured scope.",
-                        "type": "string",
-                        "const": "shell:allow-kill"
-                      },
-                      {
-                        "description": "Enables the open command without any pre-configured scope.",
-                        "type": "string",
-                        "const": "shell:allow-open"
-                      },
-                      {
-                        "description": "Enables the spawn command without any pre-configured scope.",
-                        "type": "string",
-                        "const": "shell:allow-spawn"
-                      },
-                      {
-                        "description": "Enables the stdin_write command without any pre-configured scope.",
-                        "type": "string",
-                        "const": "shell:allow-stdin-write"
-                      },
-                      {
-                        "description": "Denies the execute command without any pre-configured scope.",
-                        "type": "string",
-                        "const": "shell:deny-execute"
-                      },
-                      {
-                        "description": "Denies the kill command without any pre-configured scope.",
-                        "type": "string",
-                        "const": "shell:deny-kill"
-                      },
-                      {
-                        "description": "Denies the open command without any pre-configured scope.",
-                        "type": "string",
-                        "const": "shell:deny-open"
-                      },
-                      {
-                        "description": "Denies the spawn command without any pre-configured scope.",
-                        "type": "string",
-                        "const": "shell:deny-spawn"
-                      },
-                      {
-                        "description": "Denies the stdin_write command without any pre-configured scope.",
-                        "type": "string",
-                        "const": "shell:deny-stdin-write"
-                      }
-                    ]
-                  }
-                }
-              },
-              "then": {
-                "properties": {
-                  "allow": {
-                    "items": {
-                      "title": "ShellScopeEntry",
-                      "description": "Shell scope entry.",
-                      "anyOf": [
-                        {
-                          "type": "object",
-                          "required": [
-                            "cmd",
-                            "name"
-                          ],
-                          "properties": {
-                            "args": {
-                              "description": "The allowed arguments for the command execution.",
-                              "allOf": [
-                                {
-                                  "$ref": "#/definitions/ShellScopeEntryAllowedArgs"
-                                }
-                              ]
-                            },
-                            "cmd": {
-                              "description": "The command name. It can start with a variable that resolves to a system base directory. The variables are: `$AUDIO`, `$CACHE`, `$CONFIG`, `$DATA`, `$LOCALDATA`, `$DESKTOP`, `$DOCUMENT`, `$DOWNLOAD`, `$EXE`, `$FONT`, `$HOME`, `$PICTURE`, `$PUBLIC`, `$RUNTIME`, `$TEMPLATE`, `$VIDEO`, `$RESOURCE`, `$LOG`, `$TEMP`, `$APPCONFIG`, `$APPDATA`, `$APPLOCALDATA`, `$APPCACHE`, `$APPLOG`.",
-                              "type": "string"
-                            },
-                            "name": {
-                              "description": "The name for this allowed shell command configuration.\n\nThis name will be used inside of the webview API to call this command along with any specified arguments.",
-                              "type": "string"
-                            }
-                          },
-                          "additionalProperties": false
-                        },
-                        {
-                          "type": "object",
-                          "required": [
-                            "name",
-                            "sidecar"
-                          ],
-                          "properties": {
-                            "args": {
-                              "description": "The allowed arguments for the command execution.",
-                              "allOf": [
-                                {
-                                  "$ref": "#/definitions/ShellScopeEntryAllowedArgs"
-                                }
-                              ]
-                            },
-                            "name": {
-                              "description": "The name for this allowed shell command configuration.\n\nThis name will be used inside of the webview API to call this command along with any specified arguments.",
-                              "type": "string"
-                            },
-                            "sidecar": {
-                              "description": "If this command is a sidecar command.",
-                              "type": "boolean"
-                            }
-                          },
-                          "additionalProperties": false
-                        }
-                      ]
-                    }
-                  },
-                  "deny": {
-                    "items": {
-                      "title": "ShellScopeEntry",
-                      "description": "Shell scope entry.",
-                      "anyOf": [
-                        {
-                          "type": "object",
-                          "required": [
-                            "cmd",
-                            "name"
-                          ],
-                          "properties": {
-                            "args": {
-                              "description": "The allowed arguments for the command execution.",
-                              "allOf": [
-                                {
-                                  "$ref": "#/definitions/ShellScopeEntryAllowedArgs"
-                                }
-                              ]
-                            },
-                            "cmd": {
-                              "description": "The command name. It can start with a variable that resolves to a system base directory. The variables are: `$AUDIO`, `$CACHE`, `$CONFIG`, `$DATA`, `$LOCALDATA`, `$DESKTOP`, `$DOCUMENT`, `$DOWNLOAD`, `$EXE`, `$FONT`, `$HOME`, `$PICTURE`, `$PUBLIC`, `$RUNTIME`, `$TEMPLATE`, `$VIDEO`, `$RESOURCE`, `$LOG`, `$TEMP`, `$APPCONFIG`, `$APPDATA`, `$APPLOCALDATA`, `$APPCACHE`, `$APPLOG`.",
-                              "type": "string"
-                            },
-                            "name": {
-                              "description": "The name for this allowed shell command configuration.\n\nThis name will be used inside of the webview API to call this command along with any specified arguments.",
-                              "type": "string"
-                            }
-                          },
-                          "additionalProperties": false
-                        },
-                        {
-                          "type": "object",
-                          "required": [
-                            "name",
-                            "sidecar"
-                          ],
-                          "properties": {
-                            "args": {
-                              "description": "The allowed arguments for the command execution.",
-                              "allOf": [
-                                {
-                                  "$ref": "#/definitions/ShellScopeEntryAllowedArgs"
-                                }
-                              ]
-                            },
-                            "name": {
-                              "description": "The name for this allowed shell command configuration.\n\nThis name will be used inside of the webview API to call this command along with any specified arguments.",
-                              "type": "string"
-                            },
-                            "sidecar": {
-                              "description": "If this command is a sidecar command.",
-                              "type": "boolean"
-                            }
-                          },
-                          "additionalProperties": false
-                        }
-                      ]
-                    }
-                  }
-                }
-              },
+              "type": "object",
+              "required": [
+                "identifier"
+              ],
               "properties": {
                 "identifier": {
-                  "description": "Identifier of the permission or permission set.",
-                  "allOf": [
+                  "oneOf": [
                     {
-                      "$ref": "#/definitions/Identifier"
-                    }
-                  ]
-                }
-              }
-            },
-            {
-              "properties": {
-                "identifier": {
-                  "description": "Identifier of the permission or permission set.",
-                  "allOf": [
+                      "description": "shell:default -> This permission set configures which\nshell functionality is exposed by default.\n\n#### Granted Permissions\n\nIt allows to use the `open` functionality without any specific\nscope pre-configured. It will allow opening `http(s)://`,\n`tel:` and `mailto:` links.\n",
+                      "type": "string",
+                      "enum": [
+                        "shell:default"
+                      ]
+                    },
                     {
-                      "$ref": "#/definitions/Identifier"
+                      "description": "shell:allow-execute -> Enables the execute command without any pre-configured scope.",
+                      "type": "string",
+                      "enum": [
+                        "shell:allow-execute"
+                      ]
+                    },
+                    {
+                      "description": "shell:allow-kill -> Enables the kill command without any pre-configured scope.",
+                      "type": "string",
+                      "enum": [
+                        "shell:allow-kill"
+                      ]
+                    },
+                    {
+                      "description": "shell:allow-open -> Enables the open command without any pre-configured scope.",
+                      "type": "string",
+                      "enum": [
+                        "shell:allow-open"
+                      ]
+                    },
+                    {
+                      "description": "shell:allow-spawn -> Enables the spawn command without any pre-configured scope.",
+                      "type": "string",
+                      "enum": [
+                        "shell:allow-spawn"
+                      ]
+                    },
+                    {
+                      "description": "shell:allow-stdin-write -> Enables the stdin_write command without any pre-configured scope.",
+                      "type": "string",
+                      "enum": [
+                        "shell:allow-stdin-write"
+                      ]
+                    },
+                    {
+                      "description": "shell:deny-execute -> Denies the execute command without any pre-configured scope.",
+                      "type": "string",
+                      "enum": [
+                        "shell:deny-execute"
+                      ]
+                    },
+                    {
+                      "description": "shell:deny-kill -> Denies the kill command without any pre-configured scope.",
+                      "type": "string",
+                      "enum": [
+                        "shell:deny-kill"
+                      ]
+                    },
+                    {
+                      "description": "shell:deny-open -> Denies the open command without any pre-configured scope.",
+                      "type": "string",
+                      "enum": [
+                        "shell:deny-open"
+                      ]
+                    },
+                    {
+                      "description": "shell:deny-spawn -> Denies the spawn command without any pre-configured scope.",
+                      "type": "string",
+                      "enum": [
+                        "shell:deny-spawn"
+                      ]
+                    },
+                    {
+                      "description": "shell:deny-stdin-write -> Denies the stdin_write command without any pre-configured scope.",
+                      "type": "string",
+                      "enum": [
+                        "shell:deny-stdin-write"
+                      ]
                     }
                   ]
                 },
                 "allow": {
-                  "description": "Data that defines what is allowed by the scope.",
-                  "type": [
-                    "array",
-                    "null"
-                  ],
                   "items": {
-                    "$ref": "#/definitions/Value"
+                    "title": "Entry",
+                    "description": "A command allowed to be executed by the webview API.",
+                    "type": "object",
+                    "required": [
+                      "args",
+                      "cmd",
+                      "name",
+                      "sidecar"
+                    ],
+                    "properties": {
+                      "args": {
+                        "description": "The allowed arguments for the command execution.",
+                        "allOf": [
+                          {
+                            "$ref": "#/definitions/ShellAllowedArgs"
+                          }
+                        ]
+                      },
+                      "cmd": {
+                        "description": "The command name. It can start with a variable that resolves to a system base directory. The variables are: `$AUDIO`, `$CACHE`, `$CONFIG`, `$DATA`, `$LOCALDATA`, `$DESKTOP`, `$DOCUMENT`, `$DOWNLOAD`, `$EXE`, `$FONT`, `$HOME`, `$PICTURE`, `$PUBLIC`, `$RUNTIME`, `$TEMPLATE`, `$VIDEO`, `$RESOURCE`, `$APP`, `$LOG`, `$TEMP`, `$APPCONFIG`, `$APPDATA`, `$APPLOCALDATA`, `$APPCACHE`, `$APPLOG`.",
+                        "type": "string"
+                      },
+                      "name": {
+                        "description": "The name for this allowed shell command configuration.\n\nThis name will be used inside of the webview API to call this command along with any specified arguments.",
+                        "type": "string"
+                      },
+                      "sidecar": {
+                        "description": "If this command is a sidecar command.",
+                        "type": "boolean"
+                      }
+                    }
                   }
                 },
                 "deny": {
-                  "description": "Data that defines what is denied by the scope. This should be prioritized by validation logic.",
-                  "type": [
-                    "array",
-                    "null"
-                  ],
                   "items": {
-                    "$ref": "#/definitions/Value"
+                    "title": "Entry",
+                    "description": "A command allowed to be executed by the webview API.",
+                    "type": "object",
+                    "required": [
+                      "args",
+                      "cmd",
+                      "name",
+                      "sidecar"
+                    ],
+                    "properties": {
+                      "args": {
+                        "description": "The allowed arguments for the command execution.",
+                        "allOf": [
+                          {
+                            "$ref": "#/definitions/ShellAllowedArgs"
+                          }
+                        ]
+                      },
+                      "cmd": {
+                        "description": "The command name. It can start with a variable that resolves to a system base directory. The variables are: `$AUDIO`, `$CACHE`, `$CONFIG`, `$DATA`, `$LOCALDATA`, `$DESKTOP`, `$DOCUMENT`, `$DOWNLOAD`, `$EXE`, `$FONT`, `$HOME`, `$PICTURE`, `$PUBLIC`, `$RUNTIME`, `$TEMPLATE`, `$VIDEO`, `$RESOURCE`, `$APP`, `$LOG`, `$TEMP`, `$APPCONFIG`, `$APPDATA`, `$APPLOCALDATA`, `$APPCACHE`, `$APPLOG`.",
+                        "type": "string"
+                      },
+                      "name": {
+                        "description": "The name for this allowed shell command configuration.\n\nThis name will be used inside of the webview API to call this command along with any specified arguments.",
+                        "type": "string"
+                      },
+                      "sidecar": {
+                        "description": "If this command is a sidecar command.",
+                        "type": "boolean"
+                      }
+                    }
                   }
                 }
               }
             }
-          ],
-          "required": [
-            "identifier"
           ]
         }
       ]
     },
     "Identifier": {
-      "description": "Permission identifier",
       "oneOf": [
         {
-          "description": "No features are enabled by default, as we believe\nthe clipboard can be inherently dangerous and it is \napplication specific if read and/or write access is needed.\n\nClipboard interaction needs to be explicitly enabled.\n",
+          "description": "clipboard-manager:default -> No features are enabled by default, as we believe\nthe clipboard can be inherently dangerous and it is \napplication specific if read and/or write access is needed.\n\nClipboard interaction needs to be explicitly enabled.\n",
           "type": "string",
-          "const": "clipboard-manager:default"
+          "enum": [
+            "clipboard-manager:default"
+          ]
         },
         {
-          "description": "Enables the clear command without any pre-configured scope.",
+          "description": "clipboard-manager:allow-clear -> Enables the clear command without any pre-configured scope.",
           "type": "string",
-          "const": "clipboard-manager:allow-clear"
+          "enum": [
+            "clipboard-manager:allow-clear"
+          ]
         },
         {
-          "description": "Enables the read_image command without any pre-configured scope.",
+          "description": "clipboard-manager:allow-read-image -> Enables the read_image command without any pre-configured scope.",
           "type": "string",
-          "const": "clipboard-manager:allow-read-image"
+          "enum": [
+            "clipboard-manager:allow-read-image"
+          ]
         },
         {
-          "description": "Enables the read_text command without any pre-configured scope.",
+          "description": "clipboard-manager:allow-read-text -> Enables the read_text command without any pre-configured scope.",
           "type": "string",
-          "const": "clipboard-manager:allow-read-text"
+          "enum": [
+            "clipboard-manager:allow-read-text"
+          ]
         },
         {
-          "description": "Enables the write_html command without any pre-configured scope.",
+          "description": "clipboard-manager:allow-write-html -> Enables the write_html command without any pre-configured scope.",
           "type": "string",
-          "const": "clipboard-manager:allow-write-html"
+          "enum": [
+            "clipboard-manager:allow-write-html"
+          ]
         },
         {
-          "description": "Enables the write_image command without any pre-configured scope.",
+          "description": "clipboard-manager:allow-write-image -> Enables the write_image command without any pre-configured scope.",
           "type": "string",
-          "const": "clipboard-manager:allow-write-image"
+          "enum": [
+            "clipboard-manager:allow-write-image"
+          ]
         },
         {
-          "description": "Enables the write_text command without any pre-configured scope.",
+          "description": "clipboard-manager:allow-write-text -> Enables the write_text command without any pre-configured scope.",
           "type": "string",
-          "const": "clipboard-manager:allow-write-text"
+          "enum": [
+            "clipboard-manager:allow-write-text"
+          ]
         },
         {
-          "description": "Denies the clear command without any pre-configured scope.",
+          "description": "clipboard-manager:deny-clear -> Denies the clear command without any pre-configured scope.",
           "type": "string",
-          "const": "clipboard-manager:deny-clear"
+          "enum": [
+            "clipboard-manager:deny-clear"
+          ]
         },
         {
-          "description": "Denies the read_image command without any pre-configured scope.",
+          "description": "clipboard-manager:deny-read-image -> Denies the read_image command without any pre-configured scope.",
           "type": "string",
-          "const": "clipboard-manager:deny-read-image"
+          "enum": [
+            "clipboard-manager:deny-read-image"
+          ]
         },
         {
-          "description": "Denies the read_text command without any pre-configured scope.",
+          "description": "clipboard-manager:deny-read-text -> Denies the read_text command without any pre-configured scope.",
           "type": "string",
-          "const": "clipboard-manager:deny-read-text"
+          "enum": [
+            "clipboard-manager:deny-read-text"
+          ]
         },
         {
-          "description": "Denies the write_html command without any pre-configured scope.",
+          "description": "clipboard-manager:deny-write-html -> Denies the write_html command without any pre-configured scope.",
           "type": "string",
-          "const": "clipboard-manager:deny-write-html"
+          "enum": [
+            "clipboard-manager:deny-write-html"
+          ]
         },
         {
-          "description": "Denies the write_image command without any pre-configured scope.",
+          "description": "clipboard-manager:deny-write-image -> Denies the write_image command without any pre-configured scope.",
           "type": "string",
-          "const": "clipboard-manager:deny-write-image"
+          "enum": [
+            "clipboard-manager:deny-write-image"
+          ]
         },
         {
-          "description": "Denies the write_text command without any pre-configured scope.",
+          "description": "clipboard-manager:deny-write-text -> Denies the write_text command without any pre-configured scope.",
           "type": "string",
-          "const": "clipboard-manager:deny-write-text"
+          "enum": [
+            "clipboard-manager:deny-write-text"
+          ]
         },
         {
-          "description": "Default core plugins set which includes:\n- 'core:path:default'\n- 'core:event:default'\n- 'core:window:default'\n- 'core:webview:default'\n- 'core:app:default'\n- 'core:image:default'\n- 'core:resources:default'\n- 'core:menu:default'\n- 'core:tray:default'\n",
+          "description": "core:app:default -> Default permissions for the plugin.",
           "type": "string",
-          "const": "core:default"
+          "enum": [
+            "core:app:default"
+          ]
         },
         {
-          "description": "Default permissions for the plugin.",
+          "description": "core:app:allow-app-hide -> Enables the app_hide command without any pre-configured scope.",
           "type": "string",
-          "const": "core:app:default"
+          "enum": [
+            "core:app:allow-app-hide"
+          ]
         },
         {
-          "description": "Enables the app_hide command without any pre-configured scope.",
+          "description": "core:app:allow-app-show -> Enables the app_show command without any pre-configured scope.",
           "type": "string",
-          "const": "core:app:allow-app-hide"
+          "enum": [
+            "core:app:allow-app-show"
+          ]
         },
         {
-          "description": "Enables the app_show command without any pre-configured scope.",
+          "description": "core:app:allow-default-window-icon -> Enables the default_window_icon command without any pre-configured scope.",
           "type": "string",
-          "const": "core:app:allow-app-show"
+          "enum": [
+            "core:app:allow-default-window-icon"
+          ]
         },
         {
-          "description": "Enables the default_window_icon command without any pre-configured scope.",
+          "description": "core:app:allow-name -> Enables the name command without any pre-configured scope.",
           "type": "string",
-          "const": "core:app:allow-default-window-icon"
+          "enum": [
+            "core:app:allow-name"
+          ]
         },
         {
-          "description": "Enables the name command without any pre-configured scope.",
+          "description": "core:app:allow-tauri-version -> Enables the tauri_version command without any pre-configured scope.",
           "type": "string",
-          "const": "core:app:allow-name"
+          "enum": [
+            "core:app:allow-tauri-version"
+          ]
         },
         {
-          "description": "Enables the set_app_theme command without any pre-configured scope.",
+          "description": "core:app:allow-version -> Enables the version command without any pre-configured scope.",
           "type": "string",
-          "const": "core:app:allow-set-app-theme"
+          "enum": [
+            "core:app:allow-version"
+          ]
         },
         {
-          "description": "Enables the tauri_version command without any pre-configured scope.",
+          "description": "core:app:deny-app-hide -> Denies the app_hide command without any pre-configured scope.",
           "type": "string",
-          "const": "core:app:allow-tauri-version"
+          "enum": [
+            "core:app:deny-app-hide"
+          ]
         },
         {
-          "description": "Enables the version command without any pre-configured scope.",
+          "description": "core:app:deny-app-show -> Denies the app_show command without any pre-configured scope.",
           "type": "string",
-          "const": "core:app:allow-version"
+          "enum": [
+            "core:app:deny-app-show"
+          ]
         },
         {
-          "description": "Denies the app_hide command without any pre-configured scope.",
+          "description": "core:app:deny-default-window-icon -> Denies the default_window_icon command without any pre-configured scope.",
           "type": "string",
-          "const": "core:app:deny-app-hide"
+          "enum": [
+            "core:app:deny-default-window-icon"
+          ]
         },
         {
-          "description": "Denies the app_show command without any pre-configured scope.",
+          "description": "core:app:deny-name -> Denies the name command without any pre-configured scope.",
           "type": "string",
-          "const": "core:app:deny-app-show"
+          "enum": [
+            "core:app:deny-name"
+          ]
         },
         {
-          "description": "Denies the default_window_icon command without any pre-configured scope.",
+          "description": "core:app:deny-tauri-version -> Denies the tauri_version command without any pre-configured scope.",
           "type": "string",
-          "const": "core:app:deny-default-window-icon"
+          "enum": [
+            "core:app:deny-tauri-version"
+          ]
         },
         {
-          "description": "Denies the name command without any pre-configured scope.",
+          "description": "core:app:deny-version -> Denies the version command without any pre-configured scope.",
           "type": "string",
-          "const": "core:app:deny-name"
+          "enum": [
+            "core:app:deny-version"
+          ]
         },
         {
-          "description": "Denies the set_app_theme command without any pre-configured scope.",
+          "description": "core:event:default -> Default permissions for the plugin.",
           "type": "string",
-          "const": "core:app:deny-set-app-theme"
+          "enum": [
+            "core:event:default"
+          ]
         },
         {
-          "description": "Denies the tauri_version command without any pre-configured scope.",
+          "description": "core:event:allow-emit -> Enables the emit command without any pre-configured scope.",
           "type": "string",
-          "const": "core:app:deny-tauri-version"
+          "enum": [
+            "core:event:allow-emit"
+          ]
         },
         {
-          "description": "Denies the version command without any pre-configured scope.",
+          "description": "core:event:allow-emit-to -> Enables the emit_to command without any pre-configured scope.",
           "type": "string",
-          "const": "core:app:deny-version"
+          "enum": [
+            "core:event:allow-emit-to"
+          ]
         },
         {
-          "description": "Default permissions for the plugin.",
+          "description": "core:event:allow-listen -> Enables the listen command without any pre-configured scope.",
           "type": "string",
-          "const": "core:event:default"
+          "enum": [
+            "core:event:allow-listen"
+          ]
         },
         {
-          "description": "Enables the emit command without any pre-configured scope.",
+          "description": "core:event:allow-unlisten -> Enables the unlisten command without any pre-configured scope.",
           "type": "string",
-          "const": "core:event:allow-emit"
+          "enum": [
+            "core:event:allow-unlisten"
+          ]
         },
         {
-          "description": "Enables the emit_to command without any pre-configured scope.",
+          "description": "core:event:deny-emit -> Denies the emit command without any pre-configured scope.",
           "type": "string",
-          "const": "core:event:allow-emit-to"
+          "enum": [
+            "core:event:deny-emit"
+          ]
         },
         {
-          "description": "Enables the listen command without any pre-configured scope.",
+          "description": "core:event:deny-emit-to -> Denies the emit_to command without any pre-configured scope.",
           "type": "string",
-          "const": "core:event:allow-listen"
+          "enum": [
+            "core:event:deny-emit-to"
+          ]
         },
         {
-          "description": "Enables the unlisten command without any pre-configured scope.",
+          "description": "core:event:deny-listen -> Denies the listen command without any pre-configured scope.",
           "type": "string",
-          "const": "core:event:allow-unlisten"
+          "enum": [
+            "core:event:deny-listen"
+          ]
         },
         {
-          "description": "Denies the emit command without any pre-configured scope.",
+          "description": "core:event:deny-unlisten -> Denies the unlisten command without any pre-configured scope.",
           "type": "string",
-          "const": "core:event:deny-emit"
+          "enum": [
+            "core:event:deny-unlisten"
+          ]
         },
         {
-          "description": "Denies the emit_to command without any pre-configured scope.",
+          "description": "core:image:default -> Default permissions for the plugin.",
           "type": "string",
-          "const": "core:event:deny-emit-to"
+          "enum": [
+            "core:image:default"
+          ]
         },
         {
-          "description": "Denies the listen command without any pre-configured scope.",
+          "description": "core:image:allow-from-bytes -> Enables the from_bytes command without any pre-configured scope.",
           "type": "string",
-          "const": "core:event:deny-listen"
+          "enum": [
+            "core:image:allow-from-bytes"
+          ]
         },
         {
-          "description": "Denies the unlisten command without any pre-configured scope.",
+          "description": "core:image:allow-from-path -> Enables the from_path command without any pre-configured scope.",
           "type": "string",
-          "const": "core:event:deny-unlisten"
+          "enum": [
+            "core:image:allow-from-path"
+          ]
         },
         {
-          "description": "Default permissions for the plugin.",
+          "description": "core:image:allow-new -> Enables the new command without any pre-configured scope.",
           "type": "string",
-          "const": "core:image:default"
+          "enum": [
+            "core:image:allow-new"
+          ]
         },
         {
-          "description": "Enables the from_bytes command without any pre-configured scope.",
+          "description": "core:image:allow-rgba -> Enables the rgba command without any pre-configured scope.",
           "type": "string",
-          "const": "core:image:allow-from-bytes"
+          "enum": [
+            "core:image:allow-rgba"
+          ]
         },
         {
-          "description": "Enables the from_path command without any pre-configured scope.",
+          "description": "core:image:allow-size -> Enables the size command without any pre-configured scope.",
           "type": "string",
-          "const": "core:image:allow-from-path"
+          "enum": [
+            "core:image:allow-size"
+          ]
         },
         {
-          "description": "Enables the new command without any pre-configured scope.",
+          "description": "core:image:deny-from-bytes -> Denies the from_bytes command without any pre-configured scope.",
           "type": "string",
-          "const": "core:image:allow-new"
+          "enum": [
+            "core:image:deny-from-bytes"
+          ]
         },
         {
-          "description": "Enables the rgba command without any pre-configured scope.",
+          "description": "core:image:deny-from-path -> Denies the from_path command without any pre-configured scope.",
           "type": "string",
-          "const": "core:image:allow-rgba"
+          "enum": [
+            "core:image:deny-from-path"
+          ]
         },
         {
-          "description": "Enables the size command without any pre-configured scope.",
+          "description": "core:image:deny-new -> Denies the new command without any pre-configured scope.",
           "type": "string",
-          "const": "core:image:allow-size"
+          "enum": [
+            "core:image:deny-new"
+          ]
         },
         {
-          "description": "Denies the from_bytes command without any pre-configured scope.",
+          "description": "core:image:deny-rgba -> Denies the rgba command without any pre-configured scope.",
           "type": "string",
-          "const": "core:image:deny-from-bytes"
+          "enum": [
+            "core:image:deny-rgba"
+          ]
         },
         {
-          "description": "Denies the from_path command without any pre-configured scope.",
+          "description": "core:image:deny-size -> Denies the size command without any pre-configured scope.",
           "type": "string",
-          "const": "core:image:deny-from-path"
+          "enum": [
+            "core:image:deny-size"
+          ]
         },
         {
-          "description": "Denies the new command without any pre-configured scope.",
+          "description": "core:menu:default -> Default permissions for the plugin.",
           "type": "string",
-          "const": "core:image:deny-new"
+          "enum": [
+            "core:menu:default"
+          ]
         },
         {
-          "description": "Denies the rgba command without any pre-configured scope.",
+          "description": "core:menu:allow-append -> Enables the append command without any pre-configured scope.",
           "type": "string",
-          "const": "core:image:deny-rgba"
+          "enum": [
+            "core:menu:allow-append"
+          ]
         },
         {
-          "description": "Denies the size command without any pre-configured scope.",
+          "description": "core:menu:allow-create-default -> Enables the create_default command without any pre-configured scope.",
           "type": "string",
-          "const": "core:image:deny-size"
+          "enum": [
+            "core:menu:allow-create-default"
+          ]
         },
         {
-          "description": "Default permissions for the plugin.",
+          "description": "core:menu:allow-get -> Enables the get command without any pre-configured scope.",
           "type": "string",
-          "const": "core:menu:default"
+          "enum": [
+            "core:menu:allow-get"
+          ]
         },
         {
-          "description": "Enables the append command without any pre-configured scope.",
+          "description": "core:menu:allow-insert -> Enables the insert command without any pre-configured scope.",
           "type": "string",
-          "const": "core:menu:allow-append"
+          "enum": [
+            "core:menu:allow-insert"
+          ]
         },
         {
-          "description": "Enables the create_default command without any pre-configured scope.",
+          "description": "core:menu:allow-is-checked -> Enables the is_checked command without any pre-configured scope.",
           "type": "string",
-          "const": "core:menu:allow-create-default"
+          "enum": [
+            "core:menu:allow-is-checked"
+          ]
         },
         {
-          "description": "Enables the get command without any pre-configured scope.",
+          "description": "core:menu:allow-is-enabled -> Enables the is_enabled command without any pre-configured scope.",
           "type": "string",
-          "const": "core:menu:allow-get"
+          "enum": [
+            "core:menu:allow-is-enabled"
+          ]
         },
         {
-          "description": "Enables the insert command without any pre-configured scope.",
+          "description": "core:menu:allow-items -> Enables the items command without any pre-configured scope.",
           "type": "string",
-          "const": "core:menu:allow-insert"
+          "enum": [
+            "core:menu:allow-items"
+          ]
         },
         {
-          "description": "Enables the is_checked command without any pre-configured scope.",
+          "description": "core:menu:allow-new -> Enables the new command without any pre-configured scope.",
           "type": "string",
-          "const": "core:menu:allow-is-checked"
+          "enum": [
+            "core:menu:allow-new"
+          ]
         },
         {
-          "description": "Enables the is_enabled command without any pre-configured scope.",
+          "description": "core:menu:allow-popup -> Enables the popup command without any pre-configured scope.",
           "type": "string",
-          "const": "core:menu:allow-is-enabled"
+          "enum": [
+            "core:menu:allow-popup"
+          ]
         },
         {
-          "description": "Enables the items command without any pre-configured scope.",
+          "description": "core:menu:allow-prepend -> Enables the prepend command without any pre-configured scope.",
           "type": "string",
-          "const": "core:menu:allow-items"
+          "enum": [
+            "core:menu:allow-prepend"
+          ]
         },
         {
-          "description": "Enables the new command without any pre-configured scope.",
+          "description": "core:menu:allow-remove -> Enables the remove command without any pre-configured scope.",
           "type": "string",
-          "const": "core:menu:allow-new"
+          "enum": [
+            "core:menu:allow-remove"
+          ]
         },
         {
-          "description": "Enables the popup command without any pre-configured scope.",
+          "description": "core:menu:allow-remove-at -> Enables the remove_at command without any pre-configured scope.",
           "type": "string",
-          "const": "core:menu:allow-popup"
+          "enum": [
+            "core:menu:allow-remove-at"
+          ]
         },
         {
-          "description": "Enables the prepend command without any pre-configured scope.",
+          "description": "core:menu:allow-set-accelerator -> Enables the set_accelerator command without any pre-configured scope.",
           "type": "string",
-          "const": "core:menu:allow-prepend"
+          "enum": [
+            "core:menu:allow-set-accelerator"
+          ]
         },
         {
-          "description": "Enables the remove command without any pre-configured scope.",
+          "description": "core:menu:allow-set-as-app-menu -> Enables the set_as_app_menu command without any pre-configured scope.",
           "type": "string",
-          "const": "core:menu:allow-remove"
+          "enum": [
+            "core:menu:allow-set-as-app-menu"
+          ]
         },
         {
-          "description": "Enables the remove_at command without any pre-configured scope.",
+          "description": "core:menu:allow-set-as-help-menu-for-nsapp -> Enables the set_as_help_menu_for_nsapp command without any pre-configured scope.",
           "type": "string",
-          "const": "core:menu:allow-remove-at"
+          "enum": [
+            "core:menu:allow-set-as-help-menu-for-nsapp"
+          ]
         },
         {
-          "description": "Enables the set_accelerator command without any pre-configured scope.",
+          "description": "core:menu:allow-set-as-window-menu -> Enables the set_as_window_menu command without any pre-configured scope.",
           "type": "string",
-          "const": "core:menu:allow-set-accelerator"
+          "enum": [
+            "core:menu:allow-set-as-window-menu"
+          ]
         },
         {
-          "description": "Enables the set_as_app_menu command without any pre-configured scope.",
+          "description": "core:menu:allow-set-as-windows-menu-for-nsapp -> Enables the set_as_windows_menu_for_nsapp command without any pre-configured scope.",
           "type": "string",
-          "const": "core:menu:allow-set-as-app-menu"
+          "enum": [
+            "core:menu:allow-set-as-windows-menu-for-nsapp"
+          ]
         },
         {
-          "description": "Enables the set_as_help_menu_for_nsapp command without any pre-configured scope.",
+          "description": "core:menu:allow-set-checked -> Enables the set_checked command without any pre-configured scope.",
           "type": "string",
-          "const": "core:menu:allow-set-as-help-menu-for-nsapp"
+          "enum": [
+            "core:menu:allow-set-checked"
+          ]
         },
         {
-          "description": "Enables the set_as_window_menu command without any pre-configured scope.",
+          "description": "core:menu:allow-set-enabled -> Enables the set_enabled command without any pre-configured scope.",
           "type": "string",
-          "const": "core:menu:allow-set-as-window-menu"
+          "enum": [
+            "core:menu:allow-set-enabled"
+          ]
         },
         {
-          "description": "Enables the set_as_windows_menu_for_nsapp command without any pre-configured scope.",
+          "description": "core:menu:allow-set-icon -> Enables the set_icon command without any pre-configured scope.",
           "type": "string",
-          "const": "core:menu:allow-set-as-windows-menu-for-nsapp"
+          "enum": [
+            "core:menu:allow-set-icon"
+          ]
         },
         {
-          "description": "Enables the set_checked command without any pre-configured scope.",
+          "description": "core:menu:allow-set-text -> Enables the set_text command without any pre-configured scope.",
           "type": "string",
-          "const": "core:menu:allow-set-checked"
+          "enum": [
+            "core:menu:allow-set-text"
+          ]
         },
         {
-          "description": "Enables the set_enabled command without any pre-configured scope.",
+          "description": "core:menu:allow-text -> Enables the text command without any pre-configured scope.",
           "type": "string",
-          "const": "core:menu:allow-set-enabled"
+          "enum": [
+            "core:menu:allow-text"
+          ]
         },
         {
-          "description": "Enables the set_icon command without any pre-configured scope.",
+          "description": "core:menu:deny-append -> Denies the append command without any pre-configured scope.",
           "type": "string",
-          "const": "core:menu:allow-set-icon"
+          "enum": [
+            "core:menu:deny-append"
+          ]
         },
         {
-          "description": "Enables the set_text command without any pre-configured scope.",
+          "description": "core:menu:deny-create-default -> Denies the create_default command without any pre-configured scope.",
           "type": "string",
-          "const": "core:menu:allow-set-text"
+          "enum": [
+            "core:menu:deny-create-default"
+          ]
         },
         {
-          "description": "Enables the text command without any pre-configured scope.",
+          "description": "core:menu:deny-get -> Denies the get command without any pre-configured scope.",
           "type": "string",
-          "const": "core:menu:allow-text"
+          "enum": [
+            "core:menu:deny-get"
+          ]
         },
         {
-          "description": "Denies the append command without any pre-configured scope.",
+          "description": "core:menu:deny-insert -> Denies the insert command without any pre-configured scope.",
           "type": "string",
-          "const": "core:menu:deny-append"
+          "enum": [
+            "core:menu:deny-insert"
+          ]
         },
         {
-          "description": "Denies the create_default command without any pre-configured scope.",
+          "description": "core:menu:deny-is-checked -> Denies the is_checked command without any pre-configured scope.",
           "type": "string",
-          "const": "core:menu:deny-create-default"
+          "enum": [
+            "core:menu:deny-is-checked"
+          ]
         },
         {
-          "description": "Denies the get command without any pre-configured scope.",
+          "description": "core:menu:deny-is-enabled -> Denies the is_enabled command without any pre-configured scope.",
           "type": "string",
-          "const": "core:menu:deny-get"
+          "enum": [
+            "core:menu:deny-is-enabled"
+          ]
         },
         {
-          "description": "Denies the insert command without any pre-configured scope.",
+          "description": "core:menu:deny-items -> Denies the items command without any pre-configured scope.",
           "type": "string",
-          "const": "core:menu:deny-insert"
+          "enum": [
+            "core:menu:deny-items"
+          ]
         },
         {
-          "description": "Denies the is_checked command without any pre-configured scope.",
+          "description": "core:menu:deny-new -> Denies the new command without any pre-configured scope.",
           "type": "string",
-          "const": "core:menu:deny-is-checked"
+          "enum": [
+            "core:menu:deny-new"
+          ]
         },
         {
-          "description": "Denies the is_enabled command without any pre-configured scope.",
+          "description": "core:menu:deny-popup -> Denies the popup command without any pre-configured scope.",
           "type": "string",
-          "const": "core:menu:deny-is-enabled"
+          "enum": [
+            "core:menu:deny-popup"
+          ]
         },
         {
-          "description": "Denies the items command without any pre-configured scope.",
+          "description": "core:menu:deny-prepend -> Denies the prepend command without any pre-configured scope.",
           "type": "string",
-          "const": "core:menu:deny-items"
+          "enum": [
+            "core:menu:deny-prepend"
+          ]
         },
         {
-          "description": "Denies the new command without any pre-configured scope.",
+          "description": "core:menu:deny-remove -> Denies the remove command without any pre-configured scope.",
           "type": "string",
-          "const": "core:menu:deny-new"
+          "enum": [
+            "core:menu:deny-remove"
+          ]
         },
         {
-          "description": "Denies the popup command without any pre-configured scope.",
+          "description": "core:menu:deny-remove-at -> Denies the remove_at command without any pre-configured scope.",
           "type": "string",
-          "const": "core:menu:deny-popup"
+          "enum": [
+            "core:menu:deny-remove-at"
+          ]
         },
         {
-          "description": "Denies the prepend command without any pre-configured scope.",
+          "description": "core:menu:deny-set-accelerator -> Denies the set_accelerator command without any pre-configured scope.",
           "type": "string",
-          "const": "core:menu:deny-prepend"
+          "enum": [
+            "core:menu:deny-set-accelerator"
+          ]
         },
         {
-          "description": "Denies the remove command without any pre-configured scope.",
+          "description": "core:menu:deny-set-as-app-menu -> Denies the set_as_app_menu command without any pre-configured scope.",
           "type": "string",
-          "const": "core:menu:deny-remove"
+          "enum": [
+            "core:menu:deny-set-as-app-menu"
+          ]
         },
         {
-          "description": "Denies the remove_at command without any pre-configured scope.",
+          "description": "core:menu:deny-set-as-help-menu-for-nsapp -> Denies the set_as_help_menu_for_nsapp command without any pre-configured scope.",
           "type": "string",
-          "const": "core:menu:deny-remove-at"
+          "enum": [
+            "core:menu:deny-set-as-help-menu-for-nsapp"
+          ]
         },
         {
-          "description": "Denies the set_accelerator command without any pre-configured scope.",
+          "description": "core:menu:deny-set-as-window-menu -> Denies the set_as_window_menu command without any pre-configured scope.",
           "type": "string",
-          "const": "core:menu:deny-set-accelerator"
+          "enum": [
+            "core:menu:deny-set-as-window-menu"
+          ]
         },
         {
-          "description": "Denies the set_as_app_menu command without any pre-configured scope.",
+          "description": "core:menu:deny-set-as-windows-menu-for-nsapp -> Denies the set_as_windows_menu_for_nsapp command without any pre-configured scope.",
           "type": "string",
-          "const": "core:menu:deny-set-as-app-menu"
+          "enum": [
+            "core:menu:deny-set-as-windows-menu-for-nsapp"
+          ]
         },
         {
-          "description": "Denies the set_as_help_menu_for_nsapp command without any pre-configured scope.",
+          "description": "core:menu:deny-set-checked -> Denies the set_checked command without any pre-configured scope.",
           "type": "string",
-          "const": "core:menu:deny-set-as-help-menu-for-nsapp"
+          "enum": [
+            "core:menu:deny-set-checked"
+          ]
         },
         {
-          "description": "Denies the set_as_window_menu command without any pre-configured scope.",
+          "description": "core:menu:deny-set-enabled -> Denies the set_enabled command without any pre-configured scope.",
           "type": "string",
-          "const": "core:menu:deny-set-as-window-menu"
+          "enum": [
+            "core:menu:deny-set-enabled"
+          ]
         },
         {
-          "description": "Denies the set_as_windows_menu_for_nsapp command without any pre-configured scope.",
+          "description": "core:menu:deny-set-icon -> Denies the set_icon command without any pre-configured scope.",
           "type": "string",
-          "const": "core:menu:deny-set-as-windows-menu-for-nsapp"
+          "enum": [
+            "core:menu:deny-set-icon"
+          ]
         },
         {
-          "description": "Denies the set_checked command without any pre-configured scope.",
+          "description": "core:menu:deny-set-text -> Denies the set_text command without any pre-configured scope.",
           "type": "string",
-          "const": "core:menu:deny-set-checked"
+          "enum": [
+            "core:menu:deny-set-text"
+          ]
         },
         {
-          "description": "Denies the set_enabled command without any pre-configured scope.",
+          "description": "core:menu:deny-text -> Denies the text command without any pre-configured scope.",
           "type": "string",
-          "const": "core:menu:deny-set-enabled"
+          "enum": [
+            "core:menu:deny-text"
+          ]
         },
         {
-          "description": "Denies the set_icon command without any pre-configured scope.",
+          "description": "core:path:default -> Default permissions for the plugin.",
           "type": "string",
-          "const": "core:menu:deny-set-icon"
+          "enum": [
+            "core:path:default"
+          ]
         },
         {
-          "description": "Denies the set_text command without any pre-configured scope.",
+          "description": "core:path:allow-basename -> Enables the basename command without any pre-configured scope.",
           "type": "string",
-          "const": "core:menu:deny-set-text"
+          "enum": [
+            "core:path:allow-basename"
+          ]
         },
         {
-          "description": "Denies the text command without any pre-configured scope.",
+          "description": "core:path:allow-dirname -> Enables the dirname command without any pre-configured scope.",
           "type": "string",
-          "const": "core:menu:deny-text"
+          "enum": [
+            "core:path:allow-dirname"
+          ]
         },
         {
-          "description": "Default permissions for the plugin.",
+          "description": "core:path:allow-extname -> Enables the extname command without any pre-configured scope.",
           "type": "string",
-          "const": "core:path:default"
+          "enum": [
+            "core:path:allow-extname"
+          ]
         },
         {
-          "description": "Enables the basename command without any pre-configured scope.",
+          "description": "core:path:allow-is-absolute -> Enables the is_absolute command without any pre-configured scope.",
           "type": "string",
-          "const": "core:path:allow-basename"
+          "enum": [
+            "core:path:allow-is-absolute"
+          ]
         },
         {
-          "description": "Enables the dirname command without any pre-configured scope.",
+          "description": "core:path:allow-join -> Enables the join command without any pre-configured scope.",
           "type": "string",
-          "const": "core:path:allow-dirname"
+          "enum": [
+            "core:path:allow-join"
+          ]
         },
         {
-          "description": "Enables the extname command without any pre-configured scope.",
+          "description": "core:path:allow-normalize -> Enables the normalize command without any pre-configured scope.",
           "type": "string",
-          "const": "core:path:allow-extname"
+          "enum": [
+            "core:path:allow-normalize"
+          ]
         },
         {
-          "description": "Enables the is_absolute command without any pre-configured scope.",
+          "description": "core:path:allow-resolve -> Enables the resolve command without any pre-configured scope.",
           "type": "string",
-          "const": "core:path:allow-is-absolute"
+          "enum": [
+            "core:path:allow-resolve"
+          ]
         },
         {
-          "description": "Enables the join command without any pre-configured scope.",
+          "description": "core:path:allow-resolve-directory -> Enables the resolve_directory command without any pre-configured scope.",
           "type": "string",
-          "const": "core:path:allow-join"
+          "enum": [
+            "core:path:allow-resolve-directory"
+          ]
         },
         {
-          "description": "Enables the normalize command without any pre-configured scope.",
+          "description": "core:path:deny-basename -> Denies the basename command without any pre-configured scope.",
           "type": "string",
-          "const": "core:path:allow-normalize"
+          "enum": [
+            "core:path:deny-basename"
+          ]
         },
         {
-          "description": "Enables the resolve command without any pre-configured scope.",
+          "description": "core:path:deny-dirname -> Denies the dirname command without any pre-configured scope.",
           "type": "string",
-          "const": "core:path:allow-resolve"
+          "enum": [
+            "core:path:deny-dirname"
+          ]
         },
         {
-          "description": "Enables the resolve_directory command without any pre-configured scope.",
+          "description": "core:path:deny-extname -> Denies the extname command without any pre-configured scope.",
           "type": "string",
-          "const": "core:path:allow-resolve-directory"
+          "enum": [
+            "core:path:deny-extname"
+          ]
         },
         {
-          "description": "Denies the basename command without any pre-configured scope.",
+          "description": "core:path:deny-is-absolute -> Denies the is_absolute command without any pre-configured scope.",
           "type": "string",
-          "const": "core:path:deny-basename"
+          "enum": [
+            "core:path:deny-is-absolute"
+          ]
         },
         {
-          "description": "Denies the dirname command without any pre-configured scope.",
+          "description": "core:path:deny-join -> Denies the join command without any pre-configured scope.",
           "type": "string",
-          "const": "core:path:deny-dirname"
+          "enum": [
+            "core:path:deny-join"
+          ]
         },
         {
-          "description": "Denies the extname command without any pre-configured scope.",
+          "description": "core:path:deny-normalize -> Denies the normalize command without any pre-configured scope.",
           "type": "string",
-          "const": "core:path:deny-extname"
+          "enum": [
+            "core:path:deny-normalize"
+          ]
         },
         {
-          "description": "Denies the is_absolute command without any pre-configured scope.",
+          "description": "core:path:deny-resolve -> Denies the resolve command without any pre-configured scope.",
           "type": "string",
-          "const": "core:path:deny-is-absolute"
+          "enum": [
+            "core:path:deny-resolve"
+          ]
         },
         {
-          "description": "Denies the join command without any pre-configured scope.",
+          "description": "core:path:deny-resolve-directory -> Denies the resolve_directory command without any pre-configured scope.",
           "type": "string",
-          "const": "core:path:deny-join"
+          "enum": [
+            "core:path:deny-resolve-directory"
+          ]
         },
         {
-          "description": "Denies the normalize command without any pre-configured scope.",
+          "description": "core:resources:default -> Default permissions for the plugin.",
           "type": "string",
-          "const": "core:path:deny-normalize"
+          "enum": [
+            "core:resources:default"
+          ]
         },
         {
-          "description": "Denies the resolve command without any pre-configured scope.",
+          "description": "core:resources:allow-close -> Enables the close command without any pre-configured scope.",
           "type": "string",
-          "const": "core:path:deny-resolve"
+          "enum": [
+            "core:resources:allow-close"
+          ]
         },
         {
-          "description": "Denies the resolve_directory command without any pre-configured scope.",
+          "description": "core:resources:deny-close -> Denies the close command without any pre-configured scope.",
           "type": "string",
-          "const": "core:path:deny-resolve-directory"
+          "enum": [
+            "core:resources:deny-close"
+          ]
         },
         {
-          "description": "Default permissions for the plugin.",
+          "description": "core:tray:default -> Default permissions for the plugin.",
           "type": "string",
-          "const": "core:resources:default"
+          "enum": [
+            "core:tray:default"
+          ]
         },
         {
-          "description": "Enables the close command without any pre-configured scope.",
+          "description": "core:tray:allow-get-by-id -> Enables the get_by_id command without any pre-configured scope.",
           "type": "string",
-          "const": "core:resources:allow-close"
+          "enum": [
+            "core:tray:allow-get-by-id"
+          ]
         },
         {
-          "description": "Denies the close command without any pre-configured scope.",
+          "description": "core:tray:allow-new -> Enables the new command without any pre-configured scope.",
           "type": "string",
-          "const": "core:resources:deny-close"
+          "enum": [
+            "core:tray:allow-new"
+          ]
         },
         {
-          "description": "Default permissions for the plugin.",
+          "description": "core:tray:allow-remove-by-id -> Enables the remove_by_id command without any pre-configured scope.",
           "type": "string",
-          "const": "core:tray:default"
+          "enum": [
+            "core:tray:allow-remove-by-id"
+          ]
         },
         {
-          "description": "Enables the get_by_id command without any pre-configured scope.",
+          "description": "core:tray:allow-set-icon -> Enables the set_icon command without any pre-configured scope.",
           "type": "string",
-          "const": "core:tray:allow-get-by-id"
+          "enum": [
+            "core:tray:allow-set-icon"
+          ]
         },
         {
-          "description": "Enables the new command without any pre-configured scope.",
+          "description": "core:tray:allow-set-icon-as-template -> Enables the set_icon_as_template command without any pre-configured scope.",
           "type": "string",
-          "const": "core:tray:allow-new"
+          "enum": [
+            "core:tray:allow-set-icon-as-template"
+          ]
         },
         {
-          "description": "Enables the remove_by_id command without any pre-configured scope.",
+          "description": "core:tray:allow-set-menu -> Enables the set_menu command without any pre-configured scope.",
           "type": "string",
-          "const": "core:tray:allow-remove-by-id"
+          "enum": [
+            "core:tray:allow-set-menu"
+          ]
         },
         {
-          "description": "Enables the set_icon command without any pre-configured scope.",
+          "description": "core:tray:allow-set-show-menu-on-left-click -> Enables the set_show_menu_on_left_click command without any pre-configured scope.",
           "type": "string",
-          "const": "core:tray:allow-set-icon"
+          "enum": [
+            "core:tray:allow-set-show-menu-on-left-click"
+          ]
         },
         {
-          "description": "Enables the set_icon_as_template command without any pre-configured scope.",
+          "description": "core:tray:allow-set-temp-dir-path -> Enables the set_temp_dir_path command without any pre-configured scope.",
           "type": "string",
-          "const": "core:tray:allow-set-icon-as-template"
+          "enum": [
+            "core:tray:allow-set-temp-dir-path"
+          ]
         },
         {
-          "description": "Enables the set_menu command without any pre-configured scope.",
+          "description": "core:tray:allow-set-title -> Enables the set_title command without any pre-configured scope.",
           "type": "string",
-          "const": "core:tray:allow-set-menu"
+          "enum": [
+            "core:tray:allow-set-title"
+          ]
         },
         {
-          "description": "Enables the set_show_menu_on_left_click command without any pre-configured scope.",
+          "description": "core:tray:allow-set-tooltip -> Enables the set_tooltip command without any pre-configured scope.",
           "type": "string",
-          "const": "core:tray:allow-set-show-menu-on-left-click"
+          "enum": [
+            "core:tray:allow-set-tooltip"
+          ]
         },
         {
-          "description": "Enables the set_temp_dir_path command without any pre-configured scope.",
+          "description": "core:tray:allow-set-visible -> Enables the set_visible command without any pre-configured scope.",
           "type": "string",
-          "const": "core:tray:allow-set-temp-dir-path"
+          "enum": [
+            "core:tray:allow-set-visible"
+          ]
         },
         {
-          "description": "Enables the set_title command without any pre-configured scope.",
+          "description": "core:tray:deny-get-by-id -> Denies the get_by_id command without any pre-configured scope.",
           "type": "string",
-          "const": "core:tray:allow-set-title"
+          "enum": [
+            "core:tray:deny-get-by-id"
+          ]
         },
         {
-          "description": "Enables the set_tooltip command without any pre-configured scope.",
+          "description": "core:tray:deny-new -> Denies the new command without any pre-configured scope.",
           "type": "string",
-          "const": "core:tray:allow-set-tooltip"
+          "enum": [
+            "core:tray:deny-new"
+          ]
         },
         {
-          "description": "Enables the set_visible command without any pre-configured scope.",
+          "description": "core:tray:deny-remove-by-id -> Denies the remove_by_id command without any pre-configured scope.",
           "type": "string",
-          "const": "core:tray:allow-set-visible"
+          "enum": [
+            "core:tray:deny-remove-by-id"
+          ]
         },
         {
-          "description": "Denies the get_by_id command without any pre-configured scope.",
+          "description": "core:tray:deny-set-icon -> Denies the set_icon command without any pre-configured scope.",
           "type": "string",
-          "const": "core:tray:deny-get-by-id"
+          "enum": [
+            "core:tray:deny-set-icon"
+          ]
         },
         {
-          "description": "Denies the new command without any pre-configured scope.",
+          "description": "core:tray:deny-set-icon-as-template -> Denies the set_icon_as_template command without any pre-configured scope.",
           "type": "string",
-          "const": "core:tray:deny-new"
+          "enum": [
+            "core:tray:deny-set-icon-as-template"
+          ]
         },
         {
-          "description": "Denies the remove_by_id command without any pre-configured scope.",
+          "description": "core:tray:deny-set-menu -> Denies the set_menu command without any pre-configured scope.",
           "type": "string",
-          "const": "core:tray:deny-remove-by-id"
+          "enum": [
+            "core:tray:deny-set-menu"
+          ]
         },
         {
-          "description": "Denies the set_icon command without any pre-configured scope.",
+          "description": "core:tray:deny-set-show-menu-on-left-click -> Denies the set_show_menu_on_left_click command without any pre-configured scope.",
           "type": "string",
-          "const": "core:tray:deny-set-icon"
+          "enum": [
+            "core:tray:deny-set-show-menu-on-left-click"
+          ]
         },
         {
-          "description": "Denies the set_icon_as_template command without any pre-configured scope.",
+          "description": "core:tray:deny-set-temp-dir-path -> Denies the set_temp_dir_path command without any pre-configured scope.",
           "type": "string",
-          "const": "core:tray:deny-set-icon-as-template"
+          "enum": [
+            "core:tray:deny-set-temp-dir-path"
+          ]
         },
         {
-          "description": "Denies the set_menu command without any pre-configured scope.",
+          "description": "core:tray:deny-set-title -> Denies the set_title command without any pre-configured scope.",
           "type": "string",
-          "const": "core:tray:deny-set-menu"
+          "enum": [
+            "core:tray:deny-set-title"
+          ]
         },
         {
-          "description": "Denies the set_show_menu_on_left_click command without any pre-configured scope.",
+          "description": "core:tray:deny-set-tooltip -> Denies the set_tooltip command without any pre-configured scope.",
           "type": "string",
-          "const": "core:tray:deny-set-show-menu-on-left-click"
+          "enum": [
+            "core:tray:deny-set-tooltip"
+          ]
         },
         {
-          "description": "Denies the set_temp_dir_path command without any pre-configured scope.",
+          "description": "core:tray:deny-set-visible -> Denies the set_visible command without any pre-configured scope.",
           "type": "string",
-          "const": "core:tray:deny-set-temp-dir-path"
+          "enum": [
+            "core:tray:deny-set-visible"
+          ]
         },
         {
-          "description": "Denies the set_title command without any pre-configured scope.",
+          "description": "core:webview:default -> Default permissions for the plugin.",
           "type": "string",
-          "const": "core:tray:deny-set-title"
+          "enum": [
+            "core:webview:default"
+          ]
         },
         {
-          "description": "Denies the set_tooltip command without any pre-configured scope.",
+          "description": "core:webview:allow-create-webview -> Enables the create_webview command without any pre-configured scope.",
           "type": "string",
-          "const": "core:tray:deny-set-tooltip"
+          "enum": [
+            "core:webview:allow-create-webview"
+          ]
         },
         {
-          "description": "Denies the set_visible command without any pre-configured scope.",
+          "description": "core:webview:allow-create-webview-window -> Enables the create_webview_window command without any pre-configured scope.",
           "type": "string",
-          "const": "core:tray:deny-set-visible"
+          "enum": [
+            "core:webview:allow-create-webview-window"
+          ]
         },
         {
-          "description": "Default permissions for the plugin.",
+          "description": "core:webview:allow-get-all-webviews -> Enables the get_all_webviews command without any pre-configured scope.",
           "type": "string",
-          "const": "core:webview:default"
+          "enum": [
+            "core:webview:allow-get-all-webviews"
+          ]
         },
         {
-          "description": "Enables the clear_all_browsing_data command without any pre-configured scope.",
+          "description": "core:webview:allow-internal-toggle-devtools -> Enables the internal_toggle_devtools command without any pre-configured scope.",
           "type": "string",
-          "const": "core:webview:allow-clear-all-browsing-data"
+          "enum": [
+            "core:webview:allow-internal-toggle-devtools"
+          ]
         },
         {
-          "description": "Enables the create_webview command without any pre-configured scope.",
+          "description": "core:webview:allow-print -> Enables the print command without any pre-configured scope.",
           "type": "string",
-          "const": "core:webview:allow-create-webview"
+          "enum": [
+            "core:webview:allow-print"
+          ]
         },
         {
-          "description": "Enables the create_webview_window command without any pre-configured scope.",
+          "description": "core:webview:allow-reparent -> Enables the reparent command without any pre-configured scope.",
           "type": "string",
-          "const": "core:webview:allow-create-webview-window"
+          "enum": [
+            "core:webview:allow-reparent"
+          ]
         },
         {
-          "description": "Enables the get_all_webviews command without any pre-configured scope.",
+          "description": "core:webview:allow-set-webview-focus -> Enables the set_webview_focus command without any pre-configured scope.",
           "type": "string",
-          "const": "core:webview:allow-get-all-webviews"
+          "enum": [
+            "core:webview:allow-set-webview-focus"
+          ]
         },
         {
-          "description": "Enables the internal_toggle_devtools command without any pre-configured scope.",
+          "description": "core:webview:allow-set-webview-position -> Enables the set_webview_position command without any pre-configured scope.",
           "type": "string",
-          "const": "core:webview:allow-internal-toggle-devtools"
+          "enum": [
+            "core:webview:allow-set-webview-position"
+          ]
         },
         {
-          "description": "Enables the print command without any pre-configured scope.",
+          "description": "core:webview:allow-set-webview-size -> Enables the set_webview_size command without any pre-configured scope.",
           "type": "string",
-          "const": "core:webview:allow-print"
+          "enum": [
+            "core:webview:allow-set-webview-size"
+          ]
         },
         {
-          "description": "Enables the reparent command without any pre-configured scope.",
+          "description": "core:webview:allow-set-webview-zoom -> Enables the set_webview_zoom command without any pre-configured scope.",
           "type": "string",
-          "const": "core:webview:allow-reparent"
+          "enum": [
+            "core:webview:allow-set-webview-zoom"
+          ]
         },
         {
-          "description": "Enables the set_webview_focus command without any pre-configured scope.",
+          "description": "core:webview:allow-webview-close -> Enables the webview_close command without any pre-configured scope.",
           "type": "string",
-          "const": "core:webview:allow-set-webview-focus"
+          "enum": [
+            "core:webview:allow-webview-close"
+          ]
         },
         {
-          "description": "Enables the set_webview_position command without any pre-configured scope.",
+          "description": "core:webview:allow-webview-position -> Enables the webview_position command without any pre-configured scope.",
           "type": "string",
-          "const": "core:webview:allow-set-webview-position"
+          "enum": [
+            "core:webview:allow-webview-position"
+          ]
         },
         {
-          "description": "Enables the set_webview_size command without any pre-configured scope.",
+          "description": "core:webview:allow-webview-size -> Enables the webview_size command without any pre-configured scope.",
           "type": "string",
-          "const": "core:webview:allow-set-webview-size"
+          "enum": [
+            "core:webview:allow-webview-size"
+          ]
         },
         {
-          "description": "Enables the set_webview_zoom command without any pre-configured scope.",
+          "description": "core:webview:deny-create-webview -> Denies the create_webview command without any pre-configured scope.",
           "type": "string",
-          "const": "core:webview:allow-set-webview-zoom"
+          "enum": [
+            "core:webview:deny-create-webview"
+          ]
         },
         {
-          "description": "Enables the webview_close command without any pre-configured scope.",
+          "description": "core:webview:deny-create-webview-window -> Denies the create_webview_window command without any pre-configured scope.",
           "type": "string",
-          "const": "core:webview:allow-webview-close"
+          "enum": [
+            "core:webview:deny-create-webview-window"
+          ]
         },
         {
-          "description": "Enables the webview_hide command without any pre-configured scope.",
+          "description": "core:webview:deny-get-all-webviews -> Denies the get_all_webviews command without any pre-configured scope.",
           "type": "string",
-          "const": "core:webview:allow-webview-hide"
+          "enum": [
+            "core:webview:deny-get-all-webviews"
+          ]
         },
         {
-          "description": "Enables the webview_position command without any pre-configured scope.",
+          "description": "core:webview:deny-internal-toggle-devtools -> Denies the internal_toggle_devtools command without any pre-configured scope.",
           "type": "string",
-          "const": "core:webview:allow-webview-position"
+          "enum": [
+            "core:webview:deny-internal-toggle-devtools"
+          ]
         },
         {
-          "description": "Enables the webview_show command without any pre-configured scope.",
+          "description": "core:webview:deny-print -> Denies the print command without any pre-configured scope.",
           "type": "string",
-          "const": "core:webview:allow-webview-show"
+          "enum": [
+            "core:webview:deny-print"
+          ]
         },
         {
-          "description": "Enables the webview_size command without any pre-configured scope.",
+          "description": "core:webview:deny-reparent -> Denies the reparent command without any pre-configured scope.",
           "type": "string",
-          "const": "core:webview:allow-webview-size"
+          "enum": [
+            "core:webview:deny-reparent"
+          ]
         },
         {
-          "description": "Denies the clear_all_browsing_data command without any pre-configured scope.",
+          "description": "core:webview:deny-set-webview-focus -> Denies the set_webview_focus command without any pre-configured scope.",
           "type": "string",
-          "const": "core:webview:deny-clear-all-browsing-data"
+          "enum": [
+            "core:webview:deny-set-webview-focus"
+          ]
         },
         {
-          "description": "Denies the create_webview command without any pre-configured scope.",
+          "description": "core:webview:deny-set-webview-position -> Denies the set_webview_position command without any pre-configured scope.",
           "type": "string",
-          "const": "core:webview:deny-create-webview"
+          "enum": [
+            "core:webview:deny-set-webview-position"
+          ]
         },
         {
-          "description": "Denies the create_webview_window command without any pre-configured scope.",
+          "description": "core:webview:deny-set-webview-size -> Denies the set_webview_size command without any pre-configured scope.",
           "type": "string",
-          "const": "core:webview:deny-create-webview-window"
+          "enum": [
+            "core:webview:deny-set-webview-size"
+          ]
         },
         {
-          "description": "Denies the get_all_webviews command without any pre-configured scope.",
+          "description": "core:webview:deny-set-webview-zoom -> Denies the set_webview_zoom command without any pre-configured scope.",
           "type": "string",
-          "const": "core:webview:deny-get-all-webviews"
+          "enum": [
+            "core:webview:deny-set-webview-zoom"
+          ]
         },
         {
-          "description": "Denies the internal_toggle_devtools command without any pre-configured scope.",
+          "description": "core:webview:deny-webview-close -> Denies the webview_close command without any pre-configured scope.",
           "type": "string",
-          "const": "core:webview:deny-internal-toggle-devtools"
+          "enum": [
+            "core:webview:deny-webview-close"
+          ]
         },
         {
-          "description": "Denies the print command without any pre-configured scope.",
+          "description": "core:webview:deny-webview-position -> Denies the webview_position command without any pre-configured scope.",
           "type": "string",
-          "const": "core:webview:deny-print"
+          "enum": [
+            "core:webview:deny-webview-position"
+          ]
         },
         {
-          "description": "Denies the reparent command without any pre-configured scope.",
+          "description": "core:webview:deny-webview-size -> Denies the webview_size command without any pre-configured scope.",
           "type": "string",
-          "const": "core:webview:deny-reparent"
+          "enum": [
+            "core:webview:deny-webview-size"
+          ]
         },
         {
-          "description": "Denies the set_webview_focus command without any pre-configured scope.",
+          "description": "core:window:default -> Default permissions for the plugin.",
           "type": "string",
-          "const": "core:webview:deny-set-webview-focus"
+          "enum": [
+            "core:window:default"
+          ]
         },
         {
-          "description": "Denies the set_webview_position command without any pre-configured scope.",
+          "description": "core:window:allow-available-monitors -> Enables the available_monitors command without any pre-configured scope.",
           "type": "string",
-          "const": "core:webview:deny-set-webview-position"
+          "enum": [
+            "core:window:allow-available-monitors"
+          ]
         },
         {
-          "description": "Denies the set_webview_size command without any pre-configured scope.",
+          "description": "core:window:allow-center -> Enables the center command without any pre-configured scope.",
           "type": "string",
-          "const": "core:webview:deny-set-webview-size"
+          "enum": [
+            "core:window:allow-center"
+          ]
         },
         {
-          "description": "Denies the set_webview_zoom command without any pre-configured scope.",
+          "description": "core:window:allow-close -> Enables the close command without any pre-configured scope.",
           "type": "string",
-          "const": "core:webview:deny-set-webview-zoom"
+          "enum": [
+            "core:window:allow-close"
+          ]
         },
         {
-          "description": "Denies the webview_close command without any pre-configured scope.",
+          "description": "core:window:allow-create -> Enables the create command without any pre-configured scope.",
           "type": "string",
-          "const": "core:webview:deny-webview-close"
+          "enum": [
+            "core:window:allow-create"
+          ]
         },
         {
-          "description": "Denies the webview_hide command without any pre-configured scope.",
+          "description": "core:window:allow-current-monitor -> Enables the current_monitor command without any pre-configured scope.",
           "type": "string",
-          "const": "core:webview:deny-webview-hide"
+          "enum": [
+            "core:window:allow-current-monitor"
+          ]
         },
         {
-          "description": "Denies the webview_position command without any pre-configured scope.",
+          "description": "core:window:allow-cursor-position -> Enables the cursor_position command without any pre-configured scope.",
           "type": "string",
-          "const": "core:webview:deny-webview-position"
+          "enum": [
+            "core:window:allow-cursor-position"
+          ]
         },
         {
-          "description": "Denies the webview_show command without any pre-configured scope.",
+          "description": "core:window:allow-destroy -> Enables the destroy command without any pre-configured scope.",
           "type": "string",
-          "const": "core:webview:deny-webview-show"
+          "enum": [
+            "core:window:allow-destroy"
+          ]
         },
         {
-          "description": "Denies the webview_size command without any pre-configured scope.",
+          "description": "core:window:allow-get-all-windows -> Enables the get_all_windows command without any pre-configured scope.",
           "type": "string",
-          "const": "core:webview:deny-webview-size"
+          "enum": [
+            "core:window:allow-get-all-windows"
+          ]
         },
         {
-          "description": "Default permissions for the plugin.",
+          "description": "core:window:allow-hide -> Enables the hide command without any pre-configured scope.",
           "type": "string",
-          "const": "core:window:default"
+          "enum": [
+            "core:window:allow-hide"
+          ]
         },
         {
-          "description": "Enables the available_monitors command without any pre-configured scope.",
+          "description": "core:window:allow-inner-position -> Enables the inner_position command without any pre-configured scope.",
           "type": "string",
-          "const": "core:window:allow-available-monitors"
+          "enum": [
+            "core:window:allow-inner-position"
+          ]
         },
         {
-          "description": "Enables the center command without any pre-configured scope.",
+          "description": "core:window:allow-inner-size -> Enables the inner_size command without any pre-configured scope.",
           "type": "string",
-          "const": "core:window:allow-center"
+          "enum": [
+            "core:window:allow-inner-size"
+          ]
         },
         {
-          "description": "Enables the close command without any pre-configured scope.",
+          "description": "core:window:allow-internal-toggle-maximize -> Enables the internal_toggle_maximize command without any pre-configured scope.",
           "type": "string",
-          "const": "core:window:allow-close"
+          "enum": [
+            "core:window:allow-internal-toggle-maximize"
+          ]
         },
         {
-          "description": "Enables the create command without any pre-configured scope.",
+          "description": "core:window:allow-is-closable -> Enables the is_closable command without any pre-configured scope.",
           "type": "string",
-          "const": "core:window:allow-create"
+          "enum": [
+            "core:window:allow-is-closable"
+          ]
         },
         {
-          "description": "Enables the current_monitor command without any pre-configured scope.",
+          "description": "core:window:allow-is-decorated -> Enables the is_decorated command without any pre-configured scope.",
           "type": "string",
-          "const": "core:window:allow-current-monitor"
+          "enum": [
+            "core:window:allow-is-decorated"
+          ]
         },
         {
-          "description": "Enables the cursor_position command without any pre-configured scope.",
+          "description": "core:window:allow-is-focused -> Enables the is_focused command without any pre-configured scope.",
           "type": "string",
-          "const": "core:window:allow-cursor-position"
+          "enum": [
+            "core:window:allow-is-focused"
+          ]
         },
         {
-          "description": "Enables the destroy command without any pre-configured scope.",
+          "description": "core:window:allow-is-fullscreen -> Enables the is_fullscreen command without any pre-configured scope.",
           "type": "string",
-          "const": "core:window:allow-destroy"
+          "enum": [
+            "core:window:allow-is-fullscreen"
+          ]
         },
         {
-          "description": "Enables the get_all_windows command without any pre-configured scope.",
+          "description": "core:window:allow-is-maximizable -> Enables the is_maximizable command without any pre-configured scope.",
           "type": "string",
-          "const": "core:window:allow-get-all-windows"
+          "enum": [
+            "core:window:allow-is-maximizable"
+          ]
         },
         {
-          "description": "Enables the hide command without any pre-configured scope.",
+          "description": "core:window:allow-is-maximized -> Enables the is_maximized command without any pre-configured scope.",
           "type": "string",
-          "const": "core:window:allow-hide"
+          "enum": [
+            "core:window:allow-is-maximized"
+          ]
         },
         {
-          "description": "Enables the inner_position command without any pre-configured scope.",
+          "description": "core:window:allow-is-minimizable -> Enables the is_minimizable command without any pre-configured scope.",
           "type": "string",
-          "const": "core:window:allow-inner-position"
+          "enum": [
+            "core:window:allow-is-minimizable"
+          ]
         },
         {
-          "description": "Enables the inner_size command without any pre-configured scope.",
+          "description": "core:window:allow-is-minimized -> Enables the is_minimized command without any pre-configured scope.",
           "type": "string",
-          "const": "core:window:allow-inner-size"
+          "enum": [
+            "core:window:allow-is-minimized"
+          ]
         },
         {
-          "description": "Enables the internal_toggle_maximize command without any pre-configured scope.",
+          "description": "core:window:allow-is-resizable -> Enables the is_resizable command without any pre-configured scope.",
           "type": "string",
-          "const": "core:window:allow-internal-toggle-maximize"
+          "enum": [
+            "core:window:allow-is-resizable"
+          ]
         },
         {
-          "description": "Enables the is_closable command without any pre-configured scope.",
+          "description": "core:window:allow-is-visible -> Enables the is_visible command without any pre-configured scope.",
           "type": "string",
-          "const": "core:window:allow-is-closable"
+          "enum": [
+            "core:window:allow-is-visible"
+          ]
         },
         {
-          "description": "Enables the is_decorated command without any pre-configured scope.",
+          "description": "core:window:allow-maximize -> Enables the maximize command without any pre-configured scope.",
           "type": "string",
-          "const": "core:window:allow-is-decorated"
+          "enum": [
+            "core:window:allow-maximize"
+          ]
         },
         {
-          "description": "Enables the is_enabled command without any pre-configured scope.",
+          "description": "core:window:allow-minimize -> Enables the minimize command without any pre-configured scope.",
           "type": "string",
-          "const": "core:window:allow-is-enabled"
+          "enum": [
+            "core:window:allow-minimize"
+          ]
         },
         {
-          "description": "Enables the is_focused command without any pre-configured scope.",
+          "description": "core:window:allow-monitor-from-point -> Enables the monitor_from_point command without any pre-configured scope.",
           "type": "string",
-          "const": "core:window:allow-is-focused"
+          "enum": [
+            "core:window:allow-monitor-from-point"
+          ]
         },
         {
-          "description": "Enables the is_fullscreen command without any pre-configured scope.",
+          "description": "core:window:allow-outer-position -> Enables the outer_position command without any pre-configured scope.",
           "type": "string",
-          "const": "core:window:allow-is-fullscreen"
+          "enum": [
+            "core:window:allow-outer-position"
+          ]
         },
         {
-          "description": "Enables the is_maximizable command without any pre-configured scope.",
+          "description": "core:window:allow-outer-size -> Enables the outer_size command without any pre-configured scope.",
           "type": "string",
-          "const": "core:window:allow-is-maximizable"
+          "enum": [
+            "core:window:allow-outer-size"
+          ]
         },
         {
-          "description": "Enables the is_maximized command without any pre-configured scope.",
+          "description": "core:window:allow-primary-monitor -> Enables the primary_monitor command without any pre-configured scope.",
           "type": "string",
-          "const": "core:window:allow-is-maximized"
+          "enum": [
+            "core:window:allow-primary-monitor"
+          ]
         },
         {
-          "description": "Enables the is_minimizable command without any pre-configured scope.",
+          "description": "core:window:allow-request-user-attention -> Enables the request_user_attention command without any pre-configured scope.",
           "type": "string",
-          "const": "core:window:allow-is-minimizable"
+          "enum": [
+            "core:window:allow-request-user-attention"
+          ]
         },
         {
-          "description": "Enables the is_minimized command without any pre-configured scope.",
+          "description": "core:window:allow-scale-factor -> Enables the scale_factor command without any pre-configured scope.",
           "type": "string",
-          "const": "core:window:allow-is-minimized"
+          "enum": [
+            "core:window:allow-scale-factor"
+          ]
         },
         {
-          "description": "Enables the is_resizable command without any pre-configured scope.",
+          "description": "core:window:allow-set-always-on-bottom -> Enables the set_always_on_bottom command without any pre-configured scope.",
           "type": "string",
-          "const": "core:window:allow-is-resizable"
+          "enum": [
+            "core:window:allow-set-always-on-bottom"
+          ]
         },
         {
-          "description": "Enables the is_visible command without any pre-configured scope.",
+          "description": "core:window:allow-set-always-on-top -> Enables the set_always_on_top command without any pre-configured scope.",
           "type": "string",
-          "const": "core:window:allow-is-visible"
+          "enum": [
+            "core:window:allow-set-always-on-top"
+          ]
         },
         {
-          "description": "Enables the maximize command without any pre-configured scope.",
+          "description": "core:window:allow-set-closable -> Enables the set_closable command without any pre-configured scope.",
           "type": "string",
-          "const": "core:window:allow-maximize"
+          "enum": [
+            "core:window:allow-set-closable"
+          ]
         },
         {
-          "description": "Enables the minimize command without any pre-configured scope.",
+          "description": "core:window:allow-set-content-protected -> Enables the set_content_protected command without any pre-configured scope.",
           "type": "string",
-          "const": "core:window:allow-minimize"
+          "enum": [
+            "core:window:allow-set-content-protected"
+          ]
         },
         {
-          "description": "Enables the monitor_from_point command without any pre-configured scope.",
+          "description": "core:window:allow-set-cursor-grab -> Enables the set_cursor_grab command without any pre-configured scope.",
           "type": "string",
-          "const": "core:window:allow-monitor-from-point"
+          "enum": [
+            "core:window:allow-set-cursor-grab"
+          ]
         },
         {
-          "description": "Enables the outer_position command without any pre-configured scope.",
+          "description": "core:window:allow-set-cursor-icon -> Enables the set_cursor_icon command without any pre-configured scope.",
           "type": "string",
-          "const": "core:window:allow-outer-position"
+          "enum": [
+            "core:window:allow-set-cursor-icon"
+          ]
         },
         {
-          "description": "Enables the outer_size command without any pre-configured scope.",
+          "description": "core:window:allow-set-cursor-position -> Enables the set_cursor_position command without any pre-configured scope.",
           "type": "string",
-          "const": "core:window:allow-outer-size"
+          "enum": [
+            "core:window:allow-set-cursor-position"
+          ]
         },
         {
-          "description": "Enables the primary_monitor command without any pre-configured scope.",
+          "description": "core:window:allow-set-cursor-visible -> Enables the set_cursor_visible command without any pre-configured scope.",
           "type": "string",
-          "const": "core:window:allow-primary-monitor"
+          "enum": [
+            "core:window:allow-set-cursor-visible"
+          ]
         },
         {
-          "description": "Enables the request_user_attention command without any pre-configured scope.",
+          "description": "core:window:allow-set-decorations -> Enables the set_decorations command without any pre-configured scope.",
           "type": "string",
-          "const": "core:window:allow-request-user-attention"
+          "enum": [
+            "core:window:allow-set-decorations"
+          ]
         },
         {
-          "description": "Enables the scale_factor command without any pre-configured scope.",
+          "description": "core:window:allow-set-effects -> Enables the set_effects command without any pre-configured scope.",
           "type": "string",
-          "const": "core:window:allow-scale-factor"
+          "enum": [
+            "core:window:allow-set-effects"
+          ]
         },
         {
-          "description": "Enables the set_always_on_bottom command without any pre-configured scope.",
+          "description": "core:window:allow-set-focus -> Enables the set_focus command without any pre-configured scope.",
           "type": "string",
-          "const": "core:window:allow-set-always-on-bottom"
+          "enum": [
+            "core:window:allow-set-focus"
+          ]
         },
         {
-          "description": "Enables the set_always_on_top command without any pre-configured scope.",
+          "description": "core:window:allow-set-fullscreen -> Enables the set_fullscreen command without any pre-configured scope.",
           "type": "string",
-          "const": "core:window:allow-set-always-on-top"
+          "enum": [
+            "core:window:allow-set-fullscreen"
+          ]
         },
         {
-          "description": "Enables the set_closable command without any pre-configured scope.",
+          "description": "core:window:allow-set-icon -> Enables the set_icon command without any pre-configured scope.",
           "type": "string",
-          "const": "core:window:allow-set-closable"
+          "enum": [
+            "core:window:allow-set-icon"
+          ]
         },
         {
-          "description": "Enables the set_content_protected command without any pre-configured scope.",
+          "description": "core:window:allow-set-ignore-cursor-events -> Enables the set_ignore_cursor_events command without any pre-configured scope.",
           "type": "string",
-          "const": "core:window:allow-set-content-protected"
+          "enum": [
+            "core:window:allow-set-ignore-cursor-events"
+          ]
         },
         {
-          "description": "Enables the set_cursor_grab command without any pre-configured scope.",
+          "description": "core:window:allow-set-max-size -> Enables the set_max_size command without any pre-configured scope.",
           "type": "string",
-          "const": "core:window:allow-set-cursor-grab"
+          "enum": [
+            "core:window:allow-set-max-size"
+          ]
         },
         {
-          "description": "Enables the set_cursor_icon command without any pre-configured scope.",
+          "description": "core:window:allow-set-maximizable -> Enables the set_maximizable command without any pre-configured scope.",
           "type": "string",
-          "const": "core:window:allow-set-cursor-icon"
+          "enum": [
+            "core:window:allow-set-maximizable"
+          ]
         },
         {
-          "description": "Enables the set_cursor_position command without any pre-configured scope.",
+          "description": "core:window:allow-set-min-size -> Enables the set_min_size command without any pre-configured scope.",
           "type": "string",
-          "const": "core:window:allow-set-cursor-position"
+          "enum": [
+            "core:window:allow-set-min-size"
+          ]
         },
         {
-          "description": "Enables the set_cursor_visible command without any pre-configured scope.",
+          "description": "core:window:allow-set-minimizable -> Enables the set_minimizable command without any pre-configured scope.",
           "type": "string",
-          "const": "core:window:allow-set-cursor-visible"
+          "enum": [
+            "core:window:allow-set-minimizable"
+          ]
         },
         {
-          "description": "Enables the set_decorations command without any pre-configured scope.",
+          "description": "core:window:allow-set-position -> Enables the set_position command without any pre-configured scope.",
           "type": "string",
-          "const": "core:window:allow-set-decorations"
+          "enum": [
+            "core:window:allow-set-position"
+          ]
         },
         {
-          "description": "Enables the set_effects command without any pre-configured scope.",
+          "description": "core:window:allow-set-progress-bar -> Enables the set_progress_bar command without any pre-configured scope.",
           "type": "string",
-          "const": "core:window:allow-set-effects"
+          "enum": [
+            "core:window:allow-set-progress-bar"
+          ]
         },
         {
-          "description": "Enables the set_enabled command without any pre-configured scope.",
+          "description": "core:window:allow-set-resizable -> Enables the set_resizable command without any pre-configured scope.",
           "type": "string",
-          "const": "core:window:allow-set-enabled"
+          "enum": [
+            "core:window:allow-set-resizable"
+          ]
         },
         {
-          "description": "Enables the set_focus command without any pre-configured scope.",
+          "description": "core:window:allow-set-shadow -> Enables the set_shadow command without any pre-configured scope.",
           "type": "string",
-          "const": "core:window:allow-set-focus"
+          "enum": [
+            "core:window:allow-set-shadow"
+          ]
         },
         {
-          "description": "Enables the set_fullscreen command without any pre-configured scope.",
+          "description": "core:window:allow-set-size -> Enables the set_size command without any pre-configured scope.",
           "type": "string",
-          "const": "core:window:allow-set-fullscreen"
+          "enum": [
+            "core:window:allow-set-size"
+          ]
         },
         {
-          "description": "Enables the set_icon command without any pre-configured scope.",
+          "description": "core:window:allow-set-size-constraints -> Enables the set_size_constraints command without any pre-configured scope.",
           "type": "string",
-          "const": "core:window:allow-set-icon"
+          "enum": [
+            "core:window:allow-set-size-constraints"
+          ]
         },
         {
-          "description": "Enables the set_ignore_cursor_events command without any pre-configured scope.",
+          "description": "core:window:allow-set-skip-taskbar -> Enables the set_skip_taskbar command without any pre-configured scope.",
           "type": "string",
-          "const": "core:window:allow-set-ignore-cursor-events"
+          "enum": [
+            "core:window:allow-set-skip-taskbar"
+          ]
         },
         {
-          "description": "Enables the set_max_size command without any pre-configured scope.",
+          "description": "core:window:allow-set-title -> Enables the set_title command without any pre-configured scope.",
           "type": "string",
-          "const": "core:window:allow-set-max-size"
+          "enum": [
+            "core:window:allow-set-title"
+          ]
         },
         {
-          "description": "Enables the set_maximizable command without any pre-configured scope.",
+          "description": "core:window:allow-set-title-bar-style -> Enables the set_title_bar_style command without any pre-configured scope.",
           "type": "string",
-          "const": "core:window:allow-set-maximizable"
+          "enum": [
+            "core:window:allow-set-title-bar-style"
+          ]
         },
         {
-          "description": "Enables the set_min_size command without any pre-configured scope.",
+          "description": "core:window:allow-set-visible-on-all-workspaces -> Enables the set_visible_on_all_workspaces command without any pre-configured scope.",
           "type": "string",
-          "const": "core:window:allow-set-min-size"
+          "enum": [
+            "core:window:allow-set-visible-on-all-workspaces"
+          ]
         },
         {
-          "description": "Enables the set_minimizable command without any pre-configured scope.",
+          "description": "core:window:allow-show -> Enables the show command without any pre-configured scope.",
           "type": "string",
-          "const": "core:window:allow-set-minimizable"
+          "enum": [
+            "core:window:allow-show"
+          ]
         },
         {
-          "description": "Enables the set_position command without any pre-configured scope.",
+          "description": "core:window:allow-start-dragging -> Enables the start_dragging command without any pre-configured scope.",
           "type": "string",
-          "const": "core:window:allow-set-position"
+          "enum": [
+            "core:window:allow-start-dragging"
+          ]
         },
         {
-          "description": "Enables the set_progress_bar command without any pre-configured scope.",
+          "description": "core:window:allow-start-resize-dragging -> Enables the start_resize_dragging command without any pre-configured scope.",
           "type": "string",
-          "const": "core:window:allow-set-progress-bar"
+          "enum": [
+            "core:window:allow-start-resize-dragging"
+          ]
         },
         {
-          "description": "Enables the set_resizable command without any pre-configured scope.",
+          "description": "core:window:allow-theme -> Enables the theme command without any pre-configured scope.",
           "type": "string",
-          "const": "core:window:allow-set-resizable"
+          "enum": [
+            "core:window:allow-theme"
+          ]
         },
         {
-          "description": "Enables the set_shadow command without any pre-configured scope.",
+          "description": "core:window:allow-title -> Enables the title command without any pre-configured scope.",
           "type": "string",
-          "const": "core:window:allow-set-shadow"
+          "enum": [
+            "core:window:allow-title"
+          ]
         },
         {
-          "description": "Enables the set_size command without any pre-configured scope.",
+          "description": "core:window:allow-toggle-maximize -> Enables the toggle_maximize command without any pre-configured scope.",
           "type": "string",
-          "const": "core:window:allow-set-size"
+          "enum": [
+            "core:window:allow-toggle-maximize"
+          ]
         },
         {
-          "description": "Enables the set_size_constraints command without any pre-configured scope.",
+          "description": "core:window:allow-unmaximize -> Enables the unmaximize command without any pre-configured scope.",
           "type": "string",
-          "const": "core:window:allow-set-size-constraints"
+          "enum": [
+            "core:window:allow-unmaximize"
+          ]
         },
         {
-          "description": "Enables the set_skip_taskbar command without any pre-configured scope.",
+          "description": "core:window:allow-unminimize -> Enables the unminimize command without any pre-configured scope.",
           "type": "string",
-          "const": "core:window:allow-set-skip-taskbar"
+          "enum": [
+            "core:window:allow-unminimize"
+          ]
         },
         {
-          "description": "Enables the set_theme command without any pre-configured scope.",
+          "description": "core:window:deny-available-monitors -> Denies the available_monitors command without any pre-configured scope.",
           "type": "string",
-          "const": "core:window:allow-set-theme"
+          "enum": [
+            "core:window:deny-available-monitors"
+          ]
         },
         {
-          "description": "Enables the set_title command without any pre-configured scope.",
+          "description": "core:window:deny-center -> Denies the center command without any pre-configured scope.",
           "type": "string",
-          "const": "core:window:allow-set-title"
+          "enum": [
+            "core:window:deny-center"
+          ]
         },
         {
-          "description": "Enables the set_title_bar_style command without any pre-configured scope.",
+          "description": "core:window:deny-close -> Denies the close command without any pre-configured scope.",
           "type": "string",
-          "const": "core:window:allow-set-title-bar-style"
+          "enum": [
+            "core:window:deny-close"
+          ]
         },
         {
-          "description": "Enables the set_visible_on_all_workspaces command without any pre-configured scope.",
+          "description": "core:window:deny-create -> Denies the create command without any pre-configured scope.",
           "type": "string",
-          "const": "core:window:allow-set-visible-on-all-workspaces"
+          "enum": [
+            "core:window:deny-create"
+          ]
         },
         {
-          "description": "Enables the show command without any pre-configured scope.",
+          "description": "core:window:deny-current-monitor -> Denies the current_monitor command without any pre-configured scope.",
           "type": "string",
-          "const": "core:window:allow-show"
+          "enum": [
+            "core:window:deny-current-monitor"
+          ]
         },
         {
-          "description": "Enables the start_dragging command without any pre-configured scope.",
+          "description": "core:window:deny-cursor-position -> Denies the cursor_position command without any pre-configured scope.",
           "type": "string",
-          "const": "core:window:allow-start-dragging"
+          "enum": [
+            "core:window:deny-cursor-position"
+          ]
         },
         {
-          "description": "Enables the start_resize_dragging command without any pre-configured scope.",
+          "description": "core:window:deny-destroy -> Denies the destroy command without any pre-configured scope.",
           "type": "string",
-          "const": "core:window:allow-start-resize-dragging"
+          "enum": [
+            "core:window:deny-destroy"
+          ]
         },
         {
-          "description": "Enables the theme command without any pre-configured scope.",
+          "description": "core:window:deny-get-all-windows -> Denies the get_all_windows command without any pre-configured scope.",
           "type": "string",
-          "const": "core:window:allow-theme"
+          "enum": [
+            "core:window:deny-get-all-windows"
+          ]
         },
         {
-          "description": "Enables the title command without any pre-configured scope.",
+          "description": "core:window:deny-hide -> Denies the hide command without any pre-configured scope.",
           "type": "string",
-          "const": "core:window:allow-title"
+          "enum": [
+            "core:window:deny-hide"
+          ]
         },
         {
-          "description": "Enables the toggle_maximize command without any pre-configured scope.",
+          "description": "core:window:deny-inner-position -> Denies the inner_position command without any pre-configured scope.",
           "type": "string",
-          "const": "core:window:allow-toggle-maximize"
+          "enum": [
+            "core:window:deny-inner-position"
+          ]
         },
         {
-          "description": "Enables the unmaximize command without any pre-configured scope.",
+          "description": "core:window:deny-inner-size -> Denies the inner_size command without any pre-configured scope.",
           "type": "string",
-          "const": "core:window:allow-unmaximize"
+          "enum": [
+            "core:window:deny-inner-size"
+          ]
         },
         {
-          "description": "Enables the unminimize command without any pre-configured scope.",
+          "description": "core:window:deny-internal-toggle-maximize -> Denies the internal_toggle_maximize command without any pre-configured scope.",
           "type": "string",
-          "const": "core:window:allow-unminimize"
+          "enum": [
+            "core:window:deny-internal-toggle-maximize"
+          ]
         },
         {
-          "description": "Denies the available_monitors command without any pre-configured scope.",
+          "description": "core:window:deny-is-closable -> Denies the is_closable command without any pre-configured scope.",
           "type": "string",
-          "const": "core:window:deny-available-monitors"
+          "enum": [
+            "core:window:deny-is-closable"
+          ]
         },
         {
-          "description": "Denies the center command without any pre-configured scope.",
+          "description": "core:window:deny-is-decorated -> Denies the is_decorated command without any pre-configured scope.",
           "type": "string",
-          "const": "core:window:deny-center"
+          "enum": [
+            "core:window:deny-is-decorated"
+          ]
         },
         {
-          "description": "Denies the close command without any pre-configured scope.",
+          "description": "core:window:deny-is-focused -> Denies the is_focused command without any pre-configured scope.",
           "type": "string",
-          "const": "core:window:deny-close"
+          "enum": [
+            "core:window:deny-is-focused"
+          ]
         },
         {
-          "description": "Denies the create command without any pre-configured scope.",
+          "description": "core:window:deny-is-fullscreen -> Denies the is_fullscreen command without any pre-configured scope.",
           "type": "string",
-          "const": "core:window:deny-create"
+          "enum": [
+            "core:window:deny-is-fullscreen"
+          ]
         },
         {
-          "description": "Denies the current_monitor command without any pre-configured scope.",
+          "description": "core:window:deny-is-maximizable -> Denies the is_maximizable command without any pre-configured scope.",
           "type": "string",
-          "const": "core:window:deny-current-monitor"
+          "enum": [
+            "core:window:deny-is-maximizable"
+          ]
         },
         {
-          "description": "Denies the cursor_position command without any pre-configured scope.",
+          "description": "core:window:deny-is-maximized -> Denies the is_maximized command without any pre-configured scope.",
           "type": "string",
-          "const": "core:window:deny-cursor-position"
+          "enum": [
+            "core:window:deny-is-maximized"
+          ]
         },
         {
-          "description": "Denies the destroy command without any pre-configured scope.",
+          "description": "core:window:deny-is-minimizable -> Denies the is_minimizable command without any pre-configured scope.",
           "type": "string",
-          "const": "core:window:deny-destroy"
+          "enum": [
+            "core:window:deny-is-minimizable"
+          ]
         },
         {
-          "description": "Denies the get_all_windows command without any pre-configured scope.",
+          "description": "core:window:deny-is-minimized -> Denies the is_minimized command without any pre-configured scope.",
           "type": "string",
-          "const": "core:window:deny-get-all-windows"
+          "enum": [
+            "core:window:deny-is-minimized"
+          ]
         },
         {
-          "description": "Denies the hide command without any pre-configured scope.",
+          "description": "core:window:deny-is-resizable -> Denies the is_resizable command without any pre-configured scope.",
           "type": "string",
-          "const": "core:window:deny-hide"
+          "enum": [
+            "core:window:deny-is-resizable"
+          ]
         },
         {
-          "description": "Denies the inner_position command without any pre-configured scope.",
+          "description": "core:window:deny-is-visible -> Denies the is_visible command without any pre-configured scope.",
           "type": "string",
-          "const": "core:window:deny-inner-position"
+          "enum": [
+            "core:window:deny-is-visible"
+          ]
         },
         {
-          "description": "Denies the inner_size command without any pre-configured scope.",
+          "description": "core:window:deny-maximize -> Denies the maximize command without any pre-configured scope.",
           "type": "string",
-          "const": "core:window:deny-inner-size"
+          "enum": [
+            "core:window:deny-maximize"
+          ]
         },
         {
-          "description": "Denies the internal_toggle_maximize command without any pre-configured scope.",
+          "description": "core:window:deny-minimize -> Denies the minimize command without any pre-configured scope.",
           "type": "string",
-          "const": "core:window:deny-internal-toggle-maximize"
+          "enum": [
+            "core:window:deny-minimize"
+          ]
         },
         {
-          "description": "Denies the is_closable command without any pre-configured scope.",
+          "description": "core:window:deny-monitor-from-point -> Denies the monitor_from_point command without any pre-configured scope.",
           "type": "string",
-          "const": "core:window:deny-is-closable"
+          "enum": [
+            "core:window:deny-monitor-from-point"
+          ]
         },
         {
-          "description": "Denies the is_decorated command without any pre-configured scope.",
+          "description": "core:window:deny-outer-position -> Denies the outer_position command without any pre-configured scope.",
           "type": "string",
-          "const": "core:window:deny-is-decorated"
+          "enum": [
+            "core:window:deny-outer-position"
+          ]
         },
         {
-          "description": "Denies the is_enabled command without any pre-configured scope.",
+          "description": "core:window:deny-outer-size -> Denies the outer_size command without any pre-configured scope.",
           "type": "string",
-          "const": "core:window:deny-is-enabled"
+          "enum": [
+            "core:window:deny-outer-size"
+          ]
         },
         {
-          "description": "Denies the is_focused command without any pre-configured scope.",
+          "description": "core:window:deny-primary-monitor -> Denies the primary_monitor command without any pre-configured scope.",
           "type": "string",
-          "const": "core:window:deny-is-focused"
+          "enum": [
+            "core:window:deny-primary-monitor"
+          ]
         },
         {
-          "description": "Denies the is_fullscreen command without any pre-configured scope.",
+          "description": "core:window:deny-request-user-attention -> Denies the request_user_attention command without any pre-configured scope.",
           "type": "string",
-          "const": "core:window:deny-is-fullscreen"
+          "enum": [
+            "core:window:deny-request-user-attention"
+          ]
         },
         {
-          "description": "Denies the is_maximizable command without any pre-configured scope.",
+          "description": "core:window:deny-scale-factor -> Denies the scale_factor command without any pre-configured scope.",
           "type": "string",
-          "const": "core:window:deny-is-maximizable"
+          "enum": [
+            "core:window:deny-scale-factor"
+          ]
         },
         {
-          "description": "Denies the is_maximized command without any pre-configured scope.",
+          "description": "core:window:deny-set-always-on-bottom -> Denies the set_always_on_bottom command without any pre-configured scope.",
           "type": "string",
-          "const": "core:window:deny-is-maximized"
+          "enum": [
+            "core:window:deny-set-always-on-bottom"
+          ]
         },
         {
-          "description": "Denies the is_minimizable command without any pre-configured scope.",
+          "description": "core:window:deny-set-always-on-top -> Denies the set_always_on_top command without any pre-configured scope.",
           "type": "string",
-          "const": "core:window:deny-is-minimizable"
+          "enum": [
+            "core:window:deny-set-always-on-top"
+          ]
         },
         {
-          "description": "Denies the is_minimized command without any pre-configured scope.",
+          "description": "core:window:deny-set-closable -> Denies the set_closable command without any pre-configured scope.",
           "type": "string",
-          "const": "core:window:deny-is-minimized"
+          "enum": [
+            "core:window:deny-set-closable"
+          ]
         },
         {
-          "description": "Denies the is_resizable command without any pre-configured scope.",
+          "description": "core:window:deny-set-content-protected -> Denies the set_content_protected command without any pre-configured scope.",
           "type": "string",
-          "const": "core:window:deny-is-resizable"
+          "enum": [
+            "core:window:deny-set-content-protected"
+          ]
         },
         {
-          "description": "Denies the is_visible command without any pre-configured scope.",
+          "description": "core:window:deny-set-cursor-grab -> Denies the set_cursor_grab command without any pre-configured scope.",
           "type": "string",
-          "const": "core:window:deny-is-visible"
+          "enum": [
+            "core:window:deny-set-cursor-grab"
+          ]
         },
         {
-          "description": "Denies the maximize command without any pre-configured scope.",
+          "description": "core:window:deny-set-cursor-icon -> Denies the set_cursor_icon command without any pre-configured scope.",
           "type": "string",
-          "const": "core:window:deny-maximize"
+          "enum": [
+            "core:window:deny-set-cursor-icon"
+          ]
         },
         {
-          "description": "Denies the minimize command without any pre-configured scope.",
+          "description": "core:window:deny-set-cursor-position -> Denies the set_cursor_position command without any pre-configured scope.",
           "type": "string",
-          "const": "core:window:deny-minimize"
+          "enum": [
+            "core:window:deny-set-cursor-position"
+          ]
         },
         {
-          "description": "Denies the monitor_from_point command without any pre-configured scope.",
+          "description": "core:window:deny-set-cursor-visible -> Denies the set_cursor_visible command without any pre-configured scope.",
           "type": "string",
-          "const": "core:window:deny-monitor-from-point"
+          "enum": [
+            "core:window:deny-set-cursor-visible"
+          ]
         },
         {
-          "description": "Denies the outer_position command without any pre-configured scope.",
+          "description": "core:window:deny-set-decorations -> Denies the set_decorations command without any pre-configured scope.",
           "type": "string",
-          "const": "core:window:deny-outer-position"
+          "enum": [
+            "core:window:deny-set-decorations"
+          ]
         },
         {
-          "description": "Denies the outer_size command without any pre-configured scope.",
+          "description": "core:window:deny-set-effects -> Denies the set_effects command without any pre-configured scope.",
           "type": "string",
-          "const": "core:window:deny-outer-size"
+          "enum": [
+            "core:window:deny-set-effects"
+          ]
         },
         {
-          "description": "Denies the primary_monitor command without any pre-configured scope.",
+          "description": "core:window:deny-set-focus -> Denies the set_focus command without any pre-configured scope.",
           "type": "string",
-          "const": "core:window:deny-primary-monitor"
+          "enum": [
+            "core:window:deny-set-focus"
+          ]
         },
         {
-          "description": "Denies the request_user_attention command without any pre-configured scope.",
+          "description": "core:window:deny-set-fullscreen -> Denies the set_fullscreen command without any pre-configured scope.",
           "type": "string",
-          "const": "core:window:deny-request-user-attention"
+          "enum": [
+            "core:window:deny-set-fullscreen"
+          ]
         },
         {
-          "description": "Denies the scale_factor command without any pre-configured scope.",
+          "description": "core:window:deny-set-icon -> Denies the set_icon command without any pre-configured scope.",
           "type": "string",
-          "const": "core:window:deny-scale-factor"
+          "enum": [
+            "core:window:deny-set-icon"
+          ]
         },
         {
-          "description": "Denies the set_always_on_bottom command without any pre-configured scope.",
+          "description": "core:window:deny-set-ignore-cursor-events -> Denies the set_ignore_cursor_events command without any pre-configured scope.",
           "type": "string",
-          "const": "core:window:deny-set-always-on-bottom"
+          "enum": [
+            "core:window:deny-set-ignore-cursor-events"
+          ]
         },
         {
-          "description": "Denies the set_always_on_top command without any pre-configured scope.",
+          "description": "core:window:deny-set-max-size -> Denies the set_max_size command without any pre-configured scope.",
           "type": "string",
-          "const": "core:window:deny-set-always-on-top"
+          "enum": [
+            "core:window:deny-set-max-size"
+          ]
         },
         {
-          "description": "Denies the set_closable command without any pre-configured scope.",
+          "description": "core:window:deny-set-maximizable -> Denies the set_maximizable command without any pre-configured scope.",
           "type": "string",
-          "const": "core:window:deny-set-closable"
+          "enum": [
+            "core:window:deny-set-maximizable"
+          ]
         },
         {
-          "description": "Denies the set_content_protected command without any pre-configured scope.",
+          "description": "core:window:deny-set-min-size -> Denies the set_min_size command without any pre-configured scope.",
           "type": "string",
-          "const": "core:window:deny-set-content-protected"
+          "enum": [
+            "core:window:deny-set-min-size"
+          ]
         },
         {
-          "description": "Denies the set_cursor_grab command without any pre-configured scope.",
+          "description": "core:window:deny-set-minimizable -> Denies the set_minimizable command without any pre-configured scope.",
           "type": "string",
-          "const": "core:window:deny-set-cursor-grab"
+          "enum": [
+            "core:window:deny-set-minimizable"
+          ]
         },
         {
-          "description": "Denies the set_cursor_icon command without any pre-configured scope.",
+          "description": "core:window:deny-set-position -> Denies the set_position command without any pre-configured scope.",
           "type": "string",
-          "const": "core:window:deny-set-cursor-icon"
+          "enum": [
+            "core:window:deny-set-position"
+          ]
         },
         {
-          "description": "Denies the set_cursor_position command without any pre-configured scope.",
+          "description": "core:window:deny-set-progress-bar -> Denies the set_progress_bar command without any pre-configured scope.",
           "type": "string",
-          "const": "core:window:deny-set-cursor-position"
+          "enum": [
+            "core:window:deny-set-progress-bar"
+          ]
         },
         {
-          "description": "Denies the set_cursor_visible command without any pre-configured scope.",
+          "description": "core:window:deny-set-resizable -> Denies the set_resizable command without any pre-configured scope.",
           "type": "string",
-          "const": "core:window:deny-set-cursor-visible"
+          "enum": [
+            "core:window:deny-set-resizable"
+          ]
         },
         {
-          "description": "Denies the set_decorations command without any pre-configured scope.",
+          "description": "core:window:deny-set-shadow -> Denies the set_shadow command without any pre-configured scope.",
           "type": "string",
-          "const": "core:window:deny-set-decorations"
+          "enum": [
+            "core:window:deny-set-shadow"
+          ]
         },
         {
-          "description": "Denies the set_effects command without any pre-configured scope.",
+          "description": "core:window:deny-set-size -> Denies the set_size command without any pre-configured scope.",
           "type": "string",
-          "const": "core:window:deny-set-effects"
+          "enum": [
+            "core:window:deny-set-size"
+          ]
         },
         {
-          "description": "Denies the set_enabled command without any pre-configured scope.",
+          "description": "core:window:deny-set-size-constraints -> Denies the set_size_constraints command without any pre-configured scope.",
           "type": "string",
-          "const": "core:window:deny-set-enabled"
+          "enum": [
+            "core:window:deny-set-size-constraints"
+          ]
         },
         {
-          "description": "Denies the set_focus command without any pre-configured scope.",
+          "description": "core:window:deny-set-skip-taskbar -> Denies the set_skip_taskbar command without any pre-configured scope.",
           "type": "string",
-          "const": "core:window:deny-set-focus"
+          "enum": [
+            "core:window:deny-set-skip-taskbar"
+          ]
         },
         {
-          "description": "Denies the set_fullscreen command without any pre-configured scope.",
+          "description": "core:window:deny-set-title -> Denies the set_title command without any pre-configured scope.",
           "type": "string",
-          "const": "core:window:deny-set-fullscreen"
+          "enum": [
+            "core:window:deny-set-title"
+          ]
         },
         {
-          "description": "Denies the set_icon command without any pre-configured scope.",
+          "description": "core:window:deny-set-title-bar-style -> Denies the set_title_bar_style command without any pre-configured scope.",
           "type": "string",
-          "const": "core:window:deny-set-icon"
+          "enum": [
+            "core:window:deny-set-title-bar-style"
+          ]
         },
         {
-          "description": "Denies the set_ignore_cursor_events command without any pre-configured scope.",
+          "description": "core:window:deny-set-visible-on-all-workspaces -> Denies the set_visible_on_all_workspaces command without any pre-configured scope.",
           "type": "string",
-          "const": "core:window:deny-set-ignore-cursor-events"
+          "enum": [
+            "core:window:deny-set-visible-on-all-workspaces"
+          ]
         },
         {
-          "description": "Denies the set_max_size command without any pre-configured scope.",
+          "description": "core:window:deny-show -> Denies the show command without any pre-configured scope.",
           "type": "string",
-          "const": "core:window:deny-set-max-size"
+          "enum": [
+            "core:window:deny-show"
+          ]
         },
         {
-          "description": "Denies the set_maximizable command without any pre-configured scope.",
+          "description": "core:window:deny-start-dragging -> Denies the start_dragging command without any pre-configured scope.",
           "type": "string",
-          "const": "core:window:deny-set-maximizable"
+          "enum": [
+            "core:window:deny-start-dragging"
+          ]
         },
         {
-          "description": "Denies the set_min_size command without any pre-configured scope.",
+          "description": "core:window:deny-start-resize-dragging -> Denies the start_resize_dragging command without any pre-configured scope.",
           "type": "string",
-          "const": "core:window:deny-set-min-size"
+          "enum": [
+            "core:window:deny-start-resize-dragging"
+          ]
         },
         {
-          "description": "Denies the set_minimizable command without any pre-configured scope.",
+          "description": "core:window:deny-theme -> Denies the theme command without any pre-configured scope.",
           "type": "string",
-          "const": "core:window:deny-set-minimizable"
+          "enum": [
+            "core:window:deny-theme"
+          ]
         },
         {
-          "description": "Denies the set_position command without any pre-configured scope.",
+          "description": "core:window:deny-title -> Denies the title command without any pre-configured scope.",
           "type": "string",
-          "const": "core:window:deny-set-position"
+          "enum": [
+            "core:window:deny-title"
+          ]
         },
         {
-          "description": "Denies the set_progress_bar command without any pre-configured scope.",
+          "description": "core:window:deny-toggle-maximize -> Denies the toggle_maximize command without any pre-configured scope.",
           "type": "string",
-          "const": "core:window:deny-set-progress-bar"
+          "enum": [
+            "core:window:deny-toggle-maximize"
+          ]
         },
         {
-          "description": "Denies the set_resizable command without any pre-configured scope.",
+          "description": "core:window:deny-unmaximize -> Denies the unmaximize command without any pre-configured scope.",
           "type": "string",
-          "const": "core:window:deny-set-resizable"
+          "enum": [
+            "core:window:deny-unmaximize"
+          ]
         },
         {
-          "description": "Denies the set_shadow command without any pre-configured scope.",
+          "description": "core:window:deny-unminimize -> Denies the unminimize command without any pre-configured scope.",
           "type": "string",
-          "const": "core:window:deny-set-shadow"
+          "enum": [
+            "core:window:deny-unminimize"
+          ]
         },
         {
-          "description": "Denies the set_size command without any pre-configured scope.",
+          "description": "dialog:default -> This permission set configures the types of dialogs\navailable from the dialog plugin.\n\n#### Granted Permissions\n\nAll dialog types are enabled.\n\n\n",
           "type": "string",
-          "const": "core:window:deny-set-size"
+          "enum": [
+            "dialog:default"
+          ]
         },
         {
-          "description": "Denies the set_size_constraints command without any pre-configured scope.",
+          "description": "dialog:allow-ask -> Enables the ask command without any pre-configured scope.",
           "type": "string",
-          "const": "core:window:deny-set-size-constraints"
+          "enum": [
+            "dialog:allow-ask"
+          ]
         },
         {
-          "description": "Denies the set_skip_taskbar command without any pre-configured scope.",
+          "description": "dialog:allow-confirm -> Enables the confirm command without any pre-configured scope.",
           "type": "string",
-          "const": "core:window:deny-set-skip-taskbar"
+          "enum": [
+            "dialog:allow-confirm"
+          ]
         },
         {
-          "description": "Denies the set_theme command without any pre-configured scope.",
+          "description": "dialog:allow-message -> Enables the message command without any pre-configured scope.",
           "type": "string",
-          "const": "core:window:deny-set-theme"
+          "enum": [
+            "dialog:allow-message"
+          ]
         },
         {
-          "description": "Denies the set_title command without any pre-configured scope.",
+          "description": "dialog:allow-open -> Enables the open command without any pre-configured scope.",
           "type": "string",
-          "const": "core:window:deny-set-title"
+          "enum": [
+            "dialog:allow-open"
+          ]
         },
         {
-          "description": "Denies the set_title_bar_style command without any pre-configured scope.",
+          "description": "dialog:allow-save -> Enables the save command without any pre-configured scope.",
           "type": "string",
-          "const": "core:window:deny-set-title-bar-style"
+          "enum": [
+            "dialog:allow-save"
+          ]
         },
         {
-          "description": "Denies the set_visible_on_all_workspaces command without any pre-configured scope.",
+          "description": "dialog:deny-ask -> Denies the ask command without any pre-configured scope.",
           "type": "string",
-          "const": "core:window:deny-set-visible-on-all-workspaces"
+          "enum": [
+            "dialog:deny-ask"
+          ]
         },
         {
-          "description": "Denies the show command without any pre-configured scope.",
+          "description": "dialog:deny-confirm -> Denies the confirm command without any pre-configured scope.",
           "type": "string",
-          "const": "core:window:deny-show"
+          "enum": [
+            "dialog:deny-confirm"
+          ]
         },
         {
-          "description": "Denies the start_dragging command without any pre-configured scope.",
+          "description": "dialog:deny-message -> Denies the message command without any pre-configured scope.",
           "type": "string",
-          "const": "core:window:deny-start-dragging"
+          "enum": [
+            "dialog:deny-message"
+          ]
         },
         {
-          "description": "Denies the start_resize_dragging command without any pre-configured scope.",
+          "description": "dialog:deny-open -> Denies the open command without any pre-configured scope.",
           "type": "string",
-          "const": "core:window:deny-start-resize-dragging"
+          "enum": [
+            "dialog:deny-open"
+          ]
         },
         {
-          "description": "Denies the theme command without any pre-configured scope.",
+          "description": "dialog:deny-save -> Denies the save command without any pre-configured scope.",
           "type": "string",
-          "const": "core:window:deny-theme"
+          "enum": [
+            "dialog:deny-save"
+          ]
         },
         {
-          "description": "Denies the title command without any pre-configured scope.",
+          "description": "log:default -> Allows the log command",
           "type": "string",
-          "const": "core:window:deny-title"
+          "enum": [
+            "log:default"
+          ]
         },
         {
-          "description": "Denies the toggle_maximize command without any pre-configured scope.",
+          "description": "log:allow-log -> Enables the log command without any pre-configured scope.",
           "type": "string",
-          "const": "core:window:deny-toggle-maximize"
+          "enum": [
+            "log:allow-log"
+          ]
         },
         {
-          "description": "Denies the unmaximize command without any pre-configured scope.",
+          "description": "log:deny-log -> Denies the log command without any pre-configured scope.",
           "type": "string",
-          "const": "core:window:deny-unmaximize"
+          "enum": [
+            "log:deny-log"
+          ]
         },
         {
-          "description": "Denies the unminimize command without any pre-configured scope.",
+          "description": "notification:default -> This permission set configures which\nnotification features are by default exposed.\n\n#### Granted Permissions\n\nIt allows all notification related features.\n\n",
           "type": "string",
-          "const": "core:window:deny-unminimize"
+          "enum": [
+            "notification:default"
+          ]
         },
         {
-          "description": "This permission set configures the types of dialogs\navailable from the dialog plugin.\n\n#### Granted Permissions\n\nAll dialog types are enabled.\n\n\n",
+          "description": "notification:allow-batch -> Enables the batch command without any pre-configured scope.",
           "type": "string",
-          "const": "dialog:default"
+          "enum": [
+            "notification:allow-batch"
+          ]
         },
         {
-          "description": "Enables the ask command without any pre-configured scope.",
+          "description": "notification:allow-cancel -> Enables the cancel command without any pre-configured scope.",
           "type": "string",
-          "const": "dialog:allow-ask"
+          "enum": [
+            "notification:allow-cancel"
+          ]
         },
         {
-          "description": "Enables the confirm command without any pre-configured scope.",
+          "description": "notification:allow-check-permissions -> Enables the check_permissions command without any pre-configured scope.",
           "type": "string",
-          "const": "dialog:allow-confirm"
+          "enum": [
+            "notification:allow-check-permissions"
+          ]
         },
         {
-          "description": "Enables the message command without any pre-configured scope.",
+          "description": "notification:allow-create-channel -> Enables the create_channel command without any pre-configured scope.",
           "type": "string",
-          "const": "dialog:allow-message"
+          "enum": [
+            "notification:allow-create-channel"
+          ]
         },
         {
-          "description": "Enables the open command without any pre-configured scope.",
+          "description": "notification:allow-delete-channel -> Enables the delete_channel command without any pre-configured scope.",
           "type": "string",
-          "const": "dialog:allow-open"
+          "enum": [
+            "notification:allow-delete-channel"
+          ]
         },
         {
-          "description": "Enables the save command without any pre-configured scope.",
+          "description": "notification:allow-get-active -> Enables the get_active command without any pre-configured scope.",
           "type": "string",
-          "const": "dialog:allow-save"
+          "enum": [
+            "notification:allow-get-active"
+          ]
         },
         {
-          "description": "Denies the ask command without any pre-configured scope.",
+          "description": "notification:allow-get-pending -> Enables the get_pending command without any pre-configured scope.",
           "type": "string",
-          "const": "dialog:deny-ask"
+          "enum": [
+            "notification:allow-get-pending"
+          ]
         },
         {
-          "description": "Denies the confirm command without any pre-configured scope.",
+          "description": "notification:allow-is-permission-granted -> Enables the is_permission_granted command without any pre-configured scope.",
           "type": "string",
-          "const": "dialog:deny-confirm"
+          "enum": [
+            "notification:allow-is-permission-granted"
+          ]
         },
         {
-          "description": "Denies the message command without any pre-configured scope.",
+          "description": "notification:allow-list-channels -> Enables the list_channels command without any pre-configured scope.",
           "type": "string",
-          "const": "dialog:deny-message"
+          "enum": [
+            "notification:allow-list-channels"
+          ]
         },
         {
-          "description": "Denies the open command without any pre-configured scope.",
+          "description": "notification:allow-notify -> Enables the notify command without any pre-configured scope.",
           "type": "string",
-          "const": "dialog:deny-open"
+          "enum": [
+            "notification:allow-notify"
+          ]
         },
         {
-          "description": "Denies the save command without any pre-configured scope.",
+          "description": "notification:allow-permission-state -> Enables the permission_state command without any pre-configured scope.",
           "type": "string",
-          "const": "dialog:deny-save"
+          "enum": [
+            "notification:allow-permission-state"
+          ]
         },
         {
-          "description": "Allows the log command",
+          "description": "notification:allow-register-action-types -> Enables the register_action_types command without any pre-configured scope.",
           "type": "string",
-          "const": "log:default"
+          "enum": [
+            "notification:allow-register-action-types"
+          ]
         },
         {
-          "description": "Enables the log command without any pre-configured scope.",
+          "description": "notification:allow-register-listener -> Enables the register_listener command without any pre-configured scope.",
           "type": "string",
-          "const": "log:allow-log"
+          "enum": [
+            "notification:allow-register-listener"
+          ]
         },
         {
-          "description": "Denies the log command without any pre-configured scope.",
+          "description": "notification:allow-remove-active -> Enables the remove_active command without any pre-configured scope.",
           "type": "string",
-          "const": "log:deny-log"
+          "enum": [
+            "notification:allow-remove-active"
+          ]
         },
         {
-          "description": "This permission set configures which\nnotification features are by default exposed.\n\n#### Granted Permissions\n\nIt allows all notification related features.\n\n",
+          "description": "notification:allow-request-permission -> Enables the request_permission command without any pre-configured scope.",
           "type": "string",
-          "const": "notification:default"
+          "enum": [
+            "notification:allow-request-permission"
+          ]
         },
         {
-          "description": "Enables the batch command without any pre-configured scope.",
+          "description": "notification:allow-show -> Enables the show command without any pre-configured scope.",
           "type": "string",
-          "const": "notification:allow-batch"
+          "enum": [
+            "notification:allow-show"
+          ]
         },
         {
-          "description": "Enables the cancel command without any pre-configured scope.",
+          "description": "notification:deny-batch -> Denies the batch command without any pre-configured scope.",
           "type": "string",
-          "const": "notification:allow-cancel"
+          "enum": [
+            "notification:deny-batch"
+          ]
         },
         {
-          "description": "Enables the check_permissions command without any pre-configured scope.",
+          "description": "notification:deny-cancel -> Denies the cancel command without any pre-configured scope.",
           "type": "string",
-          "const": "notification:allow-check-permissions"
+          "enum": [
+            "notification:deny-cancel"
+          ]
         },
         {
-          "description": "Enables the create_channel command without any pre-configured scope.",
+          "description": "notification:deny-check-permissions -> Denies the check_permissions command without any pre-configured scope.",
           "type": "string",
-          "const": "notification:allow-create-channel"
+          "enum": [
+            "notification:deny-check-permissions"
+          ]
         },
         {
-          "description": "Enables the delete_channel command without any pre-configured scope.",
+          "description": "notification:deny-create-channel -> Denies the create_channel command without any pre-configured scope.",
           "type": "string",
-          "const": "notification:allow-delete-channel"
+          "enum": [
+            "notification:deny-create-channel"
+          ]
         },
         {
-          "description": "Enables the get_active command without any pre-configured scope.",
+          "description": "notification:deny-delete-channel -> Denies the delete_channel command without any pre-configured scope.",
           "type": "string",
-          "const": "notification:allow-get-active"
+          "enum": [
+            "notification:deny-delete-channel"
+          ]
         },
         {
-          "description": "Enables the get_pending command without any pre-configured scope.",
+          "description": "notification:deny-get-active -> Denies the get_active command without any pre-configured scope.",
           "type": "string",
-          "const": "notification:allow-get-pending"
+          "enum": [
+            "notification:deny-get-active"
+          ]
         },
         {
-          "description": "Enables the is_permission_granted command without any pre-configured scope.",
+          "description": "notification:deny-get-pending -> Denies the get_pending command without any pre-configured scope.",
           "type": "string",
-          "const": "notification:allow-is-permission-granted"
+          "enum": [
+            "notification:deny-get-pending"
+          ]
         },
         {
-          "description": "Enables the list_channels command without any pre-configured scope.",
+          "description": "notification:deny-is-permission-granted -> Denies the is_permission_granted command without any pre-configured scope.",
           "type": "string",
-          "const": "notification:allow-list-channels"
+          "enum": [
+            "notification:deny-is-permission-granted"
+          ]
         },
         {
-          "description": "Enables the notify command without any pre-configured scope.",
+          "description": "notification:deny-list-channels -> Denies the list_channels command without any pre-configured scope.",
           "type": "string",
-          "const": "notification:allow-notify"
+          "enum": [
+            "notification:deny-list-channels"
+          ]
         },
         {
-          "description": "Enables the permission_state command without any pre-configured scope.",
+          "description": "notification:deny-notify -> Denies the notify command without any pre-configured scope.",
           "type": "string",
-          "const": "notification:allow-permission-state"
+          "enum": [
+            "notification:deny-notify"
+          ]
         },
         {
-          "description": "Enables the register_action_types command without any pre-configured scope.",
+          "description": "notification:deny-permission-state -> Denies the permission_state command without any pre-configured scope.",
           "type": "string",
-          "const": "notification:allow-register-action-types"
+          "enum": [
+            "notification:deny-permission-state"
+          ]
         },
         {
-          "description": "Enables the register_listener command without any pre-configured scope.",
+          "description": "notification:deny-register-action-types -> Denies the register_action_types command without any pre-configured scope.",
           "type": "string",
-          "const": "notification:allow-register-listener"
+          "enum": [
+            "notification:deny-register-action-types"
+          ]
         },
         {
-          "description": "Enables the remove_active command without any pre-configured scope.",
+          "description": "notification:deny-register-listener -> Denies the register_listener command without any pre-configured scope.",
           "type": "string",
-          "const": "notification:allow-remove-active"
+          "enum": [
+            "notification:deny-register-listener"
+          ]
         },
         {
-          "description": "Enables the request_permission command without any pre-configured scope.",
+          "description": "notification:deny-remove-active -> Denies the remove_active command without any pre-configured scope.",
           "type": "string",
-          "const": "notification:allow-request-permission"
+          "enum": [
+            "notification:deny-remove-active"
+          ]
         },
         {
-          "description": "Enables the show command without any pre-configured scope.",
+          "description": "notification:deny-request-permission -> Denies the request_permission command without any pre-configured scope.",
           "type": "string",
-          "const": "notification:allow-show"
+          "enum": [
+            "notification:deny-request-permission"
+          ]
         },
         {
-          "description": "Denies the batch command without any pre-configured scope.",
+          "description": "notification:deny-show -> Denies the show command without any pre-configured scope.",
           "type": "string",
-          "const": "notification:deny-batch"
+          "enum": [
+            "notification:deny-show"
+          ]
         },
         {
-          "description": "Denies the cancel command without any pre-configured scope.",
+          "description": "os:default -> This permission set configures which\noperating system information are available\nto gather from the frontend.\n\n#### Granted Permissions\n\nAll information except the host name are available.\n\n",
           "type": "string",
-          "const": "notification:deny-cancel"
+          "enum": [
+            "os:default"
+          ]
         },
         {
-          "description": "Denies the check_permissions command without any pre-configured scope.",
+          "description": "os:allow-arch -> Enables the arch command without any pre-configured scope.",
           "type": "string",
-          "const": "notification:deny-check-permissions"
+          "enum": [
+            "os:allow-arch"
+          ]
         },
         {
-          "description": "Denies the create_channel command without any pre-configured scope.",
+          "description": "os:allow-exe-extension -> Enables the exe_extension command without any pre-configured scope.",
           "type": "string",
-          "const": "notification:deny-create-channel"
+          "enum": [
+            "os:allow-exe-extension"
+          ]
         },
         {
-          "description": "Denies the delete_channel command without any pre-configured scope.",
+          "description": "os:allow-family -> Enables the family command without any pre-configured scope.",
           "type": "string",
-          "const": "notification:deny-delete-channel"
+          "enum": [
+            "os:allow-family"
+          ]
         },
         {
-          "description": "Denies the get_active command without any pre-configured scope.",
+          "description": "os:allow-hostname -> Enables the hostname command without any pre-configured scope.",
           "type": "string",
-          "const": "notification:deny-get-active"
+          "enum": [
+            "os:allow-hostname"
+          ]
         },
         {
-          "description": "Denies the get_pending command without any pre-configured scope.",
+          "description": "os:allow-locale -> Enables the locale command without any pre-configured scope.",
           "type": "string",
-          "const": "notification:deny-get-pending"
+          "enum": [
+            "os:allow-locale"
+          ]
         },
         {
-          "description": "Denies the is_permission_granted command without any pre-configured scope.",
+          "description": "os:allow-os-type -> Enables the os_type command without any pre-configured scope.",
           "type": "string",
-          "const": "notification:deny-is-permission-granted"
+          "enum": [
+            "os:allow-os-type"
+          ]
         },
         {
-          "description": "Denies the list_channels command without any pre-configured scope.",
+          "description": "os:allow-platform -> Enables the platform command without any pre-configured scope.",
           "type": "string",
-          "const": "notification:deny-list-channels"
+          "enum": [
+            "os:allow-platform"
+          ]
         },
         {
-          "description": "Denies the notify command without any pre-configured scope.",
+          "description": "os:allow-version -> Enables the version command without any pre-configured scope.",
           "type": "string",
-          "const": "notification:deny-notify"
+          "enum": [
+            "os:allow-version"
+          ]
         },
         {
-          "description": "Denies the permission_state command without any pre-configured scope.",
+          "description": "os:deny-arch -> Denies the arch command without any pre-configured scope.",
           "type": "string",
-          "const": "notification:deny-permission-state"
+          "enum": [
+            "os:deny-arch"
+          ]
         },
         {
-          "description": "Denies the register_action_types command without any pre-configured scope.",
+          "description": "os:deny-exe-extension -> Denies the exe_extension command without any pre-configured scope.",
           "type": "string",
-          "const": "notification:deny-register-action-types"
+          "enum": [
+            "os:deny-exe-extension"
+          ]
         },
         {
-          "description": "Denies the register_listener command without any pre-configured scope.",
+          "description": "os:deny-family -> Denies the family command without any pre-configured scope.",
           "type": "string",
-          "const": "notification:deny-register-listener"
+          "enum": [
+            "os:deny-family"
+          ]
         },
         {
-          "description": "Denies the remove_active command without any pre-configured scope.",
+          "description": "os:deny-hostname -> Denies the hostname command without any pre-configured scope.",
           "type": "string",
-          "const": "notification:deny-remove-active"
+          "enum": [
+            "os:deny-hostname"
+          ]
         },
         {
-          "description": "Denies the request_permission command without any pre-configured scope.",
+          "description": "os:deny-locale -> Denies the locale command without any pre-configured scope.",
           "type": "string",
-          "const": "notification:deny-request-permission"
+          "enum": [
+            "os:deny-locale"
+          ]
         },
         {
-          "description": "Denies the show command without any pre-configured scope.",
+          "description": "os:deny-os-type -> Denies the os_type command without any pre-configured scope.",
           "type": "string",
-          "const": "notification:deny-show"
+          "enum": [
+            "os:deny-os-type"
+          ]
         },
         {
-          "description": "This permission set configures which\noperating system information are available\nto gather from the frontend.\n\n#### Granted Permissions\n\nAll information except the host name are available.\n\n",
+          "description": "os:deny-platform -> Denies the platform command without any pre-configured scope.",
           "type": "string",
-          "const": "os:default"
+          "enum": [
+            "os:deny-platform"
+          ]
         },
         {
-          "description": "Enables the arch command without any pre-configured scope.",
+          "description": "os:deny-version -> Denies the version command without any pre-configured scope.",
           "type": "string",
-          "const": "os:allow-arch"
+          "enum": [
+            "os:deny-version"
+          ]
         },
         {
-          "description": "Enables the exe_extension command without any pre-configured scope.",
+          "description": "shell:default -> This permission set configures which\nshell functionality is exposed by default.\n\n#### Granted Permissions\n\nIt allows to use the `open` functionality without any specific\nscope pre-configured. It will allow opening `http(s)://`,\n`tel:` and `mailto:` links.\n",
           "type": "string",
-          "const": "os:allow-exe-extension"
+          "enum": [
+            "shell:default"
+          ]
         },
         {
-          "description": "Enables the family command without any pre-configured scope.",
+          "description": "shell:allow-execute -> Enables the execute command without any pre-configured scope.",
           "type": "string",
-          "const": "os:allow-family"
+          "enum": [
+            "shell:allow-execute"
+          ]
         },
         {
-          "description": "Enables the hostname command without any pre-configured scope.",
+          "description": "shell:allow-kill -> Enables the kill command without any pre-configured scope.",
           "type": "string",
-          "const": "os:allow-hostname"
+          "enum": [
+            "shell:allow-kill"
+          ]
         },
         {
-          "description": "Enables the locale command without any pre-configured scope.",
+          "description": "shell:allow-open -> Enables the open command without any pre-configured scope.",
           "type": "string",
-          "const": "os:allow-locale"
+          "enum": [
+            "shell:allow-open"
+          ]
         },
         {
-          "description": "Enables the os_type command without any pre-configured scope.",
+          "description": "shell:allow-spawn -> Enables the spawn command without any pre-configured scope.",
           "type": "string",
-          "const": "os:allow-os-type"
+          "enum": [
+            "shell:allow-spawn"
+          ]
         },
         {
-          "description": "Enables the platform command without any pre-configured scope.",
+          "description": "shell:allow-stdin-write -> Enables the stdin_write command without any pre-configured scope.",
           "type": "string",
-          "const": "os:allow-platform"
+          "enum": [
+            "shell:allow-stdin-write"
+          ]
         },
         {
-          "description": "Enables the version command without any pre-configured scope.",
+          "description": "shell:deny-execute -> Denies the execute command without any pre-configured scope.",
           "type": "string",
-          "const": "os:allow-version"
+          "enum": [
+            "shell:deny-execute"
+          ]
         },
         {
-          "description": "Denies the arch command without any pre-configured scope.",
+          "description": "shell:deny-kill -> Denies the kill command without any pre-configured scope.",
           "type": "string",
-          "const": "os:deny-arch"
+          "enum": [
+            "shell:deny-kill"
+          ]
         },
         {
-          "description": "Denies the exe_extension command without any pre-configured scope.",
+          "description": "shell:deny-open -> Denies the open command without any pre-configured scope.",
           "type": "string",
-          "const": "os:deny-exe-extension"
+          "enum": [
+            "shell:deny-open"
+          ]
         },
         {
-          "description": "Denies the family command without any pre-configured scope.",
+          "description": "shell:deny-spawn -> Denies the spawn command without any pre-configured scope.",
           "type": "string",
-          "const": "os:deny-family"
+          "enum": [
+            "shell:deny-spawn"
+          ]
         },
         {
-          "description": "Denies the hostname command without any pre-configured scope.",
+          "description": "shell:deny-stdin-write -> Denies the stdin_write command without any pre-configured scope.",
           "type": "string",
-          "const": "os:deny-hostname"
+          "enum": [
+            "shell:deny-stdin-write"
+          ]
         },
         {
-          "description": "Denies the locale command without any pre-configured scope.",
+          "description": "window-state:default -> This permission set configures what kind of\noperations are available from the window state plugin.\n\n#### Granted Permissions\n\nAll operations are enabled by default.\n\n",
           "type": "string",
-          "const": "os:deny-locale"
+          "enum": [
+            "window-state:default"
+          ]
         },
         {
-          "description": "Denies the os_type command without any pre-configured scope.",
+          "description": "window-state:allow-filename -> Enables the filename command without any pre-configured scope.",
           "type": "string",
-          "const": "os:deny-os-type"
+          "enum": [
+            "window-state:allow-filename"
+          ]
         },
         {
-          "description": "Denies the platform command without any pre-configured scope.",
+          "description": "window-state:allow-restore-state -> Enables the restore_state command without any pre-configured scope.",
           "type": "string",
-          "const": "os:deny-platform"
+          "enum": [
+            "window-state:allow-restore-state"
+          ]
         },
         {
-          "description": "Denies the version command without any pre-configured scope.",
+          "description": "window-state:allow-save-window-state -> Enables the save_window_state command without any pre-configured scope.",
           "type": "string",
-          "const": "os:deny-version"
+          "enum": [
+            "window-state:allow-save-window-state"
+          ]
         },
         {
-          "description": "This permission set configures which\nshell functionality is exposed by default.\n\n#### Granted Permissions\n\nIt allows to use the `open` functionality without any specific\nscope pre-configured. It will allow opening `http(s)://`,\n`tel:` and `mailto:` links.\n",
+          "description": "window-state:deny-filename -> Denies the filename command without any pre-configured scope.",
           "type": "string",
-          "const": "shell:default"
+          "enum": [
+            "window-state:deny-filename"
+          ]
         },
         {
-          "description": "Enables the execute command without any pre-configured scope.",
+          "description": "window-state:deny-restore-state -> Denies the restore_state command without any pre-configured scope.",
           "type": "string",
-          "const": "shell:allow-execute"
+          "enum": [
+            "window-state:deny-restore-state"
+          ]
         },
         {
-          "description": "Enables the kill command without any pre-configured scope.",
+          "description": "window-state:deny-save-window-state -> Denies the save_window_state command without any pre-configured scope.",
           "type": "string",
-          "const": "shell:allow-kill"
-        },
-        {
-          "description": "Enables the open command without any pre-configured scope.",
-          "type": "string",
-          "const": "shell:allow-open"
-        },
-        {
-          "description": "Enables the spawn command without any pre-configured scope.",
-          "type": "string",
-          "const": "shell:allow-spawn"
-        },
-        {
-          "description": "Enables the stdin_write command without any pre-configured scope.",
-          "type": "string",
-          "const": "shell:allow-stdin-write"
-        },
-        {
-          "description": "Denies the execute command without any pre-configured scope.",
-          "type": "string",
-          "const": "shell:deny-execute"
-        },
-        {
-          "description": "Denies the kill command without any pre-configured scope.",
-          "type": "string",
-          "const": "shell:deny-kill"
-        },
-        {
-          "description": "Denies the open command without any pre-configured scope.",
-          "type": "string",
-          "const": "shell:deny-open"
-        },
-        {
-          "description": "Denies the spawn command without any pre-configured scope.",
-          "type": "string",
-          "const": "shell:deny-spawn"
-        },
-        {
-          "description": "Denies the stdin_write command without any pre-configured scope.",
-          "type": "string",
-          "const": "shell:deny-stdin-write"
-        },
-        {
-          "description": "This permission set configures what kind of\noperations are available from the window state plugin.\n\n#### Granted Permissions\n\nAll operations are enabled by default.\n\n",
-          "type": "string",
-          "const": "window-state:default"
-        },
-        {
-          "description": "Enables the filename command without any pre-configured scope.",
-          "type": "string",
-          "const": "window-state:allow-filename"
-        },
-        {
-          "description": "Enables the restore_state command without any pre-configured scope.",
-          "type": "string",
-          "const": "window-state:allow-restore-state"
-        },
-        {
-          "description": "Enables the save_window_state command without any pre-configured scope.",
-          "type": "string",
-          "const": "window-state:allow-save-window-state"
-        },
-        {
-          "description": "Denies the filename command without any pre-configured scope.",
-          "type": "string",
-          "const": "window-state:deny-filename"
-        },
-        {
-          "description": "Denies the restore_state command without any pre-configured scope.",
-          "type": "string",
-          "const": "window-state:deny-restore-state"
-        },
-        {
-          "description": "Denies the save_window_state command without any pre-configured scope.",
-          "type": "string",
-          "const": "window-state:deny-save-window-state"
+          "enum": [
+            "window-state:deny-save-window-state"
+          ]
         }
       ]
     },
@@ -2426,7 +3027,7 @@
         }
       ]
     },
-    "ShellScopeEntryAllowedArg": {
+    "ShellAllowedArg": {
       "description": "A command argument allowed to be executed by the webview API.",
       "anyOf": [
         {
@@ -2454,18 +3055,18 @@
         }
       ]
     },
-    "ShellScopeEntryAllowedArgs": {
-      "description": "A set of command arguments allowed to be executed by the webview API.\n\nA value of `true` will allow any arguments to be passed to the command. `false` will disable all arguments. A list of [`ShellScopeEntryAllowedArg`] will set those arguments as the only valid arguments to be passed to the attached command configuration.",
+    "ShellAllowedArgs": {
+      "description": "A set of command arguments allowed to be executed by the webview API.\n\nA value of `true` will allow any arguments to be passed to the command. `false` will disable all arguments. A list of [`ShellAllowedArg`] will set those arguments as the only valid arguments to be passed to the attached command configuration.",
       "anyOf": [
         {
           "description": "Use a simple boolean to allow all or disable all arguments to this command configuration.",
           "type": "boolean"
         },
         {
-          "description": "A specific set of [`ShellScopeEntryAllowedArg`] that are valid to call for the command configuration.",
+          "description": "A specific set of [`ShellAllowedArg`] that are valid to call for the command configuration.",
           "type": "array",
           "items": {
-            "$ref": "#/definitions/ShellScopeEntryAllowedArg"
+            "$ref": "#/definitions/ShellAllowedArg"
           }
         }
       ]
diff --git a/desktop/tauri/src-tauri/gen/schemas/linux-schema.json b/desktop/tauri/src-tauri/gen/schemas/linux-schema.json
index 10fb08fb..797ccb5c 100644
--- a/desktop/tauri/src-tauri/gen/schemas/linux-schema.json
+++ b/desktop/tauri/src-tauri/gen/schemas/linux-schema.json
@@ -133,2202 +133,2803 @@
         {
           "description": "Reference a permission or permission set by identifier and extends its scope.",
           "type": "object",
-          "allOf": [
+          "oneOf": [
             {
-              "if": {
-                "properties": {
-                  "identifier": {
-                    "anyOf": [
-                      {
-                        "description": "This permission set configures which\nshell functionality is exposed by default.\n\n#### Granted Permissions\n\nIt allows to use the `open` functionality without any specific\nscope pre-configured. It will allow opening `http(s)://`,\n`tel:` and `mailto:` links.\n",
-                        "type": "string",
-                        "const": "shell:default"
-                      },
-                      {
-                        "description": "Enables the execute command without any pre-configured scope.",
-                        "type": "string",
-                        "const": "shell:allow-execute"
-                      },
-                      {
-                        "description": "Enables the kill command without any pre-configured scope.",
-                        "type": "string",
-                        "const": "shell:allow-kill"
-                      },
-                      {
-                        "description": "Enables the open command without any pre-configured scope.",
-                        "type": "string",
-                        "const": "shell:allow-open"
-                      },
-                      {
-                        "description": "Enables the spawn command without any pre-configured scope.",
-                        "type": "string",
-                        "const": "shell:allow-spawn"
-                      },
-                      {
-                        "description": "Enables the stdin_write command without any pre-configured scope.",
-                        "type": "string",
-                        "const": "shell:allow-stdin-write"
-                      },
-                      {
-                        "description": "Denies the execute command without any pre-configured scope.",
-                        "type": "string",
-                        "const": "shell:deny-execute"
-                      },
-                      {
-                        "description": "Denies the kill command without any pre-configured scope.",
-                        "type": "string",
-                        "const": "shell:deny-kill"
-                      },
-                      {
-                        "description": "Denies the open command without any pre-configured scope.",
-                        "type": "string",
-                        "const": "shell:deny-open"
-                      },
-                      {
-                        "description": "Denies the spawn command without any pre-configured scope.",
-                        "type": "string",
-                        "const": "shell:deny-spawn"
-                      },
-                      {
-                        "description": "Denies the stdin_write command without any pre-configured scope.",
-                        "type": "string",
-                        "const": "shell:deny-stdin-write"
-                      }
-                    ]
-                  }
-                }
-              },
-              "then": {
-                "properties": {
-                  "allow": {
-                    "items": {
-                      "title": "ShellScopeEntry",
-                      "description": "Shell scope entry.",
-                      "anyOf": [
-                        {
-                          "type": "object",
-                          "required": [
-                            "cmd",
-                            "name"
-                          ],
-                          "properties": {
-                            "args": {
-                              "description": "The allowed arguments for the command execution.",
-                              "allOf": [
-                                {
-                                  "$ref": "#/definitions/ShellScopeEntryAllowedArgs"
-                                }
-                              ]
-                            },
-                            "cmd": {
-                              "description": "The command name. It can start with a variable that resolves to a system base directory. The variables are: `$AUDIO`, `$CACHE`, `$CONFIG`, `$DATA`, `$LOCALDATA`, `$DESKTOP`, `$DOCUMENT`, `$DOWNLOAD`, `$EXE`, `$FONT`, `$HOME`, `$PICTURE`, `$PUBLIC`, `$RUNTIME`, `$TEMPLATE`, `$VIDEO`, `$RESOURCE`, `$LOG`, `$TEMP`, `$APPCONFIG`, `$APPDATA`, `$APPLOCALDATA`, `$APPCACHE`, `$APPLOG`.",
-                              "type": "string"
-                            },
-                            "name": {
-                              "description": "The name for this allowed shell command configuration.\n\nThis name will be used inside of the webview API to call this command along with any specified arguments.",
-                              "type": "string"
-                            }
-                          },
-                          "additionalProperties": false
-                        },
-                        {
-                          "type": "object",
-                          "required": [
-                            "name",
-                            "sidecar"
-                          ],
-                          "properties": {
-                            "args": {
-                              "description": "The allowed arguments for the command execution.",
-                              "allOf": [
-                                {
-                                  "$ref": "#/definitions/ShellScopeEntryAllowedArgs"
-                                }
-                              ]
-                            },
-                            "name": {
-                              "description": "The name for this allowed shell command configuration.\n\nThis name will be used inside of the webview API to call this command along with any specified arguments.",
-                              "type": "string"
-                            },
-                            "sidecar": {
-                              "description": "If this command is a sidecar command.",
-                              "type": "boolean"
-                            }
-                          },
-                          "additionalProperties": false
-                        }
-                      ]
-                    }
-                  },
-                  "deny": {
-                    "items": {
-                      "title": "ShellScopeEntry",
-                      "description": "Shell scope entry.",
-                      "anyOf": [
-                        {
-                          "type": "object",
-                          "required": [
-                            "cmd",
-                            "name"
-                          ],
-                          "properties": {
-                            "args": {
-                              "description": "The allowed arguments for the command execution.",
-                              "allOf": [
-                                {
-                                  "$ref": "#/definitions/ShellScopeEntryAllowedArgs"
-                                }
-                              ]
-                            },
-                            "cmd": {
-                              "description": "The command name. It can start with a variable that resolves to a system base directory. The variables are: `$AUDIO`, `$CACHE`, `$CONFIG`, `$DATA`, `$LOCALDATA`, `$DESKTOP`, `$DOCUMENT`, `$DOWNLOAD`, `$EXE`, `$FONT`, `$HOME`, `$PICTURE`, `$PUBLIC`, `$RUNTIME`, `$TEMPLATE`, `$VIDEO`, `$RESOURCE`, `$LOG`, `$TEMP`, `$APPCONFIG`, `$APPDATA`, `$APPLOCALDATA`, `$APPCACHE`, `$APPLOG`.",
-                              "type": "string"
-                            },
-                            "name": {
-                              "description": "The name for this allowed shell command configuration.\n\nThis name will be used inside of the webview API to call this command along with any specified arguments.",
-                              "type": "string"
-                            }
-                          },
-                          "additionalProperties": false
-                        },
-                        {
-                          "type": "object",
-                          "required": [
-                            "name",
-                            "sidecar"
-                          ],
-                          "properties": {
-                            "args": {
-                              "description": "The allowed arguments for the command execution.",
-                              "allOf": [
-                                {
-                                  "$ref": "#/definitions/ShellScopeEntryAllowedArgs"
-                                }
-                              ]
-                            },
-                            "name": {
-                              "description": "The name for this allowed shell command configuration.\n\nThis name will be used inside of the webview API to call this command along with any specified arguments.",
-                              "type": "string"
-                            },
-                            "sidecar": {
-                              "description": "If this command is a sidecar command.",
-                              "type": "boolean"
-                            }
-                          },
-                          "additionalProperties": false
-                        }
-                      ]
-                    }
-                  }
-                }
-              },
+              "type": "object",
+              "required": [
+                "identifier"
+              ],
               "properties": {
                 "identifier": {
-                  "description": "Identifier of the permission or permission set.",
-                  "allOf": [
+                  "oneOf": [
                     {
-                      "$ref": "#/definitions/Identifier"
-                    }
-                  ]
-                }
-              }
-            },
-            {
-              "properties": {
-                "identifier": {
-                  "description": "Identifier of the permission or permission set.",
-                  "allOf": [
+                      "description": "shell:default -> This permission set configures which\nshell functionality is exposed by default.\n\n#### Granted Permissions\n\nIt allows to use the `open` functionality without any specific\nscope pre-configured. It will allow opening `http(s)://`,\n`tel:` and `mailto:` links.\n",
+                      "type": "string",
+                      "enum": [
+                        "shell:default"
+                      ]
+                    },
                     {
-                      "$ref": "#/definitions/Identifier"
+                      "description": "shell:allow-execute -> Enables the execute command without any pre-configured scope.",
+                      "type": "string",
+                      "enum": [
+                        "shell:allow-execute"
+                      ]
+                    },
+                    {
+                      "description": "shell:allow-kill -> Enables the kill command without any pre-configured scope.",
+                      "type": "string",
+                      "enum": [
+                        "shell:allow-kill"
+                      ]
+                    },
+                    {
+                      "description": "shell:allow-open -> Enables the open command without any pre-configured scope.",
+                      "type": "string",
+                      "enum": [
+                        "shell:allow-open"
+                      ]
+                    },
+                    {
+                      "description": "shell:allow-spawn -> Enables the spawn command without any pre-configured scope.",
+                      "type": "string",
+                      "enum": [
+                        "shell:allow-spawn"
+                      ]
+                    },
+                    {
+                      "description": "shell:allow-stdin-write -> Enables the stdin_write command without any pre-configured scope.",
+                      "type": "string",
+                      "enum": [
+                        "shell:allow-stdin-write"
+                      ]
+                    },
+                    {
+                      "description": "shell:deny-execute -> Denies the execute command without any pre-configured scope.",
+                      "type": "string",
+                      "enum": [
+                        "shell:deny-execute"
+                      ]
+                    },
+                    {
+                      "description": "shell:deny-kill -> Denies the kill command without any pre-configured scope.",
+                      "type": "string",
+                      "enum": [
+                        "shell:deny-kill"
+                      ]
+                    },
+                    {
+                      "description": "shell:deny-open -> Denies the open command without any pre-configured scope.",
+                      "type": "string",
+                      "enum": [
+                        "shell:deny-open"
+                      ]
+                    },
+                    {
+                      "description": "shell:deny-spawn -> Denies the spawn command without any pre-configured scope.",
+                      "type": "string",
+                      "enum": [
+                        "shell:deny-spawn"
+                      ]
+                    },
+                    {
+                      "description": "shell:deny-stdin-write -> Denies the stdin_write command without any pre-configured scope.",
+                      "type": "string",
+                      "enum": [
+                        "shell:deny-stdin-write"
+                      ]
                     }
                   ]
                 },
                 "allow": {
-                  "description": "Data that defines what is allowed by the scope.",
-                  "type": [
-                    "array",
-                    "null"
-                  ],
                   "items": {
-                    "$ref": "#/definitions/Value"
+                    "title": "Entry",
+                    "description": "A command allowed to be executed by the webview API.",
+                    "type": "object",
+                    "required": [
+                      "args",
+                      "cmd",
+                      "name",
+                      "sidecar"
+                    ],
+                    "properties": {
+                      "args": {
+                        "description": "The allowed arguments for the command execution.",
+                        "allOf": [
+                          {
+                            "$ref": "#/definitions/ShellAllowedArgs"
+                          }
+                        ]
+                      },
+                      "cmd": {
+                        "description": "The command name. It can start with a variable that resolves to a system base directory. The variables are: `$AUDIO`, `$CACHE`, `$CONFIG`, `$DATA`, `$LOCALDATA`, `$DESKTOP`, `$DOCUMENT`, `$DOWNLOAD`, `$EXE`, `$FONT`, `$HOME`, `$PICTURE`, `$PUBLIC`, `$RUNTIME`, `$TEMPLATE`, `$VIDEO`, `$RESOURCE`, `$APP`, `$LOG`, `$TEMP`, `$APPCONFIG`, `$APPDATA`, `$APPLOCALDATA`, `$APPCACHE`, `$APPLOG`.",
+                        "type": "string"
+                      },
+                      "name": {
+                        "description": "The name for this allowed shell command configuration.\n\nThis name will be used inside of the webview API to call this command along with any specified arguments.",
+                        "type": "string"
+                      },
+                      "sidecar": {
+                        "description": "If this command is a sidecar command.",
+                        "type": "boolean"
+                      }
+                    }
                   }
                 },
                 "deny": {
-                  "description": "Data that defines what is denied by the scope. This should be prioritized by validation logic.",
-                  "type": [
-                    "array",
-                    "null"
-                  ],
                   "items": {
-                    "$ref": "#/definitions/Value"
+                    "title": "Entry",
+                    "description": "A command allowed to be executed by the webview API.",
+                    "type": "object",
+                    "required": [
+                      "args",
+                      "cmd",
+                      "name",
+                      "sidecar"
+                    ],
+                    "properties": {
+                      "args": {
+                        "description": "The allowed arguments for the command execution.",
+                        "allOf": [
+                          {
+                            "$ref": "#/definitions/ShellAllowedArgs"
+                          }
+                        ]
+                      },
+                      "cmd": {
+                        "description": "The command name. It can start with a variable that resolves to a system base directory. The variables are: `$AUDIO`, `$CACHE`, `$CONFIG`, `$DATA`, `$LOCALDATA`, `$DESKTOP`, `$DOCUMENT`, `$DOWNLOAD`, `$EXE`, `$FONT`, `$HOME`, `$PICTURE`, `$PUBLIC`, `$RUNTIME`, `$TEMPLATE`, `$VIDEO`, `$RESOURCE`, `$APP`, `$LOG`, `$TEMP`, `$APPCONFIG`, `$APPDATA`, `$APPLOCALDATA`, `$APPCACHE`, `$APPLOG`.",
+                        "type": "string"
+                      },
+                      "name": {
+                        "description": "The name for this allowed shell command configuration.\n\nThis name will be used inside of the webview API to call this command along with any specified arguments.",
+                        "type": "string"
+                      },
+                      "sidecar": {
+                        "description": "If this command is a sidecar command.",
+                        "type": "boolean"
+                      }
+                    }
                   }
                 }
               }
             }
-          ],
-          "required": [
-            "identifier"
           ]
         }
       ]
     },
     "Identifier": {
-      "description": "Permission identifier",
       "oneOf": [
         {
-          "description": "No features are enabled by default, as we believe\nthe clipboard can be inherently dangerous and it is \napplication specific if read and/or write access is needed.\n\nClipboard interaction needs to be explicitly enabled.\n",
+          "description": "clipboard-manager:default -> No features are enabled by default, as we believe\nthe clipboard can be inherently dangerous and it is \napplication specific if read and/or write access is needed.\n\nClipboard interaction needs to be explicitly enabled.\n",
           "type": "string",
-          "const": "clipboard-manager:default"
+          "enum": [
+            "clipboard-manager:default"
+          ]
         },
         {
-          "description": "Enables the clear command without any pre-configured scope.",
+          "description": "clipboard-manager:allow-clear -> Enables the clear command without any pre-configured scope.",
           "type": "string",
-          "const": "clipboard-manager:allow-clear"
+          "enum": [
+            "clipboard-manager:allow-clear"
+          ]
         },
         {
-          "description": "Enables the read_image command without any pre-configured scope.",
+          "description": "clipboard-manager:allow-read-image -> Enables the read_image command without any pre-configured scope.",
           "type": "string",
-          "const": "clipboard-manager:allow-read-image"
+          "enum": [
+            "clipboard-manager:allow-read-image"
+          ]
         },
         {
-          "description": "Enables the read_text command without any pre-configured scope.",
+          "description": "clipboard-manager:allow-read-text -> Enables the read_text command without any pre-configured scope.",
           "type": "string",
-          "const": "clipboard-manager:allow-read-text"
+          "enum": [
+            "clipboard-manager:allow-read-text"
+          ]
         },
         {
-          "description": "Enables the write_html command without any pre-configured scope.",
+          "description": "clipboard-manager:allow-write-html -> Enables the write_html command without any pre-configured scope.",
           "type": "string",
-          "const": "clipboard-manager:allow-write-html"
+          "enum": [
+            "clipboard-manager:allow-write-html"
+          ]
         },
         {
-          "description": "Enables the write_image command without any pre-configured scope.",
+          "description": "clipboard-manager:allow-write-image -> Enables the write_image command without any pre-configured scope.",
           "type": "string",
-          "const": "clipboard-manager:allow-write-image"
+          "enum": [
+            "clipboard-manager:allow-write-image"
+          ]
         },
         {
-          "description": "Enables the write_text command without any pre-configured scope.",
+          "description": "clipboard-manager:allow-write-text -> Enables the write_text command without any pre-configured scope.",
           "type": "string",
-          "const": "clipboard-manager:allow-write-text"
+          "enum": [
+            "clipboard-manager:allow-write-text"
+          ]
         },
         {
-          "description": "Denies the clear command without any pre-configured scope.",
+          "description": "clipboard-manager:deny-clear -> Denies the clear command without any pre-configured scope.",
           "type": "string",
-          "const": "clipboard-manager:deny-clear"
+          "enum": [
+            "clipboard-manager:deny-clear"
+          ]
         },
         {
-          "description": "Denies the read_image command without any pre-configured scope.",
+          "description": "clipboard-manager:deny-read-image -> Denies the read_image command without any pre-configured scope.",
           "type": "string",
-          "const": "clipboard-manager:deny-read-image"
+          "enum": [
+            "clipboard-manager:deny-read-image"
+          ]
         },
         {
-          "description": "Denies the read_text command without any pre-configured scope.",
+          "description": "clipboard-manager:deny-read-text -> Denies the read_text command without any pre-configured scope.",
           "type": "string",
-          "const": "clipboard-manager:deny-read-text"
+          "enum": [
+            "clipboard-manager:deny-read-text"
+          ]
         },
         {
-          "description": "Denies the write_html command without any pre-configured scope.",
+          "description": "clipboard-manager:deny-write-html -> Denies the write_html command without any pre-configured scope.",
           "type": "string",
-          "const": "clipboard-manager:deny-write-html"
+          "enum": [
+            "clipboard-manager:deny-write-html"
+          ]
         },
         {
-          "description": "Denies the write_image command without any pre-configured scope.",
+          "description": "clipboard-manager:deny-write-image -> Denies the write_image command without any pre-configured scope.",
           "type": "string",
-          "const": "clipboard-manager:deny-write-image"
+          "enum": [
+            "clipboard-manager:deny-write-image"
+          ]
         },
         {
-          "description": "Denies the write_text command without any pre-configured scope.",
+          "description": "clipboard-manager:deny-write-text -> Denies the write_text command without any pre-configured scope.",
           "type": "string",
-          "const": "clipboard-manager:deny-write-text"
+          "enum": [
+            "clipboard-manager:deny-write-text"
+          ]
         },
         {
-          "description": "Default core plugins set which includes:\n- 'core:path:default'\n- 'core:event:default'\n- 'core:window:default'\n- 'core:webview:default'\n- 'core:app:default'\n- 'core:image:default'\n- 'core:resources:default'\n- 'core:menu:default'\n- 'core:tray:default'\n",
+          "description": "core:app:default -> Default permissions for the plugin.",
           "type": "string",
-          "const": "core:default"
+          "enum": [
+            "core:app:default"
+          ]
         },
         {
-          "description": "Default permissions for the plugin.",
+          "description": "core:app:allow-app-hide -> Enables the app_hide command without any pre-configured scope.",
           "type": "string",
-          "const": "core:app:default"
+          "enum": [
+            "core:app:allow-app-hide"
+          ]
         },
         {
-          "description": "Enables the app_hide command without any pre-configured scope.",
+          "description": "core:app:allow-app-show -> Enables the app_show command without any pre-configured scope.",
           "type": "string",
-          "const": "core:app:allow-app-hide"
+          "enum": [
+            "core:app:allow-app-show"
+          ]
         },
         {
-          "description": "Enables the app_show command without any pre-configured scope.",
+          "description": "core:app:allow-default-window-icon -> Enables the default_window_icon command without any pre-configured scope.",
           "type": "string",
-          "const": "core:app:allow-app-show"
+          "enum": [
+            "core:app:allow-default-window-icon"
+          ]
         },
         {
-          "description": "Enables the default_window_icon command without any pre-configured scope.",
+          "description": "core:app:allow-name -> Enables the name command without any pre-configured scope.",
           "type": "string",
-          "const": "core:app:allow-default-window-icon"
+          "enum": [
+            "core:app:allow-name"
+          ]
         },
         {
-          "description": "Enables the name command without any pre-configured scope.",
+          "description": "core:app:allow-tauri-version -> Enables the tauri_version command without any pre-configured scope.",
           "type": "string",
-          "const": "core:app:allow-name"
+          "enum": [
+            "core:app:allow-tauri-version"
+          ]
         },
         {
-          "description": "Enables the set_app_theme command without any pre-configured scope.",
+          "description": "core:app:allow-version -> Enables the version command without any pre-configured scope.",
           "type": "string",
-          "const": "core:app:allow-set-app-theme"
+          "enum": [
+            "core:app:allow-version"
+          ]
         },
         {
-          "description": "Enables the tauri_version command without any pre-configured scope.",
+          "description": "core:app:deny-app-hide -> Denies the app_hide command without any pre-configured scope.",
           "type": "string",
-          "const": "core:app:allow-tauri-version"
+          "enum": [
+            "core:app:deny-app-hide"
+          ]
         },
         {
-          "description": "Enables the version command without any pre-configured scope.",
+          "description": "core:app:deny-app-show -> Denies the app_show command without any pre-configured scope.",
           "type": "string",
-          "const": "core:app:allow-version"
+          "enum": [
+            "core:app:deny-app-show"
+          ]
         },
         {
-          "description": "Denies the app_hide command without any pre-configured scope.",
+          "description": "core:app:deny-default-window-icon -> Denies the default_window_icon command without any pre-configured scope.",
           "type": "string",
-          "const": "core:app:deny-app-hide"
+          "enum": [
+            "core:app:deny-default-window-icon"
+          ]
         },
         {
-          "description": "Denies the app_show command without any pre-configured scope.",
+          "description": "core:app:deny-name -> Denies the name command without any pre-configured scope.",
           "type": "string",
-          "const": "core:app:deny-app-show"
+          "enum": [
+            "core:app:deny-name"
+          ]
         },
         {
-          "description": "Denies the default_window_icon command without any pre-configured scope.",
+          "description": "core:app:deny-tauri-version -> Denies the tauri_version command without any pre-configured scope.",
           "type": "string",
-          "const": "core:app:deny-default-window-icon"
+          "enum": [
+            "core:app:deny-tauri-version"
+          ]
         },
         {
-          "description": "Denies the name command without any pre-configured scope.",
+          "description": "core:app:deny-version -> Denies the version command without any pre-configured scope.",
           "type": "string",
-          "const": "core:app:deny-name"
+          "enum": [
+            "core:app:deny-version"
+          ]
         },
         {
-          "description": "Denies the set_app_theme command without any pre-configured scope.",
+          "description": "core:event:default -> Default permissions for the plugin.",
           "type": "string",
-          "const": "core:app:deny-set-app-theme"
+          "enum": [
+            "core:event:default"
+          ]
         },
         {
-          "description": "Denies the tauri_version command without any pre-configured scope.",
+          "description": "core:event:allow-emit -> Enables the emit command without any pre-configured scope.",
           "type": "string",
-          "const": "core:app:deny-tauri-version"
+          "enum": [
+            "core:event:allow-emit"
+          ]
         },
         {
-          "description": "Denies the version command without any pre-configured scope.",
+          "description": "core:event:allow-emit-to -> Enables the emit_to command without any pre-configured scope.",
           "type": "string",
-          "const": "core:app:deny-version"
+          "enum": [
+            "core:event:allow-emit-to"
+          ]
         },
         {
-          "description": "Default permissions for the plugin.",
+          "description": "core:event:allow-listen -> Enables the listen command without any pre-configured scope.",
           "type": "string",
-          "const": "core:event:default"
+          "enum": [
+            "core:event:allow-listen"
+          ]
         },
         {
-          "description": "Enables the emit command without any pre-configured scope.",
+          "description": "core:event:allow-unlisten -> Enables the unlisten command without any pre-configured scope.",
           "type": "string",
-          "const": "core:event:allow-emit"
+          "enum": [
+            "core:event:allow-unlisten"
+          ]
         },
         {
-          "description": "Enables the emit_to command without any pre-configured scope.",
+          "description": "core:event:deny-emit -> Denies the emit command without any pre-configured scope.",
           "type": "string",
-          "const": "core:event:allow-emit-to"
+          "enum": [
+            "core:event:deny-emit"
+          ]
         },
         {
-          "description": "Enables the listen command without any pre-configured scope.",
+          "description": "core:event:deny-emit-to -> Denies the emit_to command without any pre-configured scope.",
           "type": "string",
-          "const": "core:event:allow-listen"
+          "enum": [
+            "core:event:deny-emit-to"
+          ]
         },
         {
-          "description": "Enables the unlisten command without any pre-configured scope.",
+          "description": "core:event:deny-listen -> Denies the listen command without any pre-configured scope.",
           "type": "string",
-          "const": "core:event:allow-unlisten"
+          "enum": [
+            "core:event:deny-listen"
+          ]
         },
         {
-          "description": "Denies the emit command without any pre-configured scope.",
+          "description": "core:event:deny-unlisten -> Denies the unlisten command without any pre-configured scope.",
           "type": "string",
-          "const": "core:event:deny-emit"
+          "enum": [
+            "core:event:deny-unlisten"
+          ]
         },
         {
-          "description": "Denies the emit_to command without any pre-configured scope.",
+          "description": "core:image:default -> Default permissions for the plugin.",
           "type": "string",
-          "const": "core:event:deny-emit-to"
+          "enum": [
+            "core:image:default"
+          ]
         },
         {
-          "description": "Denies the listen command without any pre-configured scope.",
+          "description": "core:image:allow-from-bytes -> Enables the from_bytes command without any pre-configured scope.",
           "type": "string",
-          "const": "core:event:deny-listen"
+          "enum": [
+            "core:image:allow-from-bytes"
+          ]
         },
         {
-          "description": "Denies the unlisten command without any pre-configured scope.",
+          "description": "core:image:allow-from-path -> Enables the from_path command without any pre-configured scope.",
           "type": "string",
-          "const": "core:event:deny-unlisten"
+          "enum": [
+            "core:image:allow-from-path"
+          ]
         },
         {
-          "description": "Default permissions for the plugin.",
+          "description": "core:image:allow-new -> Enables the new command without any pre-configured scope.",
           "type": "string",
-          "const": "core:image:default"
+          "enum": [
+            "core:image:allow-new"
+          ]
         },
         {
-          "description": "Enables the from_bytes command without any pre-configured scope.",
+          "description": "core:image:allow-rgba -> Enables the rgba command without any pre-configured scope.",
           "type": "string",
-          "const": "core:image:allow-from-bytes"
+          "enum": [
+            "core:image:allow-rgba"
+          ]
         },
         {
-          "description": "Enables the from_path command without any pre-configured scope.",
+          "description": "core:image:allow-size -> Enables the size command without any pre-configured scope.",
           "type": "string",
-          "const": "core:image:allow-from-path"
+          "enum": [
+            "core:image:allow-size"
+          ]
         },
         {
-          "description": "Enables the new command without any pre-configured scope.",
+          "description": "core:image:deny-from-bytes -> Denies the from_bytes command without any pre-configured scope.",
           "type": "string",
-          "const": "core:image:allow-new"
+          "enum": [
+            "core:image:deny-from-bytes"
+          ]
         },
         {
-          "description": "Enables the rgba command without any pre-configured scope.",
+          "description": "core:image:deny-from-path -> Denies the from_path command without any pre-configured scope.",
           "type": "string",
-          "const": "core:image:allow-rgba"
+          "enum": [
+            "core:image:deny-from-path"
+          ]
         },
         {
-          "description": "Enables the size command without any pre-configured scope.",
+          "description": "core:image:deny-new -> Denies the new command without any pre-configured scope.",
           "type": "string",
-          "const": "core:image:allow-size"
+          "enum": [
+            "core:image:deny-new"
+          ]
         },
         {
-          "description": "Denies the from_bytes command without any pre-configured scope.",
+          "description": "core:image:deny-rgba -> Denies the rgba command without any pre-configured scope.",
           "type": "string",
-          "const": "core:image:deny-from-bytes"
+          "enum": [
+            "core:image:deny-rgba"
+          ]
         },
         {
-          "description": "Denies the from_path command without any pre-configured scope.",
+          "description": "core:image:deny-size -> Denies the size command without any pre-configured scope.",
           "type": "string",
-          "const": "core:image:deny-from-path"
+          "enum": [
+            "core:image:deny-size"
+          ]
         },
         {
-          "description": "Denies the new command without any pre-configured scope.",
+          "description": "core:menu:default -> Default permissions for the plugin.",
           "type": "string",
-          "const": "core:image:deny-new"
+          "enum": [
+            "core:menu:default"
+          ]
         },
         {
-          "description": "Denies the rgba command without any pre-configured scope.",
+          "description": "core:menu:allow-append -> Enables the append command without any pre-configured scope.",
           "type": "string",
-          "const": "core:image:deny-rgba"
+          "enum": [
+            "core:menu:allow-append"
+          ]
         },
         {
-          "description": "Denies the size command without any pre-configured scope.",
+          "description": "core:menu:allow-create-default -> Enables the create_default command without any pre-configured scope.",
           "type": "string",
-          "const": "core:image:deny-size"
+          "enum": [
+            "core:menu:allow-create-default"
+          ]
         },
         {
-          "description": "Default permissions for the plugin.",
+          "description": "core:menu:allow-get -> Enables the get command without any pre-configured scope.",
           "type": "string",
-          "const": "core:menu:default"
+          "enum": [
+            "core:menu:allow-get"
+          ]
         },
         {
-          "description": "Enables the append command without any pre-configured scope.",
+          "description": "core:menu:allow-insert -> Enables the insert command without any pre-configured scope.",
           "type": "string",
-          "const": "core:menu:allow-append"
+          "enum": [
+            "core:menu:allow-insert"
+          ]
         },
         {
-          "description": "Enables the create_default command without any pre-configured scope.",
+          "description": "core:menu:allow-is-checked -> Enables the is_checked command without any pre-configured scope.",
           "type": "string",
-          "const": "core:menu:allow-create-default"
+          "enum": [
+            "core:menu:allow-is-checked"
+          ]
         },
         {
-          "description": "Enables the get command without any pre-configured scope.",
+          "description": "core:menu:allow-is-enabled -> Enables the is_enabled command without any pre-configured scope.",
           "type": "string",
-          "const": "core:menu:allow-get"
+          "enum": [
+            "core:menu:allow-is-enabled"
+          ]
         },
         {
-          "description": "Enables the insert command without any pre-configured scope.",
+          "description": "core:menu:allow-items -> Enables the items command without any pre-configured scope.",
           "type": "string",
-          "const": "core:menu:allow-insert"
+          "enum": [
+            "core:menu:allow-items"
+          ]
         },
         {
-          "description": "Enables the is_checked command without any pre-configured scope.",
+          "description": "core:menu:allow-new -> Enables the new command without any pre-configured scope.",
           "type": "string",
-          "const": "core:menu:allow-is-checked"
+          "enum": [
+            "core:menu:allow-new"
+          ]
         },
         {
-          "description": "Enables the is_enabled command without any pre-configured scope.",
+          "description": "core:menu:allow-popup -> Enables the popup command without any pre-configured scope.",
           "type": "string",
-          "const": "core:menu:allow-is-enabled"
+          "enum": [
+            "core:menu:allow-popup"
+          ]
         },
         {
-          "description": "Enables the items command without any pre-configured scope.",
+          "description": "core:menu:allow-prepend -> Enables the prepend command without any pre-configured scope.",
           "type": "string",
-          "const": "core:menu:allow-items"
+          "enum": [
+            "core:menu:allow-prepend"
+          ]
         },
         {
-          "description": "Enables the new command without any pre-configured scope.",
+          "description": "core:menu:allow-remove -> Enables the remove command without any pre-configured scope.",
           "type": "string",
-          "const": "core:menu:allow-new"
+          "enum": [
+            "core:menu:allow-remove"
+          ]
         },
         {
-          "description": "Enables the popup command without any pre-configured scope.",
+          "description": "core:menu:allow-remove-at -> Enables the remove_at command without any pre-configured scope.",
           "type": "string",
-          "const": "core:menu:allow-popup"
+          "enum": [
+            "core:menu:allow-remove-at"
+          ]
         },
         {
-          "description": "Enables the prepend command without any pre-configured scope.",
+          "description": "core:menu:allow-set-accelerator -> Enables the set_accelerator command without any pre-configured scope.",
           "type": "string",
-          "const": "core:menu:allow-prepend"
+          "enum": [
+            "core:menu:allow-set-accelerator"
+          ]
         },
         {
-          "description": "Enables the remove command without any pre-configured scope.",
+          "description": "core:menu:allow-set-as-app-menu -> Enables the set_as_app_menu command without any pre-configured scope.",
           "type": "string",
-          "const": "core:menu:allow-remove"
+          "enum": [
+            "core:menu:allow-set-as-app-menu"
+          ]
         },
         {
-          "description": "Enables the remove_at command without any pre-configured scope.",
+          "description": "core:menu:allow-set-as-help-menu-for-nsapp -> Enables the set_as_help_menu_for_nsapp command without any pre-configured scope.",
           "type": "string",
-          "const": "core:menu:allow-remove-at"
+          "enum": [
+            "core:menu:allow-set-as-help-menu-for-nsapp"
+          ]
         },
         {
-          "description": "Enables the set_accelerator command without any pre-configured scope.",
+          "description": "core:menu:allow-set-as-window-menu -> Enables the set_as_window_menu command without any pre-configured scope.",
           "type": "string",
-          "const": "core:menu:allow-set-accelerator"
+          "enum": [
+            "core:menu:allow-set-as-window-menu"
+          ]
         },
         {
-          "description": "Enables the set_as_app_menu command without any pre-configured scope.",
+          "description": "core:menu:allow-set-as-windows-menu-for-nsapp -> Enables the set_as_windows_menu_for_nsapp command without any pre-configured scope.",
           "type": "string",
-          "const": "core:menu:allow-set-as-app-menu"
+          "enum": [
+            "core:menu:allow-set-as-windows-menu-for-nsapp"
+          ]
         },
         {
-          "description": "Enables the set_as_help_menu_for_nsapp command without any pre-configured scope.",
+          "description": "core:menu:allow-set-checked -> Enables the set_checked command without any pre-configured scope.",
           "type": "string",
-          "const": "core:menu:allow-set-as-help-menu-for-nsapp"
+          "enum": [
+            "core:menu:allow-set-checked"
+          ]
         },
         {
-          "description": "Enables the set_as_window_menu command without any pre-configured scope.",
+          "description": "core:menu:allow-set-enabled -> Enables the set_enabled command without any pre-configured scope.",
           "type": "string",
-          "const": "core:menu:allow-set-as-window-menu"
+          "enum": [
+            "core:menu:allow-set-enabled"
+          ]
         },
         {
-          "description": "Enables the set_as_windows_menu_for_nsapp command without any pre-configured scope.",
+          "description": "core:menu:allow-set-icon -> Enables the set_icon command without any pre-configured scope.",
           "type": "string",
-          "const": "core:menu:allow-set-as-windows-menu-for-nsapp"
+          "enum": [
+            "core:menu:allow-set-icon"
+          ]
         },
         {
-          "description": "Enables the set_checked command without any pre-configured scope.",
+          "description": "core:menu:allow-set-text -> Enables the set_text command without any pre-configured scope.",
           "type": "string",
-          "const": "core:menu:allow-set-checked"
+          "enum": [
+            "core:menu:allow-set-text"
+          ]
         },
         {
-          "description": "Enables the set_enabled command without any pre-configured scope.",
+          "description": "core:menu:allow-text -> Enables the text command without any pre-configured scope.",
           "type": "string",
-          "const": "core:menu:allow-set-enabled"
+          "enum": [
+            "core:menu:allow-text"
+          ]
         },
         {
-          "description": "Enables the set_icon command without any pre-configured scope.",
+          "description": "core:menu:deny-append -> Denies the append command without any pre-configured scope.",
           "type": "string",
-          "const": "core:menu:allow-set-icon"
+          "enum": [
+            "core:menu:deny-append"
+          ]
         },
         {
-          "description": "Enables the set_text command without any pre-configured scope.",
+          "description": "core:menu:deny-create-default -> Denies the create_default command without any pre-configured scope.",
           "type": "string",
-          "const": "core:menu:allow-set-text"
+          "enum": [
+            "core:menu:deny-create-default"
+          ]
         },
         {
-          "description": "Enables the text command without any pre-configured scope.",
+          "description": "core:menu:deny-get -> Denies the get command without any pre-configured scope.",
           "type": "string",
-          "const": "core:menu:allow-text"
+          "enum": [
+            "core:menu:deny-get"
+          ]
         },
         {
-          "description": "Denies the append command without any pre-configured scope.",
+          "description": "core:menu:deny-insert -> Denies the insert command without any pre-configured scope.",
           "type": "string",
-          "const": "core:menu:deny-append"
+          "enum": [
+            "core:menu:deny-insert"
+          ]
         },
         {
-          "description": "Denies the create_default command without any pre-configured scope.",
+          "description": "core:menu:deny-is-checked -> Denies the is_checked command without any pre-configured scope.",
           "type": "string",
-          "const": "core:menu:deny-create-default"
+          "enum": [
+            "core:menu:deny-is-checked"
+          ]
         },
         {
-          "description": "Denies the get command without any pre-configured scope.",
+          "description": "core:menu:deny-is-enabled -> Denies the is_enabled command without any pre-configured scope.",
           "type": "string",
-          "const": "core:menu:deny-get"
+          "enum": [
+            "core:menu:deny-is-enabled"
+          ]
         },
         {
-          "description": "Denies the insert command without any pre-configured scope.",
+          "description": "core:menu:deny-items -> Denies the items command without any pre-configured scope.",
           "type": "string",
-          "const": "core:menu:deny-insert"
+          "enum": [
+            "core:menu:deny-items"
+          ]
         },
         {
-          "description": "Denies the is_checked command without any pre-configured scope.",
+          "description": "core:menu:deny-new -> Denies the new command without any pre-configured scope.",
           "type": "string",
-          "const": "core:menu:deny-is-checked"
+          "enum": [
+            "core:menu:deny-new"
+          ]
         },
         {
-          "description": "Denies the is_enabled command without any pre-configured scope.",
+          "description": "core:menu:deny-popup -> Denies the popup command without any pre-configured scope.",
           "type": "string",
-          "const": "core:menu:deny-is-enabled"
+          "enum": [
+            "core:menu:deny-popup"
+          ]
         },
         {
-          "description": "Denies the items command without any pre-configured scope.",
+          "description": "core:menu:deny-prepend -> Denies the prepend command without any pre-configured scope.",
           "type": "string",
-          "const": "core:menu:deny-items"
+          "enum": [
+            "core:menu:deny-prepend"
+          ]
         },
         {
-          "description": "Denies the new command without any pre-configured scope.",
+          "description": "core:menu:deny-remove -> Denies the remove command without any pre-configured scope.",
           "type": "string",
-          "const": "core:menu:deny-new"
+          "enum": [
+            "core:menu:deny-remove"
+          ]
         },
         {
-          "description": "Denies the popup command without any pre-configured scope.",
+          "description": "core:menu:deny-remove-at -> Denies the remove_at command without any pre-configured scope.",
           "type": "string",
-          "const": "core:menu:deny-popup"
+          "enum": [
+            "core:menu:deny-remove-at"
+          ]
         },
         {
-          "description": "Denies the prepend command without any pre-configured scope.",
+          "description": "core:menu:deny-set-accelerator -> Denies the set_accelerator command without any pre-configured scope.",
           "type": "string",
-          "const": "core:menu:deny-prepend"
+          "enum": [
+            "core:menu:deny-set-accelerator"
+          ]
         },
         {
-          "description": "Denies the remove command without any pre-configured scope.",
+          "description": "core:menu:deny-set-as-app-menu -> Denies the set_as_app_menu command without any pre-configured scope.",
           "type": "string",
-          "const": "core:menu:deny-remove"
+          "enum": [
+            "core:menu:deny-set-as-app-menu"
+          ]
         },
         {
-          "description": "Denies the remove_at command without any pre-configured scope.",
+          "description": "core:menu:deny-set-as-help-menu-for-nsapp -> Denies the set_as_help_menu_for_nsapp command without any pre-configured scope.",
           "type": "string",
-          "const": "core:menu:deny-remove-at"
+          "enum": [
+            "core:menu:deny-set-as-help-menu-for-nsapp"
+          ]
         },
         {
-          "description": "Denies the set_accelerator command without any pre-configured scope.",
+          "description": "core:menu:deny-set-as-window-menu -> Denies the set_as_window_menu command without any pre-configured scope.",
           "type": "string",
-          "const": "core:menu:deny-set-accelerator"
+          "enum": [
+            "core:menu:deny-set-as-window-menu"
+          ]
         },
         {
-          "description": "Denies the set_as_app_menu command without any pre-configured scope.",
+          "description": "core:menu:deny-set-as-windows-menu-for-nsapp -> Denies the set_as_windows_menu_for_nsapp command without any pre-configured scope.",
           "type": "string",
-          "const": "core:menu:deny-set-as-app-menu"
+          "enum": [
+            "core:menu:deny-set-as-windows-menu-for-nsapp"
+          ]
         },
         {
-          "description": "Denies the set_as_help_menu_for_nsapp command without any pre-configured scope.",
+          "description": "core:menu:deny-set-checked -> Denies the set_checked command without any pre-configured scope.",
           "type": "string",
-          "const": "core:menu:deny-set-as-help-menu-for-nsapp"
+          "enum": [
+            "core:menu:deny-set-checked"
+          ]
         },
         {
-          "description": "Denies the set_as_window_menu command without any pre-configured scope.",
+          "description": "core:menu:deny-set-enabled -> Denies the set_enabled command without any pre-configured scope.",
           "type": "string",
-          "const": "core:menu:deny-set-as-window-menu"
+          "enum": [
+            "core:menu:deny-set-enabled"
+          ]
         },
         {
-          "description": "Denies the set_as_windows_menu_for_nsapp command without any pre-configured scope.",
+          "description": "core:menu:deny-set-icon -> Denies the set_icon command without any pre-configured scope.",
           "type": "string",
-          "const": "core:menu:deny-set-as-windows-menu-for-nsapp"
+          "enum": [
+            "core:menu:deny-set-icon"
+          ]
         },
         {
-          "description": "Denies the set_checked command without any pre-configured scope.",
+          "description": "core:menu:deny-set-text -> Denies the set_text command without any pre-configured scope.",
           "type": "string",
-          "const": "core:menu:deny-set-checked"
+          "enum": [
+            "core:menu:deny-set-text"
+          ]
         },
         {
-          "description": "Denies the set_enabled command without any pre-configured scope.",
+          "description": "core:menu:deny-text -> Denies the text command without any pre-configured scope.",
           "type": "string",
-          "const": "core:menu:deny-set-enabled"
+          "enum": [
+            "core:menu:deny-text"
+          ]
         },
         {
-          "description": "Denies the set_icon command without any pre-configured scope.",
+          "description": "core:path:default -> Default permissions for the plugin.",
           "type": "string",
-          "const": "core:menu:deny-set-icon"
+          "enum": [
+            "core:path:default"
+          ]
         },
         {
-          "description": "Denies the set_text command without any pre-configured scope.",
+          "description": "core:path:allow-basename -> Enables the basename command without any pre-configured scope.",
           "type": "string",
-          "const": "core:menu:deny-set-text"
+          "enum": [
+            "core:path:allow-basename"
+          ]
         },
         {
-          "description": "Denies the text command without any pre-configured scope.",
+          "description": "core:path:allow-dirname -> Enables the dirname command without any pre-configured scope.",
           "type": "string",
-          "const": "core:menu:deny-text"
+          "enum": [
+            "core:path:allow-dirname"
+          ]
         },
         {
-          "description": "Default permissions for the plugin.",
+          "description": "core:path:allow-extname -> Enables the extname command without any pre-configured scope.",
           "type": "string",
-          "const": "core:path:default"
+          "enum": [
+            "core:path:allow-extname"
+          ]
         },
         {
-          "description": "Enables the basename command without any pre-configured scope.",
+          "description": "core:path:allow-is-absolute -> Enables the is_absolute command without any pre-configured scope.",
           "type": "string",
-          "const": "core:path:allow-basename"
+          "enum": [
+            "core:path:allow-is-absolute"
+          ]
         },
         {
-          "description": "Enables the dirname command without any pre-configured scope.",
+          "description": "core:path:allow-join -> Enables the join command without any pre-configured scope.",
           "type": "string",
-          "const": "core:path:allow-dirname"
+          "enum": [
+            "core:path:allow-join"
+          ]
         },
         {
-          "description": "Enables the extname command without any pre-configured scope.",
+          "description": "core:path:allow-normalize -> Enables the normalize command without any pre-configured scope.",
           "type": "string",
-          "const": "core:path:allow-extname"
+          "enum": [
+            "core:path:allow-normalize"
+          ]
         },
         {
-          "description": "Enables the is_absolute command without any pre-configured scope.",
+          "description": "core:path:allow-resolve -> Enables the resolve command without any pre-configured scope.",
           "type": "string",
-          "const": "core:path:allow-is-absolute"
+          "enum": [
+            "core:path:allow-resolve"
+          ]
         },
         {
-          "description": "Enables the join command without any pre-configured scope.",
+          "description": "core:path:allow-resolve-directory -> Enables the resolve_directory command without any pre-configured scope.",
           "type": "string",
-          "const": "core:path:allow-join"
+          "enum": [
+            "core:path:allow-resolve-directory"
+          ]
         },
         {
-          "description": "Enables the normalize command without any pre-configured scope.",
+          "description": "core:path:deny-basename -> Denies the basename command without any pre-configured scope.",
           "type": "string",
-          "const": "core:path:allow-normalize"
+          "enum": [
+            "core:path:deny-basename"
+          ]
         },
         {
-          "description": "Enables the resolve command without any pre-configured scope.",
+          "description": "core:path:deny-dirname -> Denies the dirname command without any pre-configured scope.",
           "type": "string",
-          "const": "core:path:allow-resolve"
+          "enum": [
+            "core:path:deny-dirname"
+          ]
         },
         {
-          "description": "Enables the resolve_directory command without any pre-configured scope.",
+          "description": "core:path:deny-extname -> Denies the extname command without any pre-configured scope.",
           "type": "string",
-          "const": "core:path:allow-resolve-directory"
+          "enum": [
+            "core:path:deny-extname"
+          ]
         },
         {
-          "description": "Denies the basename command without any pre-configured scope.",
+          "description": "core:path:deny-is-absolute -> Denies the is_absolute command without any pre-configured scope.",
           "type": "string",
-          "const": "core:path:deny-basename"
+          "enum": [
+            "core:path:deny-is-absolute"
+          ]
         },
         {
-          "description": "Denies the dirname command without any pre-configured scope.",
+          "description": "core:path:deny-join -> Denies the join command without any pre-configured scope.",
           "type": "string",
-          "const": "core:path:deny-dirname"
+          "enum": [
+            "core:path:deny-join"
+          ]
         },
         {
-          "description": "Denies the extname command without any pre-configured scope.",
+          "description": "core:path:deny-normalize -> Denies the normalize command without any pre-configured scope.",
           "type": "string",
-          "const": "core:path:deny-extname"
+          "enum": [
+            "core:path:deny-normalize"
+          ]
         },
         {
-          "description": "Denies the is_absolute command without any pre-configured scope.",
+          "description": "core:path:deny-resolve -> Denies the resolve command without any pre-configured scope.",
           "type": "string",
-          "const": "core:path:deny-is-absolute"
+          "enum": [
+            "core:path:deny-resolve"
+          ]
         },
         {
-          "description": "Denies the join command without any pre-configured scope.",
+          "description": "core:path:deny-resolve-directory -> Denies the resolve_directory command without any pre-configured scope.",
           "type": "string",
-          "const": "core:path:deny-join"
+          "enum": [
+            "core:path:deny-resolve-directory"
+          ]
         },
         {
-          "description": "Denies the normalize command without any pre-configured scope.",
+          "description": "core:resources:default -> Default permissions for the plugin.",
           "type": "string",
-          "const": "core:path:deny-normalize"
+          "enum": [
+            "core:resources:default"
+          ]
         },
         {
-          "description": "Denies the resolve command without any pre-configured scope.",
+          "description": "core:resources:allow-close -> Enables the close command without any pre-configured scope.",
           "type": "string",
-          "const": "core:path:deny-resolve"
+          "enum": [
+            "core:resources:allow-close"
+          ]
         },
         {
-          "description": "Denies the resolve_directory command without any pre-configured scope.",
+          "description": "core:resources:deny-close -> Denies the close command without any pre-configured scope.",
           "type": "string",
-          "const": "core:path:deny-resolve-directory"
+          "enum": [
+            "core:resources:deny-close"
+          ]
         },
         {
-          "description": "Default permissions for the plugin.",
+          "description": "core:tray:default -> Default permissions for the plugin.",
           "type": "string",
-          "const": "core:resources:default"
+          "enum": [
+            "core:tray:default"
+          ]
         },
         {
-          "description": "Enables the close command without any pre-configured scope.",
+          "description": "core:tray:allow-get-by-id -> Enables the get_by_id command without any pre-configured scope.",
           "type": "string",
-          "const": "core:resources:allow-close"
+          "enum": [
+            "core:tray:allow-get-by-id"
+          ]
         },
         {
-          "description": "Denies the close command without any pre-configured scope.",
+          "description": "core:tray:allow-new -> Enables the new command without any pre-configured scope.",
           "type": "string",
-          "const": "core:resources:deny-close"
+          "enum": [
+            "core:tray:allow-new"
+          ]
         },
         {
-          "description": "Default permissions for the plugin.",
+          "description": "core:tray:allow-remove-by-id -> Enables the remove_by_id command without any pre-configured scope.",
           "type": "string",
-          "const": "core:tray:default"
+          "enum": [
+            "core:tray:allow-remove-by-id"
+          ]
         },
         {
-          "description": "Enables the get_by_id command without any pre-configured scope.",
+          "description": "core:tray:allow-set-icon -> Enables the set_icon command without any pre-configured scope.",
           "type": "string",
-          "const": "core:tray:allow-get-by-id"
+          "enum": [
+            "core:tray:allow-set-icon"
+          ]
         },
         {
-          "description": "Enables the new command without any pre-configured scope.",
+          "description": "core:tray:allow-set-icon-as-template -> Enables the set_icon_as_template command without any pre-configured scope.",
           "type": "string",
-          "const": "core:tray:allow-new"
+          "enum": [
+            "core:tray:allow-set-icon-as-template"
+          ]
         },
         {
-          "description": "Enables the remove_by_id command without any pre-configured scope.",
+          "description": "core:tray:allow-set-menu -> Enables the set_menu command without any pre-configured scope.",
           "type": "string",
-          "const": "core:tray:allow-remove-by-id"
+          "enum": [
+            "core:tray:allow-set-menu"
+          ]
         },
         {
-          "description": "Enables the set_icon command without any pre-configured scope.",
+          "description": "core:tray:allow-set-show-menu-on-left-click -> Enables the set_show_menu_on_left_click command without any pre-configured scope.",
           "type": "string",
-          "const": "core:tray:allow-set-icon"
+          "enum": [
+            "core:tray:allow-set-show-menu-on-left-click"
+          ]
         },
         {
-          "description": "Enables the set_icon_as_template command without any pre-configured scope.",
+          "description": "core:tray:allow-set-temp-dir-path -> Enables the set_temp_dir_path command without any pre-configured scope.",
           "type": "string",
-          "const": "core:tray:allow-set-icon-as-template"
+          "enum": [
+            "core:tray:allow-set-temp-dir-path"
+          ]
         },
         {
-          "description": "Enables the set_menu command without any pre-configured scope.",
+          "description": "core:tray:allow-set-title -> Enables the set_title command without any pre-configured scope.",
           "type": "string",
-          "const": "core:tray:allow-set-menu"
+          "enum": [
+            "core:tray:allow-set-title"
+          ]
         },
         {
-          "description": "Enables the set_show_menu_on_left_click command without any pre-configured scope.",
+          "description": "core:tray:allow-set-tooltip -> Enables the set_tooltip command without any pre-configured scope.",
           "type": "string",
-          "const": "core:tray:allow-set-show-menu-on-left-click"
+          "enum": [
+            "core:tray:allow-set-tooltip"
+          ]
         },
         {
-          "description": "Enables the set_temp_dir_path command without any pre-configured scope.",
+          "description": "core:tray:allow-set-visible -> Enables the set_visible command without any pre-configured scope.",
           "type": "string",
-          "const": "core:tray:allow-set-temp-dir-path"
+          "enum": [
+            "core:tray:allow-set-visible"
+          ]
         },
         {
-          "description": "Enables the set_title command without any pre-configured scope.",
+          "description": "core:tray:deny-get-by-id -> Denies the get_by_id command without any pre-configured scope.",
           "type": "string",
-          "const": "core:tray:allow-set-title"
+          "enum": [
+            "core:tray:deny-get-by-id"
+          ]
         },
         {
-          "description": "Enables the set_tooltip command without any pre-configured scope.",
+          "description": "core:tray:deny-new -> Denies the new command without any pre-configured scope.",
           "type": "string",
-          "const": "core:tray:allow-set-tooltip"
+          "enum": [
+            "core:tray:deny-new"
+          ]
         },
         {
-          "description": "Enables the set_visible command without any pre-configured scope.",
+          "description": "core:tray:deny-remove-by-id -> Denies the remove_by_id command without any pre-configured scope.",
           "type": "string",
-          "const": "core:tray:allow-set-visible"
+          "enum": [
+            "core:tray:deny-remove-by-id"
+          ]
         },
         {
-          "description": "Denies the get_by_id command without any pre-configured scope.",
+          "description": "core:tray:deny-set-icon -> Denies the set_icon command without any pre-configured scope.",
           "type": "string",
-          "const": "core:tray:deny-get-by-id"
+          "enum": [
+            "core:tray:deny-set-icon"
+          ]
         },
         {
-          "description": "Denies the new command without any pre-configured scope.",
+          "description": "core:tray:deny-set-icon-as-template -> Denies the set_icon_as_template command without any pre-configured scope.",
           "type": "string",
-          "const": "core:tray:deny-new"
+          "enum": [
+            "core:tray:deny-set-icon-as-template"
+          ]
         },
         {
-          "description": "Denies the remove_by_id command without any pre-configured scope.",
+          "description": "core:tray:deny-set-menu -> Denies the set_menu command without any pre-configured scope.",
           "type": "string",
-          "const": "core:tray:deny-remove-by-id"
+          "enum": [
+            "core:tray:deny-set-menu"
+          ]
         },
         {
-          "description": "Denies the set_icon command without any pre-configured scope.",
+          "description": "core:tray:deny-set-show-menu-on-left-click -> Denies the set_show_menu_on_left_click command without any pre-configured scope.",
           "type": "string",
-          "const": "core:tray:deny-set-icon"
+          "enum": [
+            "core:tray:deny-set-show-menu-on-left-click"
+          ]
         },
         {
-          "description": "Denies the set_icon_as_template command without any pre-configured scope.",
+          "description": "core:tray:deny-set-temp-dir-path -> Denies the set_temp_dir_path command without any pre-configured scope.",
           "type": "string",
-          "const": "core:tray:deny-set-icon-as-template"
+          "enum": [
+            "core:tray:deny-set-temp-dir-path"
+          ]
         },
         {
-          "description": "Denies the set_menu command without any pre-configured scope.",
+          "description": "core:tray:deny-set-title -> Denies the set_title command without any pre-configured scope.",
           "type": "string",
-          "const": "core:tray:deny-set-menu"
+          "enum": [
+            "core:tray:deny-set-title"
+          ]
         },
         {
-          "description": "Denies the set_show_menu_on_left_click command without any pre-configured scope.",
+          "description": "core:tray:deny-set-tooltip -> Denies the set_tooltip command without any pre-configured scope.",
           "type": "string",
-          "const": "core:tray:deny-set-show-menu-on-left-click"
+          "enum": [
+            "core:tray:deny-set-tooltip"
+          ]
         },
         {
-          "description": "Denies the set_temp_dir_path command without any pre-configured scope.",
+          "description": "core:tray:deny-set-visible -> Denies the set_visible command without any pre-configured scope.",
           "type": "string",
-          "const": "core:tray:deny-set-temp-dir-path"
+          "enum": [
+            "core:tray:deny-set-visible"
+          ]
         },
         {
-          "description": "Denies the set_title command without any pre-configured scope.",
+          "description": "core:webview:default -> Default permissions for the plugin.",
           "type": "string",
-          "const": "core:tray:deny-set-title"
+          "enum": [
+            "core:webview:default"
+          ]
         },
         {
-          "description": "Denies the set_tooltip command without any pre-configured scope.",
+          "description": "core:webview:allow-create-webview -> Enables the create_webview command without any pre-configured scope.",
           "type": "string",
-          "const": "core:tray:deny-set-tooltip"
+          "enum": [
+            "core:webview:allow-create-webview"
+          ]
         },
         {
-          "description": "Denies the set_visible command without any pre-configured scope.",
+          "description": "core:webview:allow-create-webview-window -> Enables the create_webview_window command without any pre-configured scope.",
           "type": "string",
-          "const": "core:tray:deny-set-visible"
+          "enum": [
+            "core:webview:allow-create-webview-window"
+          ]
         },
         {
-          "description": "Default permissions for the plugin.",
+          "description": "core:webview:allow-get-all-webviews -> Enables the get_all_webviews command without any pre-configured scope.",
           "type": "string",
-          "const": "core:webview:default"
+          "enum": [
+            "core:webview:allow-get-all-webviews"
+          ]
         },
         {
-          "description": "Enables the clear_all_browsing_data command without any pre-configured scope.",
+          "description": "core:webview:allow-internal-toggle-devtools -> Enables the internal_toggle_devtools command without any pre-configured scope.",
           "type": "string",
-          "const": "core:webview:allow-clear-all-browsing-data"
+          "enum": [
+            "core:webview:allow-internal-toggle-devtools"
+          ]
         },
         {
-          "description": "Enables the create_webview command without any pre-configured scope.",
+          "description": "core:webview:allow-print -> Enables the print command without any pre-configured scope.",
           "type": "string",
-          "const": "core:webview:allow-create-webview"
+          "enum": [
+            "core:webview:allow-print"
+          ]
         },
         {
-          "description": "Enables the create_webview_window command without any pre-configured scope.",
+          "description": "core:webview:allow-reparent -> Enables the reparent command without any pre-configured scope.",
           "type": "string",
-          "const": "core:webview:allow-create-webview-window"
+          "enum": [
+            "core:webview:allow-reparent"
+          ]
         },
         {
-          "description": "Enables the get_all_webviews command without any pre-configured scope.",
+          "description": "core:webview:allow-set-webview-focus -> Enables the set_webview_focus command without any pre-configured scope.",
           "type": "string",
-          "const": "core:webview:allow-get-all-webviews"
+          "enum": [
+            "core:webview:allow-set-webview-focus"
+          ]
         },
         {
-          "description": "Enables the internal_toggle_devtools command without any pre-configured scope.",
+          "description": "core:webview:allow-set-webview-position -> Enables the set_webview_position command without any pre-configured scope.",
           "type": "string",
-          "const": "core:webview:allow-internal-toggle-devtools"
+          "enum": [
+            "core:webview:allow-set-webview-position"
+          ]
         },
         {
-          "description": "Enables the print command without any pre-configured scope.",
+          "description": "core:webview:allow-set-webview-size -> Enables the set_webview_size command without any pre-configured scope.",
           "type": "string",
-          "const": "core:webview:allow-print"
+          "enum": [
+            "core:webview:allow-set-webview-size"
+          ]
         },
         {
-          "description": "Enables the reparent command without any pre-configured scope.",
+          "description": "core:webview:allow-set-webview-zoom -> Enables the set_webview_zoom command without any pre-configured scope.",
           "type": "string",
-          "const": "core:webview:allow-reparent"
+          "enum": [
+            "core:webview:allow-set-webview-zoom"
+          ]
         },
         {
-          "description": "Enables the set_webview_focus command without any pre-configured scope.",
+          "description": "core:webview:allow-webview-close -> Enables the webview_close command without any pre-configured scope.",
           "type": "string",
-          "const": "core:webview:allow-set-webview-focus"
+          "enum": [
+            "core:webview:allow-webview-close"
+          ]
         },
         {
-          "description": "Enables the set_webview_position command without any pre-configured scope.",
+          "description": "core:webview:allow-webview-position -> Enables the webview_position command without any pre-configured scope.",
           "type": "string",
-          "const": "core:webview:allow-set-webview-position"
+          "enum": [
+            "core:webview:allow-webview-position"
+          ]
         },
         {
-          "description": "Enables the set_webview_size command without any pre-configured scope.",
+          "description": "core:webview:allow-webview-size -> Enables the webview_size command without any pre-configured scope.",
           "type": "string",
-          "const": "core:webview:allow-set-webview-size"
+          "enum": [
+            "core:webview:allow-webview-size"
+          ]
         },
         {
-          "description": "Enables the set_webview_zoom command without any pre-configured scope.",
+          "description": "core:webview:deny-create-webview -> Denies the create_webview command without any pre-configured scope.",
           "type": "string",
-          "const": "core:webview:allow-set-webview-zoom"
+          "enum": [
+            "core:webview:deny-create-webview"
+          ]
         },
         {
-          "description": "Enables the webview_close command without any pre-configured scope.",
+          "description": "core:webview:deny-create-webview-window -> Denies the create_webview_window command without any pre-configured scope.",
           "type": "string",
-          "const": "core:webview:allow-webview-close"
+          "enum": [
+            "core:webview:deny-create-webview-window"
+          ]
         },
         {
-          "description": "Enables the webview_hide command without any pre-configured scope.",
+          "description": "core:webview:deny-get-all-webviews -> Denies the get_all_webviews command without any pre-configured scope.",
           "type": "string",
-          "const": "core:webview:allow-webview-hide"
+          "enum": [
+            "core:webview:deny-get-all-webviews"
+          ]
         },
         {
-          "description": "Enables the webview_position command without any pre-configured scope.",
+          "description": "core:webview:deny-internal-toggle-devtools -> Denies the internal_toggle_devtools command without any pre-configured scope.",
           "type": "string",
-          "const": "core:webview:allow-webview-position"
+          "enum": [
+            "core:webview:deny-internal-toggle-devtools"
+          ]
         },
         {
-          "description": "Enables the webview_show command without any pre-configured scope.",
+          "description": "core:webview:deny-print -> Denies the print command without any pre-configured scope.",
           "type": "string",
-          "const": "core:webview:allow-webview-show"
+          "enum": [
+            "core:webview:deny-print"
+          ]
         },
         {
-          "description": "Enables the webview_size command without any pre-configured scope.",
+          "description": "core:webview:deny-reparent -> Denies the reparent command without any pre-configured scope.",
           "type": "string",
-          "const": "core:webview:allow-webview-size"
+          "enum": [
+            "core:webview:deny-reparent"
+          ]
         },
         {
-          "description": "Denies the clear_all_browsing_data command without any pre-configured scope.",
+          "description": "core:webview:deny-set-webview-focus -> Denies the set_webview_focus command without any pre-configured scope.",
           "type": "string",
-          "const": "core:webview:deny-clear-all-browsing-data"
+          "enum": [
+            "core:webview:deny-set-webview-focus"
+          ]
         },
         {
-          "description": "Denies the create_webview command without any pre-configured scope.",
+          "description": "core:webview:deny-set-webview-position -> Denies the set_webview_position command without any pre-configured scope.",
           "type": "string",
-          "const": "core:webview:deny-create-webview"
+          "enum": [
+            "core:webview:deny-set-webview-position"
+          ]
         },
         {
-          "description": "Denies the create_webview_window command without any pre-configured scope.",
+          "description": "core:webview:deny-set-webview-size -> Denies the set_webview_size command without any pre-configured scope.",
           "type": "string",
-          "const": "core:webview:deny-create-webview-window"
+          "enum": [
+            "core:webview:deny-set-webview-size"
+          ]
         },
         {
-          "description": "Denies the get_all_webviews command without any pre-configured scope.",
+          "description": "core:webview:deny-set-webview-zoom -> Denies the set_webview_zoom command without any pre-configured scope.",
           "type": "string",
-          "const": "core:webview:deny-get-all-webviews"
+          "enum": [
+            "core:webview:deny-set-webview-zoom"
+          ]
         },
         {
-          "description": "Denies the internal_toggle_devtools command without any pre-configured scope.",
+          "description": "core:webview:deny-webview-close -> Denies the webview_close command without any pre-configured scope.",
           "type": "string",
-          "const": "core:webview:deny-internal-toggle-devtools"
+          "enum": [
+            "core:webview:deny-webview-close"
+          ]
         },
         {
-          "description": "Denies the print command without any pre-configured scope.",
+          "description": "core:webview:deny-webview-position -> Denies the webview_position command without any pre-configured scope.",
           "type": "string",
-          "const": "core:webview:deny-print"
+          "enum": [
+            "core:webview:deny-webview-position"
+          ]
         },
         {
-          "description": "Denies the reparent command without any pre-configured scope.",
+          "description": "core:webview:deny-webview-size -> Denies the webview_size command without any pre-configured scope.",
           "type": "string",
-          "const": "core:webview:deny-reparent"
+          "enum": [
+            "core:webview:deny-webview-size"
+          ]
         },
         {
-          "description": "Denies the set_webview_focus command without any pre-configured scope.",
+          "description": "core:window:default -> Default permissions for the plugin.",
           "type": "string",
-          "const": "core:webview:deny-set-webview-focus"
+          "enum": [
+            "core:window:default"
+          ]
         },
         {
-          "description": "Denies the set_webview_position command without any pre-configured scope.",
+          "description": "core:window:allow-available-monitors -> Enables the available_monitors command without any pre-configured scope.",
           "type": "string",
-          "const": "core:webview:deny-set-webview-position"
+          "enum": [
+            "core:window:allow-available-monitors"
+          ]
         },
         {
-          "description": "Denies the set_webview_size command without any pre-configured scope.",
+          "description": "core:window:allow-center -> Enables the center command without any pre-configured scope.",
           "type": "string",
-          "const": "core:webview:deny-set-webview-size"
+          "enum": [
+            "core:window:allow-center"
+          ]
         },
         {
-          "description": "Denies the set_webview_zoom command without any pre-configured scope.",
+          "description": "core:window:allow-close -> Enables the close command without any pre-configured scope.",
           "type": "string",
-          "const": "core:webview:deny-set-webview-zoom"
+          "enum": [
+            "core:window:allow-close"
+          ]
         },
         {
-          "description": "Denies the webview_close command without any pre-configured scope.",
+          "description": "core:window:allow-create -> Enables the create command without any pre-configured scope.",
           "type": "string",
-          "const": "core:webview:deny-webview-close"
+          "enum": [
+            "core:window:allow-create"
+          ]
         },
         {
-          "description": "Denies the webview_hide command without any pre-configured scope.",
+          "description": "core:window:allow-current-monitor -> Enables the current_monitor command without any pre-configured scope.",
           "type": "string",
-          "const": "core:webview:deny-webview-hide"
+          "enum": [
+            "core:window:allow-current-monitor"
+          ]
         },
         {
-          "description": "Denies the webview_position command without any pre-configured scope.",
+          "description": "core:window:allow-cursor-position -> Enables the cursor_position command without any pre-configured scope.",
           "type": "string",
-          "const": "core:webview:deny-webview-position"
+          "enum": [
+            "core:window:allow-cursor-position"
+          ]
         },
         {
-          "description": "Denies the webview_show command without any pre-configured scope.",
+          "description": "core:window:allow-destroy -> Enables the destroy command without any pre-configured scope.",
           "type": "string",
-          "const": "core:webview:deny-webview-show"
+          "enum": [
+            "core:window:allow-destroy"
+          ]
         },
         {
-          "description": "Denies the webview_size command without any pre-configured scope.",
+          "description": "core:window:allow-get-all-windows -> Enables the get_all_windows command without any pre-configured scope.",
           "type": "string",
-          "const": "core:webview:deny-webview-size"
+          "enum": [
+            "core:window:allow-get-all-windows"
+          ]
         },
         {
-          "description": "Default permissions for the plugin.",
+          "description": "core:window:allow-hide -> Enables the hide command without any pre-configured scope.",
           "type": "string",
-          "const": "core:window:default"
+          "enum": [
+            "core:window:allow-hide"
+          ]
         },
         {
-          "description": "Enables the available_monitors command without any pre-configured scope.",
+          "description": "core:window:allow-inner-position -> Enables the inner_position command without any pre-configured scope.",
           "type": "string",
-          "const": "core:window:allow-available-monitors"
+          "enum": [
+            "core:window:allow-inner-position"
+          ]
         },
         {
-          "description": "Enables the center command without any pre-configured scope.",
+          "description": "core:window:allow-inner-size -> Enables the inner_size command without any pre-configured scope.",
           "type": "string",
-          "const": "core:window:allow-center"
+          "enum": [
+            "core:window:allow-inner-size"
+          ]
         },
         {
-          "description": "Enables the close command without any pre-configured scope.",
+          "description": "core:window:allow-internal-toggle-maximize -> Enables the internal_toggle_maximize command without any pre-configured scope.",
           "type": "string",
-          "const": "core:window:allow-close"
+          "enum": [
+            "core:window:allow-internal-toggle-maximize"
+          ]
         },
         {
-          "description": "Enables the create command without any pre-configured scope.",
+          "description": "core:window:allow-is-closable -> Enables the is_closable command without any pre-configured scope.",
           "type": "string",
-          "const": "core:window:allow-create"
+          "enum": [
+            "core:window:allow-is-closable"
+          ]
         },
         {
-          "description": "Enables the current_monitor command without any pre-configured scope.",
+          "description": "core:window:allow-is-decorated -> Enables the is_decorated command without any pre-configured scope.",
           "type": "string",
-          "const": "core:window:allow-current-monitor"
+          "enum": [
+            "core:window:allow-is-decorated"
+          ]
         },
         {
-          "description": "Enables the cursor_position command without any pre-configured scope.",
+          "description": "core:window:allow-is-focused -> Enables the is_focused command without any pre-configured scope.",
           "type": "string",
-          "const": "core:window:allow-cursor-position"
+          "enum": [
+            "core:window:allow-is-focused"
+          ]
         },
         {
-          "description": "Enables the destroy command without any pre-configured scope.",
+          "description": "core:window:allow-is-fullscreen -> Enables the is_fullscreen command without any pre-configured scope.",
           "type": "string",
-          "const": "core:window:allow-destroy"
+          "enum": [
+            "core:window:allow-is-fullscreen"
+          ]
         },
         {
-          "description": "Enables the get_all_windows command without any pre-configured scope.",
+          "description": "core:window:allow-is-maximizable -> Enables the is_maximizable command without any pre-configured scope.",
           "type": "string",
-          "const": "core:window:allow-get-all-windows"
+          "enum": [
+            "core:window:allow-is-maximizable"
+          ]
         },
         {
-          "description": "Enables the hide command without any pre-configured scope.",
+          "description": "core:window:allow-is-maximized -> Enables the is_maximized command without any pre-configured scope.",
           "type": "string",
-          "const": "core:window:allow-hide"
+          "enum": [
+            "core:window:allow-is-maximized"
+          ]
         },
         {
-          "description": "Enables the inner_position command without any pre-configured scope.",
+          "description": "core:window:allow-is-minimizable -> Enables the is_minimizable command without any pre-configured scope.",
           "type": "string",
-          "const": "core:window:allow-inner-position"
+          "enum": [
+            "core:window:allow-is-minimizable"
+          ]
         },
         {
-          "description": "Enables the inner_size command without any pre-configured scope.",
+          "description": "core:window:allow-is-minimized -> Enables the is_minimized command without any pre-configured scope.",
           "type": "string",
-          "const": "core:window:allow-inner-size"
+          "enum": [
+            "core:window:allow-is-minimized"
+          ]
         },
         {
-          "description": "Enables the internal_toggle_maximize command without any pre-configured scope.",
+          "description": "core:window:allow-is-resizable -> Enables the is_resizable command without any pre-configured scope.",
           "type": "string",
-          "const": "core:window:allow-internal-toggle-maximize"
+          "enum": [
+            "core:window:allow-is-resizable"
+          ]
         },
         {
-          "description": "Enables the is_closable command without any pre-configured scope.",
+          "description": "core:window:allow-is-visible -> Enables the is_visible command without any pre-configured scope.",
           "type": "string",
-          "const": "core:window:allow-is-closable"
+          "enum": [
+            "core:window:allow-is-visible"
+          ]
         },
         {
-          "description": "Enables the is_decorated command without any pre-configured scope.",
+          "description": "core:window:allow-maximize -> Enables the maximize command without any pre-configured scope.",
           "type": "string",
-          "const": "core:window:allow-is-decorated"
+          "enum": [
+            "core:window:allow-maximize"
+          ]
         },
         {
-          "description": "Enables the is_enabled command without any pre-configured scope.",
+          "description": "core:window:allow-minimize -> Enables the minimize command without any pre-configured scope.",
           "type": "string",
-          "const": "core:window:allow-is-enabled"
+          "enum": [
+            "core:window:allow-minimize"
+          ]
         },
         {
-          "description": "Enables the is_focused command without any pre-configured scope.",
+          "description": "core:window:allow-monitor-from-point -> Enables the monitor_from_point command without any pre-configured scope.",
           "type": "string",
-          "const": "core:window:allow-is-focused"
+          "enum": [
+            "core:window:allow-monitor-from-point"
+          ]
         },
         {
-          "description": "Enables the is_fullscreen command without any pre-configured scope.",
+          "description": "core:window:allow-outer-position -> Enables the outer_position command without any pre-configured scope.",
           "type": "string",
-          "const": "core:window:allow-is-fullscreen"
+          "enum": [
+            "core:window:allow-outer-position"
+          ]
         },
         {
-          "description": "Enables the is_maximizable command without any pre-configured scope.",
+          "description": "core:window:allow-outer-size -> Enables the outer_size command without any pre-configured scope.",
           "type": "string",
-          "const": "core:window:allow-is-maximizable"
+          "enum": [
+            "core:window:allow-outer-size"
+          ]
         },
         {
-          "description": "Enables the is_maximized command without any pre-configured scope.",
+          "description": "core:window:allow-primary-monitor -> Enables the primary_monitor command without any pre-configured scope.",
           "type": "string",
-          "const": "core:window:allow-is-maximized"
+          "enum": [
+            "core:window:allow-primary-monitor"
+          ]
         },
         {
-          "description": "Enables the is_minimizable command without any pre-configured scope.",
+          "description": "core:window:allow-request-user-attention -> Enables the request_user_attention command without any pre-configured scope.",
           "type": "string",
-          "const": "core:window:allow-is-minimizable"
+          "enum": [
+            "core:window:allow-request-user-attention"
+          ]
         },
         {
-          "description": "Enables the is_minimized command without any pre-configured scope.",
+          "description": "core:window:allow-scale-factor -> Enables the scale_factor command without any pre-configured scope.",
           "type": "string",
-          "const": "core:window:allow-is-minimized"
+          "enum": [
+            "core:window:allow-scale-factor"
+          ]
         },
         {
-          "description": "Enables the is_resizable command without any pre-configured scope.",
+          "description": "core:window:allow-set-always-on-bottom -> Enables the set_always_on_bottom command without any pre-configured scope.",
           "type": "string",
-          "const": "core:window:allow-is-resizable"
+          "enum": [
+            "core:window:allow-set-always-on-bottom"
+          ]
         },
         {
-          "description": "Enables the is_visible command without any pre-configured scope.",
+          "description": "core:window:allow-set-always-on-top -> Enables the set_always_on_top command without any pre-configured scope.",
           "type": "string",
-          "const": "core:window:allow-is-visible"
+          "enum": [
+            "core:window:allow-set-always-on-top"
+          ]
         },
         {
-          "description": "Enables the maximize command without any pre-configured scope.",
+          "description": "core:window:allow-set-closable -> Enables the set_closable command without any pre-configured scope.",
           "type": "string",
-          "const": "core:window:allow-maximize"
+          "enum": [
+            "core:window:allow-set-closable"
+          ]
         },
         {
-          "description": "Enables the minimize command without any pre-configured scope.",
+          "description": "core:window:allow-set-content-protected -> Enables the set_content_protected command without any pre-configured scope.",
           "type": "string",
-          "const": "core:window:allow-minimize"
+          "enum": [
+            "core:window:allow-set-content-protected"
+          ]
         },
         {
-          "description": "Enables the monitor_from_point command without any pre-configured scope.",
+          "description": "core:window:allow-set-cursor-grab -> Enables the set_cursor_grab command without any pre-configured scope.",
           "type": "string",
-          "const": "core:window:allow-monitor-from-point"
+          "enum": [
+            "core:window:allow-set-cursor-grab"
+          ]
         },
         {
-          "description": "Enables the outer_position command without any pre-configured scope.",
+          "description": "core:window:allow-set-cursor-icon -> Enables the set_cursor_icon command without any pre-configured scope.",
           "type": "string",
-          "const": "core:window:allow-outer-position"
+          "enum": [
+            "core:window:allow-set-cursor-icon"
+          ]
         },
         {
-          "description": "Enables the outer_size command without any pre-configured scope.",
+          "description": "core:window:allow-set-cursor-position -> Enables the set_cursor_position command without any pre-configured scope.",
           "type": "string",
-          "const": "core:window:allow-outer-size"
+          "enum": [
+            "core:window:allow-set-cursor-position"
+          ]
         },
         {
-          "description": "Enables the primary_monitor command without any pre-configured scope.",
+          "description": "core:window:allow-set-cursor-visible -> Enables the set_cursor_visible command without any pre-configured scope.",
           "type": "string",
-          "const": "core:window:allow-primary-monitor"
+          "enum": [
+            "core:window:allow-set-cursor-visible"
+          ]
         },
         {
-          "description": "Enables the request_user_attention command without any pre-configured scope.",
+          "description": "core:window:allow-set-decorations -> Enables the set_decorations command without any pre-configured scope.",
           "type": "string",
-          "const": "core:window:allow-request-user-attention"
+          "enum": [
+            "core:window:allow-set-decorations"
+          ]
         },
         {
-          "description": "Enables the scale_factor command without any pre-configured scope.",
+          "description": "core:window:allow-set-effects -> Enables the set_effects command without any pre-configured scope.",
           "type": "string",
-          "const": "core:window:allow-scale-factor"
+          "enum": [
+            "core:window:allow-set-effects"
+          ]
         },
         {
-          "description": "Enables the set_always_on_bottom command without any pre-configured scope.",
+          "description": "core:window:allow-set-focus -> Enables the set_focus command without any pre-configured scope.",
           "type": "string",
-          "const": "core:window:allow-set-always-on-bottom"
+          "enum": [
+            "core:window:allow-set-focus"
+          ]
         },
         {
-          "description": "Enables the set_always_on_top command without any pre-configured scope.",
+          "description": "core:window:allow-set-fullscreen -> Enables the set_fullscreen command without any pre-configured scope.",
           "type": "string",
-          "const": "core:window:allow-set-always-on-top"
+          "enum": [
+            "core:window:allow-set-fullscreen"
+          ]
         },
         {
-          "description": "Enables the set_closable command without any pre-configured scope.",
+          "description": "core:window:allow-set-icon -> Enables the set_icon command without any pre-configured scope.",
           "type": "string",
-          "const": "core:window:allow-set-closable"
+          "enum": [
+            "core:window:allow-set-icon"
+          ]
         },
         {
-          "description": "Enables the set_content_protected command without any pre-configured scope.",
+          "description": "core:window:allow-set-ignore-cursor-events -> Enables the set_ignore_cursor_events command without any pre-configured scope.",
           "type": "string",
-          "const": "core:window:allow-set-content-protected"
+          "enum": [
+            "core:window:allow-set-ignore-cursor-events"
+          ]
         },
         {
-          "description": "Enables the set_cursor_grab command without any pre-configured scope.",
+          "description": "core:window:allow-set-max-size -> Enables the set_max_size command without any pre-configured scope.",
           "type": "string",
-          "const": "core:window:allow-set-cursor-grab"
+          "enum": [
+            "core:window:allow-set-max-size"
+          ]
         },
         {
-          "description": "Enables the set_cursor_icon command without any pre-configured scope.",
+          "description": "core:window:allow-set-maximizable -> Enables the set_maximizable command without any pre-configured scope.",
           "type": "string",
-          "const": "core:window:allow-set-cursor-icon"
+          "enum": [
+            "core:window:allow-set-maximizable"
+          ]
         },
         {
-          "description": "Enables the set_cursor_position command without any pre-configured scope.",
+          "description": "core:window:allow-set-min-size -> Enables the set_min_size command without any pre-configured scope.",
           "type": "string",
-          "const": "core:window:allow-set-cursor-position"
+          "enum": [
+            "core:window:allow-set-min-size"
+          ]
         },
         {
-          "description": "Enables the set_cursor_visible command without any pre-configured scope.",
+          "description": "core:window:allow-set-minimizable -> Enables the set_minimizable command without any pre-configured scope.",
           "type": "string",
-          "const": "core:window:allow-set-cursor-visible"
+          "enum": [
+            "core:window:allow-set-minimizable"
+          ]
         },
         {
-          "description": "Enables the set_decorations command without any pre-configured scope.",
+          "description": "core:window:allow-set-position -> Enables the set_position command without any pre-configured scope.",
           "type": "string",
-          "const": "core:window:allow-set-decorations"
+          "enum": [
+            "core:window:allow-set-position"
+          ]
         },
         {
-          "description": "Enables the set_effects command without any pre-configured scope.",
+          "description": "core:window:allow-set-progress-bar -> Enables the set_progress_bar command without any pre-configured scope.",
           "type": "string",
-          "const": "core:window:allow-set-effects"
+          "enum": [
+            "core:window:allow-set-progress-bar"
+          ]
         },
         {
-          "description": "Enables the set_enabled command without any pre-configured scope.",
+          "description": "core:window:allow-set-resizable -> Enables the set_resizable command without any pre-configured scope.",
           "type": "string",
-          "const": "core:window:allow-set-enabled"
+          "enum": [
+            "core:window:allow-set-resizable"
+          ]
         },
         {
-          "description": "Enables the set_focus command without any pre-configured scope.",
+          "description": "core:window:allow-set-shadow -> Enables the set_shadow command without any pre-configured scope.",
           "type": "string",
-          "const": "core:window:allow-set-focus"
+          "enum": [
+            "core:window:allow-set-shadow"
+          ]
         },
         {
-          "description": "Enables the set_fullscreen command without any pre-configured scope.",
+          "description": "core:window:allow-set-size -> Enables the set_size command without any pre-configured scope.",
           "type": "string",
-          "const": "core:window:allow-set-fullscreen"
+          "enum": [
+            "core:window:allow-set-size"
+          ]
         },
         {
-          "description": "Enables the set_icon command without any pre-configured scope.",
+          "description": "core:window:allow-set-size-constraints -> Enables the set_size_constraints command without any pre-configured scope.",
           "type": "string",
-          "const": "core:window:allow-set-icon"
+          "enum": [
+            "core:window:allow-set-size-constraints"
+          ]
         },
         {
-          "description": "Enables the set_ignore_cursor_events command without any pre-configured scope.",
+          "description": "core:window:allow-set-skip-taskbar -> Enables the set_skip_taskbar command without any pre-configured scope.",
           "type": "string",
-          "const": "core:window:allow-set-ignore-cursor-events"
+          "enum": [
+            "core:window:allow-set-skip-taskbar"
+          ]
         },
         {
-          "description": "Enables the set_max_size command without any pre-configured scope.",
+          "description": "core:window:allow-set-title -> Enables the set_title command without any pre-configured scope.",
           "type": "string",
-          "const": "core:window:allow-set-max-size"
+          "enum": [
+            "core:window:allow-set-title"
+          ]
         },
         {
-          "description": "Enables the set_maximizable command without any pre-configured scope.",
+          "description": "core:window:allow-set-title-bar-style -> Enables the set_title_bar_style command without any pre-configured scope.",
           "type": "string",
-          "const": "core:window:allow-set-maximizable"
+          "enum": [
+            "core:window:allow-set-title-bar-style"
+          ]
         },
         {
-          "description": "Enables the set_min_size command without any pre-configured scope.",
+          "description": "core:window:allow-set-visible-on-all-workspaces -> Enables the set_visible_on_all_workspaces command without any pre-configured scope.",
           "type": "string",
-          "const": "core:window:allow-set-min-size"
+          "enum": [
+            "core:window:allow-set-visible-on-all-workspaces"
+          ]
         },
         {
-          "description": "Enables the set_minimizable command without any pre-configured scope.",
+          "description": "core:window:allow-show -> Enables the show command without any pre-configured scope.",
           "type": "string",
-          "const": "core:window:allow-set-minimizable"
+          "enum": [
+            "core:window:allow-show"
+          ]
         },
         {
-          "description": "Enables the set_position command without any pre-configured scope.",
+          "description": "core:window:allow-start-dragging -> Enables the start_dragging command without any pre-configured scope.",
           "type": "string",
-          "const": "core:window:allow-set-position"
+          "enum": [
+            "core:window:allow-start-dragging"
+          ]
         },
         {
-          "description": "Enables the set_progress_bar command without any pre-configured scope.",
+          "description": "core:window:allow-start-resize-dragging -> Enables the start_resize_dragging command without any pre-configured scope.",
           "type": "string",
-          "const": "core:window:allow-set-progress-bar"
+          "enum": [
+            "core:window:allow-start-resize-dragging"
+          ]
         },
         {
-          "description": "Enables the set_resizable command without any pre-configured scope.",
+          "description": "core:window:allow-theme -> Enables the theme command without any pre-configured scope.",
           "type": "string",
-          "const": "core:window:allow-set-resizable"
+          "enum": [
+            "core:window:allow-theme"
+          ]
         },
         {
-          "description": "Enables the set_shadow command without any pre-configured scope.",
+          "description": "core:window:allow-title -> Enables the title command without any pre-configured scope.",
           "type": "string",
-          "const": "core:window:allow-set-shadow"
+          "enum": [
+            "core:window:allow-title"
+          ]
         },
         {
-          "description": "Enables the set_size command without any pre-configured scope.",
+          "description": "core:window:allow-toggle-maximize -> Enables the toggle_maximize command without any pre-configured scope.",
           "type": "string",
-          "const": "core:window:allow-set-size"
+          "enum": [
+            "core:window:allow-toggle-maximize"
+          ]
         },
         {
-          "description": "Enables the set_size_constraints command without any pre-configured scope.",
+          "description": "core:window:allow-unmaximize -> Enables the unmaximize command without any pre-configured scope.",
           "type": "string",
-          "const": "core:window:allow-set-size-constraints"
+          "enum": [
+            "core:window:allow-unmaximize"
+          ]
         },
         {
-          "description": "Enables the set_skip_taskbar command without any pre-configured scope.",
+          "description": "core:window:allow-unminimize -> Enables the unminimize command without any pre-configured scope.",
           "type": "string",
-          "const": "core:window:allow-set-skip-taskbar"
+          "enum": [
+            "core:window:allow-unminimize"
+          ]
         },
         {
-          "description": "Enables the set_theme command without any pre-configured scope.",
+          "description": "core:window:deny-available-monitors -> Denies the available_monitors command without any pre-configured scope.",
           "type": "string",
-          "const": "core:window:allow-set-theme"
+          "enum": [
+            "core:window:deny-available-monitors"
+          ]
         },
         {
-          "description": "Enables the set_title command without any pre-configured scope.",
+          "description": "core:window:deny-center -> Denies the center command without any pre-configured scope.",
           "type": "string",
-          "const": "core:window:allow-set-title"
+          "enum": [
+            "core:window:deny-center"
+          ]
         },
         {
-          "description": "Enables the set_title_bar_style command without any pre-configured scope.",
+          "description": "core:window:deny-close -> Denies the close command without any pre-configured scope.",
           "type": "string",
-          "const": "core:window:allow-set-title-bar-style"
+          "enum": [
+            "core:window:deny-close"
+          ]
         },
         {
-          "description": "Enables the set_visible_on_all_workspaces command without any pre-configured scope.",
+          "description": "core:window:deny-create -> Denies the create command without any pre-configured scope.",
           "type": "string",
-          "const": "core:window:allow-set-visible-on-all-workspaces"
+          "enum": [
+            "core:window:deny-create"
+          ]
         },
         {
-          "description": "Enables the show command without any pre-configured scope.",
+          "description": "core:window:deny-current-monitor -> Denies the current_monitor command without any pre-configured scope.",
           "type": "string",
-          "const": "core:window:allow-show"
+          "enum": [
+            "core:window:deny-current-monitor"
+          ]
         },
         {
-          "description": "Enables the start_dragging command without any pre-configured scope.",
+          "description": "core:window:deny-cursor-position -> Denies the cursor_position command without any pre-configured scope.",
           "type": "string",
-          "const": "core:window:allow-start-dragging"
+          "enum": [
+            "core:window:deny-cursor-position"
+          ]
         },
         {
-          "description": "Enables the start_resize_dragging command without any pre-configured scope.",
+          "description": "core:window:deny-destroy -> Denies the destroy command without any pre-configured scope.",
           "type": "string",
-          "const": "core:window:allow-start-resize-dragging"
+          "enum": [
+            "core:window:deny-destroy"
+          ]
         },
         {
-          "description": "Enables the theme command without any pre-configured scope.",
+          "description": "core:window:deny-get-all-windows -> Denies the get_all_windows command without any pre-configured scope.",
           "type": "string",
-          "const": "core:window:allow-theme"
+          "enum": [
+            "core:window:deny-get-all-windows"
+          ]
         },
         {
-          "description": "Enables the title command without any pre-configured scope.",
+          "description": "core:window:deny-hide -> Denies the hide command without any pre-configured scope.",
           "type": "string",
-          "const": "core:window:allow-title"
+          "enum": [
+            "core:window:deny-hide"
+          ]
         },
         {
-          "description": "Enables the toggle_maximize command without any pre-configured scope.",
+          "description": "core:window:deny-inner-position -> Denies the inner_position command without any pre-configured scope.",
           "type": "string",
-          "const": "core:window:allow-toggle-maximize"
+          "enum": [
+            "core:window:deny-inner-position"
+          ]
         },
         {
-          "description": "Enables the unmaximize command without any pre-configured scope.",
+          "description": "core:window:deny-inner-size -> Denies the inner_size command without any pre-configured scope.",
           "type": "string",
-          "const": "core:window:allow-unmaximize"
+          "enum": [
+            "core:window:deny-inner-size"
+          ]
         },
         {
-          "description": "Enables the unminimize command without any pre-configured scope.",
+          "description": "core:window:deny-internal-toggle-maximize -> Denies the internal_toggle_maximize command without any pre-configured scope.",
           "type": "string",
-          "const": "core:window:allow-unminimize"
+          "enum": [
+            "core:window:deny-internal-toggle-maximize"
+          ]
         },
         {
-          "description": "Denies the available_monitors command without any pre-configured scope.",
+          "description": "core:window:deny-is-closable -> Denies the is_closable command without any pre-configured scope.",
           "type": "string",
-          "const": "core:window:deny-available-monitors"
+          "enum": [
+            "core:window:deny-is-closable"
+          ]
         },
         {
-          "description": "Denies the center command without any pre-configured scope.",
+          "description": "core:window:deny-is-decorated -> Denies the is_decorated command without any pre-configured scope.",
           "type": "string",
-          "const": "core:window:deny-center"
+          "enum": [
+            "core:window:deny-is-decorated"
+          ]
         },
         {
-          "description": "Denies the close command without any pre-configured scope.",
+          "description": "core:window:deny-is-focused -> Denies the is_focused command without any pre-configured scope.",
           "type": "string",
-          "const": "core:window:deny-close"
+          "enum": [
+            "core:window:deny-is-focused"
+          ]
         },
         {
-          "description": "Denies the create command without any pre-configured scope.",
+          "description": "core:window:deny-is-fullscreen -> Denies the is_fullscreen command without any pre-configured scope.",
           "type": "string",
-          "const": "core:window:deny-create"
+          "enum": [
+            "core:window:deny-is-fullscreen"
+          ]
         },
         {
-          "description": "Denies the current_monitor command without any pre-configured scope.",
+          "description": "core:window:deny-is-maximizable -> Denies the is_maximizable command without any pre-configured scope.",
           "type": "string",
-          "const": "core:window:deny-current-monitor"
+          "enum": [
+            "core:window:deny-is-maximizable"
+          ]
         },
         {
-          "description": "Denies the cursor_position command without any pre-configured scope.",
+          "description": "core:window:deny-is-maximized -> Denies the is_maximized command without any pre-configured scope.",
           "type": "string",
-          "const": "core:window:deny-cursor-position"
+          "enum": [
+            "core:window:deny-is-maximized"
+          ]
         },
         {
-          "description": "Denies the destroy command without any pre-configured scope.",
+          "description": "core:window:deny-is-minimizable -> Denies the is_minimizable command without any pre-configured scope.",
           "type": "string",
-          "const": "core:window:deny-destroy"
+          "enum": [
+            "core:window:deny-is-minimizable"
+          ]
         },
         {
-          "description": "Denies the get_all_windows command without any pre-configured scope.",
+          "description": "core:window:deny-is-minimized -> Denies the is_minimized command without any pre-configured scope.",
           "type": "string",
-          "const": "core:window:deny-get-all-windows"
+          "enum": [
+            "core:window:deny-is-minimized"
+          ]
         },
         {
-          "description": "Denies the hide command without any pre-configured scope.",
+          "description": "core:window:deny-is-resizable -> Denies the is_resizable command without any pre-configured scope.",
           "type": "string",
-          "const": "core:window:deny-hide"
+          "enum": [
+            "core:window:deny-is-resizable"
+          ]
         },
         {
-          "description": "Denies the inner_position command without any pre-configured scope.",
+          "description": "core:window:deny-is-visible -> Denies the is_visible command without any pre-configured scope.",
           "type": "string",
-          "const": "core:window:deny-inner-position"
+          "enum": [
+            "core:window:deny-is-visible"
+          ]
         },
         {
-          "description": "Denies the inner_size command without any pre-configured scope.",
+          "description": "core:window:deny-maximize -> Denies the maximize command without any pre-configured scope.",
           "type": "string",
-          "const": "core:window:deny-inner-size"
+          "enum": [
+            "core:window:deny-maximize"
+          ]
         },
         {
-          "description": "Denies the internal_toggle_maximize command without any pre-configured scope.",
+          "description": "core:window:deny-minimize -> Denies the minimize command without any pre-configured scope.",
           "type": "string",
-          "const": "core:window:deny-internal-toggle-maximize"
+          "enum": [
+            "core:window:deny-minimize"
+          ]
         },
         {
-          "description": "Denies the is_closable command without any pre-configured scope.",
+          "description": "core:window:deny-monitor-from-point -> Denies the monitor_from_point command without any pre-configured scope.",
           "type": "string",
-          "const": "core:window:deny-is-closable"
+          "enum": [
+            "core:window:deny-monitor-from-point"
+          ]
         },
         {
-          "description": "Denies the is_decorated command without any pre-configured scope.",
+          "description": "core:window:deny-outer-position -> Denies the outer_position command without any pre-configured scope.",
           "type": "string",
-          "const": "core:window:deny-is-decorated"
+          "enum": [
+            "core:window:deny-outer-position"
+          ]
         },
         {
-          "description": "Denies the is_enabled command without any pre-configured scope.",
+          "description": "core:window:deny-outer-size -> Denies the outer_size command without any pre-configured scope.",
           "type": "string",
-          "const": "core:window:deny-is-enabled"
+          "enum": [
+            "core:window:deny-outer-size"
+          ]
         },
         {
-          "description": "Denies the is_focused command without any pre-configured scope.",
+          "description": "core:window:deny-primary-monitor -> Denies the primary_monitor command without any pre-configured scope.",
           "type": "string",
-          "const": "core:window:deny-is-focused"
+          "enum": [
+            "core:window:deny-primary-monitor"
+          ]
         },
         {
-          "description": "Denies the is_fullscreen command without any pre-configured scope.",
+          "description": "core:window:deny-request-user-attention -> Denies the request_user_attention command without any pre-configured scope.",
           "type": "string",
-          "const": "core:window:deny-is-fullscreen"
+          "enum": [
+            "core:window:deny-request-user-attention"
+          ]
         },
         {
-          "description": "Denies the is_maximizable command without any pre-configured scope.",
+          "description": "core:window:deny-scale-factor -> Denies the scale_factor command without any pre-configured scope.",
           "type": "string",
-          "const": "core:window:deny-is-maximizable"
+          "enum": [
+            "core:window:deny-scale-factor"
+          ]
         },
         {
-          "description": "Denies the is_maximized command without any pre-configured scope.",
+          "description": "core:window:deny-set-always-on-bottom -> Denies the set_always_on_bottom command without any pre-configured scope.",
           "type": "string",
-          "const": "core:window:deny-is-maximized"
+          "enum": [
+            "core:window:deny-set-always-on-bottom"
+          ]
         },
         {
-          "description": "Denies the is_minimizable command without any pre-configured scope.",
+          "description": "core:window:deny-set-always-on-top -> Denies the set_always_on_top command without any pre-configured scope.",
           "type": "string",
-          "const": "core:window:deny-is-minimizable"
+          "enum": [
+            "core:window:deny-set-always-on-top"
+          ]
         },
         {
-          "description": "Denies the is_minimized command without any pre-configured scope.",
+          "description": "core:window:deny-set-closable -> Denies the set_closable command without any pre-configured scope.",
           "type": "string",
-          "const": "core:window:deny-is-minimized"
+          "enum": [
+            "core:window:deny-set-closable"
+          ]
         },
         {
-          "description": "Denies the is_resizable command without any pre-configured scope.",
+          "description": "core:window:deny-set-content-protected -> Denies the set_content_protected command without any pre-configured scope.",
           "type": "string",
-          "const": "core:window:deny-is-resizable"
+          "enum": [
+            "core:window:deny-set-content-protected"
+          ]
         },
         {
-          "description": "Denies the is_visible command without any pre-configured scope.",
+          "description": "core:window:deny-set-cursor-grab -> Denies the set_cursor_grab command without any pre-configured scope.",
           "type": "string",
-          "const": "core:window:deny-is-visible"
+          "enum": [
+            "core:window:deny-set-cursor-grab"
+          ]
         },
         {
-          "description": "Denies the maximize command without any pre-configured scope.",
+          "description": "core:window:deny-set-cursor-icon -> Denies the set_cursor_icon command without any pre-configured scope.",
           "type": "string",
-          "const": "core:window:deny-maximize"
+          "enum": [
+            "core:window:deny-set-cursor-icon"
+          ]
         },
         {
-          "description": "Denies the minimize command without any pre-configured scope.",
+          "description": "core:window:deny-set-cursor-position -> Denies the set_cursor_position command without any pre-configured scope.",
           "type": "string",
-          "const": "core:window:deny-minimize"
+          "enum": [
+            "core:window:deny-set-cursor-position"
+          ]
         },
         {
-          "description": "Denies the monitor_from_point command without any pre-configured scope.",
+          "description": "core:window:deny-set-cursor-visible -> Denies the set_cursor_visible command without any pre-configured scope.",
           "type": "string",
-          "const": "core:window:deny-monitor-from-point"
+          "enum": [
+            "core:window:deny-set-cursor-visible"
+          ]
         },
         {
-          "description": "Denies the outer_position command without any pre-configured scope.",
+          "description": "core:window:deny-set-decorations -> Denies the set_decorations command without any pre-configured scope.",
           "type": "string",
-          "const": "core:window:deny-outer-position"
+          "enum": [
+            "core:window:deny-set-decorations"
+          ]
         },
         {
-          "description": "Denies the outer_size command without any pre-configured scope.",
+          "description": "core:window:deny-set-effects -> Denies the set_effects command without any pre-configured scope.",
           "type": "string",
-          "const": "core:window:deny-outer-size"
+          "enum": [
+            "core:window:deny-set-effects"
+          ]
         },
         {
-          "description": "Denies the primary_monitor command without any pre-configured scope.",
+          "description": "core:window:deny-set-focus -> Denies the set_focus command without any pre-configured scope.",
           "type": "string",
-          "const": "core:window:deny-primary-monitor"
+          "enum": [
+            "core:window:deny-set-focus"
+          ]
         },
         {
-          "description": "Denies the request_user_attention command without any pre-configured scope.",
+          "description": "core:window:deny-set-fullscreen -> Denies the set_fullscreen command without any pre-configured scope.",
           "type": "string",
-          "const": "core:window:deny-request-user-attention"
+          "enum": [
+            "core:window:deny-set-fullscreen"
+          ]
         },
         {
-          "description": "Denies the scale_factor command without any pre-configured scope.",
+          "description": "core:window:deny-set-icon -> Denies the set_icon command without any pre-configured scope.",
           "type": "string",
-          "const": "core:window:deny-scale-factor"
+          "enum": [
+            "core:window:deny-set-icon"
+          ]
         },
         {
-          "description": "Denies the set_always_on_bottom command without any pre-configured scope.",
+          "description": "core:window:deny-set-ignore-cursor-events -> Denies the set_ignore_cursor_events command without any pre-configured scope.",
           "type": "string",
-          "const": "core:window:deny-set-always-on-bottom"
+          "enum": [
+            "core:window:deny-set-ignore-cursor-events"
+          ]
         },
         {
-          "description": "Denies the set_always_on_top command without any pre-configured scope.",
+          "description": "core:window:deny-set-max-size -> Denies the set_max_size command without any pre-configured scope.",
           "type": "string",
-          "const": "core:window:deny-set-always-on-top"
+          "enum": [
+            "core:window:deny-set-max-size"
+          ]
         },
         {
-          "description": "Denies the set_closable command without any pre-configured scope.",
+          "description": "core:window:deny-set-maximizable -> Denies the set_maximizable command without any pre-configured scope.",
           "type": "string",
-          "const": "core:window:deny-set-closable"
+          "enum": [
+            "core:window:deny-set-maximizable"
+          ]
         },
         {
-          "description": "Denies the set_content_protected command without any pre-configured scope.",
+          "description": "core:window:deny-set-min-size -> Denies the set_min_size command without any pre-configured scope.",
           "type": "string",
-          "const": "core:window:deny-set-content-protected"
+          "enum": [
+            "core:window:deny-set-min-size"
+          ]
         },
         {
-          "description": "Denies the set_cursor_grab command without any pre-configured scope.",
+          "description": "core:window:deny-set-minimizable -> Denies the set_minimizable command without any pre-configured scope.",
           "type": "string",
-          "const": "core:window:deny-set-cursor-grab"
+          "enum": [
+            "core:window:deny-set-minimizable"
+          ]
         },
         {
-          "description": "Denies the set_cursor_icon command without any pre-configured scope.",
+          "description": "core:window:deny-set-position -> Denies the set_position command without any pre-configured scope.",
           "type": "string",
-          "const": "core:window:deny-set-cursor-icon"
+          "enum": [
+            "core:window:deny-set-position"
+          ]
         },
         {
-          "description": "Denies the set_cursor_position command without any pre-configured scope.",
+          "description": "core:window:deny-set-progress-bar -> Denies the set_progress_bar command without any pre-configured scope.",
           "type": "string",
-          "const": "core:window:deny-set-cursor-position"
+          "enum": [
+            "core:window:deny-set-progress-bar"
+          ]
         },
         {
-          "description": "Denies the set_cursor_visible command without any pre-configured scope.",
+          "description": "core:window:deny-set-resizable -> Denies the set_resizable command without any pre-configured scope.",
           "type": "string",
-          "const": "core:window:deny-set-cursor-visible"
+          "enum": [
+            "core:window:deny-set-resizable"
+          ]
         },
         {
-          "description": "Denies the set_decorations command without any pre-configured scope.",
+          "description": "core:window:deny-set-shadow -> Denies the set_shadow command without any pre-configured scope.",
           "type": "string",
-          "const": "core:window:deny-set-decorations"
+          "enum": [
+            "core:window:deny-set-shadow"
+          ]
         },
         {
-          "description": "Denies the set_effects command without any pre-configured scope.",
+          "description": "core:window:deny-set-size -> Denies the set_size command without any pre-configured scope.",
           "type": "string",
-          "const": "core:window:deny-set-effects"
+          "enum": [
+            "core:window:deny-set-size"
+          ]
         },
         {
-          "description": "Denies the set_enabled command without any pre-configured scope.",
+          "description": "core:window:deny-set-size-constraints -> Denies the set_size_constraints command without any pre-configured scope.",
           "type": "string",
-          "const": "core:window:deny-set-enabled"
+          "enum": [
+            "core:window:deny-set-size-constraints"
+          ]
         },
         {
-          "description": "Denies the set_focus command without any pre-configured scope.",
+          "description": "core:window:deny-set-skip-taskbar -> Denies the set_skip_taskbar command without any pre-configured scope.",
           "type": "string",
-          "const": "core:window:deny-set-focus"
+          "enum": [
+            "core:window:deny-set-skip-taskbar"
+          ]
         },
         {
-          "description": "Denies the set_fullscreen command without any pre-configured scope.",
+          "description": "core:window:deny-set-title -> Denies the set_title command without any pre-configured scope.",
           "type": "string",
-          "const": "core:window:deny-set-fullscreen"
+          "enum": [
+            "core:window:deny-set-title"
+          ]
         },
         {
-          "description": "Denies the set_icon command without any pre-configured scope.",
+          "description": "core:window:deny-set-title-bar-style -> Denies the set_title_bar_style command without any pre-configured scope.",
           "type": "string",
-          "const": "core:window:deny-set-icon"
+          "enum": [
+            "core:window:deny-set-title-bar-style"
+          ]
         },
         {
-          "description": "Denies the set_ignore_cursor_events command without any pre-configured scope.",
+          "description": "core:window:deny-set-visible-on-all-workspaces -> Denies the set_visible_on_all_workspaces command without any pre-configured scope.",
           "type": "string",
-          "const": "core:window:deny-set-ignore-cursor-events"
+          "enum": [
+            "core:window:deny-set-visible-on-all-workspaces"
+          ]
         },
         {
-          "description": "Denies the set_max_size command without any pre-configured scope.",
+          "description": "core:window:deny-show -> Denies the show command without any pre-configured scope.",
           "type": "string",
-          "const": "core:window:deny-set-max-size"
+          "enum": [
+            "core:window:deny-show"
+          ]
         },
         {
-          "description": "Denies the set_maximizable command without any pre-configured scope.",
+          "description": "core:window:deny-start-dragging -> Denies the start_dragging command without any pre-configured scope.",
           "type": "string",
-          "const": "core:window:deny-set-maximizable"
+          "enum": [
+            "core:window:deny-start-dragging"
+          ]
         },
         {
-          "description": "Denies the set_min_size command without any pre-configured scope.",
+          "description": "core:window:deny-start-resize-dragging -> Denies the start_resize_dragging command without any pre-configured scope.",
           "type": "string",
-          "const": "core:window:deny-set-min-size"
+          "enum": [
+            "core:window:deny-start-resize-dragging"
+          ]
         },
         {
-          "description": "Denies the set_minimizable command without any pre-configured scope.",
+          "description": "core:window:deny-theme -> Denies the theme command without any pre-configured scope.",
           "type": "string",
-          "const": "core:window:deny-set-minimizable"
+          "enum": [
+            "core:window:deny-theme"
+          ]
         },
         {
-          "description": "Denies the set_position command without any pre-configured scope.",
+          "description": "core:window:deny-title -> Denies the title command without any pre-configured scope.",
           "type": "string",
-          "const": "core:window:deny-set-position"
+          "enum": [
+            "core:window:deny-title"
+          ]
         },
         {
-          "description": "Denies the set_progress_bar command without any pre-configured scope.",
+          "description": "core:window:deny-toggle-maximize -> Denies the toggle_maximize command without any pre-configured scope.",
           "type": "string",
-          "const": "core:window:deny-set-progress-bar"
+          "enum": [
+            "core:window:deny-toggle-maximize"
+          ]
         },
         {
-          "description": "Denies the set_resizable command without any pre-configured scope.",
+          "description": "core:window:deny-unmaximize -> Denies the unmaximize command without any pre-configured scope.",
           "type": "string",
-          "const": "core:window:deny-set-resizable"
+          "enum": [
+            "core:window:deny-unmaximize"
+          ]
         },
         {
-          "description": "Denies the set_shadow command without any pre-configured scope.",
+          "description": "core:window:deny-unminimize -> Denies the unminimize command without any pre-configured scope.",
           "type": "string",
-          "const": "core:window:deny-set-shadow"
+          "enum": [
+            "core:window:deny-unminimize"
+          ]
         },
         {
-          "description": "Denies the set_size command without any pre-configured scope.",
+          "description": "dialog:default -> This permission set configures the types of dialogs\navailable from the dialog plugin.\n\n#### Granted Permissions\n\nAll dialog types are enabled.\n\n\n",
           "type": "string",
-          "const": "core:window:deny-set-size"
+          "enum": [
+            "dialog:default"
+          ]
         },
         {
-          "description": "Denies the set_size_constraints command without any pre-configured scope.",
+          "description": "dialog:allow-ask -> Enables the ask command without any pre-configured scope.",
           "type": "string",
-          "const": "core:window:deny-set-size-constraints"
+          "enum": [
+            "dialog:allow-ask"
+          ]
         },
         {
-          "description": "Denies the set_skip_taskbar command without any pre-configured scope.",
+          "description": "dialog:allow-confirm -> Enables the confirm command without any pre-configured scope.",
           "type": "string",
-          "const": "core:window:deny-set-skip-taskbar"
+          "enum": [
+            "dialog:allow-confirm"
+          ]
         },
         {
-          "description": "Denies the set_theme command without any pre-configured scope.",
+          "description": "dialog:allow-message -> Enables the message command without any pre-configured scope.",
           "type": "string",
-          "const": "core:window:deny-set-theme"
+          "enum": [
+            "dialog:allow-message"
+          ]
         },
         {
-          "description": "Denies the set_title command without any pre-configured scope.",
+          "description": "dialog:allow-open -> Enables the open command without any pre-configured scope.",
           "type": "string",
-          "const": "core:window:deny-set-title"
+          "enum": [
+            "dialog:allow-open"
+          ]
         },
         {
-          "description": "Denies the set_title_bar_style command without any pre-configured scope.",
+          "description": "dialog:allow-save -> Enables the save command without any pre-configured scope.",
           "type": "string",
-          "const": "core:window:deny-set-title-bar-style"
+          "enum": [
+            "dialog:allow-save"
+          ]
         },
         {
-          "description": "Denies the set_visible_on_all_workspaces command without any pre-configured scope.",
+          "description": "dialog:deny-ask -> Denies the ask command without any pre-configured scope.",
           "type": "string",
-          "const": "core:window:deny-set-visible-on-all-workspaces"
+          "enum": [
+            "dialog:deny-ask"
+          ]
         },
         {
-          "description": "Denies the show command without any pre-configured scope.",
+          "description": "dialog:deny-confirm -> Denies the confirm command without any pre-configured scope.",
           "type": "string",
-          "const": "core:window:deny-show"
+          "enum": [
+            "dialog:deny-confirm"
+          ]
         },
         {
-          "description": "Denies the start_dragging command without any pre-configured scope.",
+          "description": "dialog:deny-message -> Denies the message command without any pre-configured scope.",
           "type": "string",
-          "const": "core:window:deny-start-dragging"
+          "enum": [
+            "dialog:deny-message"
+          ]
         },
         {
-          "description": "Denies the start_resize_dragging command without any pre-configured scope.",
+          "description": "dialog:deny-open -> Denies the open command without any pre-configured scope.",
           "type": "string",
-          "const": "core:window:deny-start-resize-dragging"
+          "enum": [
+            "dialog:deny-open"
+          ]
         },
         {
-          "description": "Denies the theme command without any pre-configured scope.",
+          "description": "dialog:deny-save -> Denies the save command without any pre-configured scope.",
           "type": "string",
-          "const": "core:window:deny-theme"
+          "enum": [
+            "dialog:deny-save"
+          ]
         },
         {
-          "description": "Denies the title command without any pre-configured scope.",
+          "description": "log:default -> Allows the log command",
           "type": "string",
-          "const": "core:window:deny-title"
+          "enum": [
+            "log:default"
+          ]
         },
         {
-          "description": "Denies the toggle_maximize command without any pre-configured scope.",
+          "description": "log:allow-log -> Enables the log command without any pre-configured scope.",
           "type": "string",
-          "const": "core:window:deny-toggle-maximize"
+          "enum": [
+            "log:allow-log"
+          ]
         },
         {
-          "description": "Denies the unmaximize command without any pre-configured scope.",
+          "description": "log:deny-log -> Denies the log command without any pre-configured scope.",
           "type": "string",
-          "const": "core:window:deny-unmaximize"
+          "enum": [
+            "log:deny-log"
+          ]
         },
         {
-          "description": "Denies the unminimize command without any pre-configured scope.",
+          "description": "notification:default -> This permission set configures which\nnotification features are by default exposed.\n\n#### Granted Permissions\n\nIt allows all notification related features.\n\n",
           "type": "string",
-          "const": "core:window:deny-unminimize"
+          "enum": [
+            "notification:default"
+          ]
         },
         {
-          "description": "This permission set configures the types of dialogs\navailable from the dialog plugin.\n\n#### Granted Permissions\n\nAll dialog types are enabled.\n\n\n",
+          "description": "notification:allow-batch -> Enables the batch command without any pre-configured scope.",
           "type": "string",
-          "const": "dialog:default"
+          "enum": [
+            "notification:allow-batch"
+          ]
         },
         {
-          "description": "Enables the ask command without any pre-configured scope.",
+          "description": "notification:allow-cancel -> Enables the cancel command without any pre-configured scope.",
           "type": "string",
-          "const": "dialog:allow-ask"
+          "enum": [
+            "notification:allow-cancel"
+          ]
         },
         {
-          "description": "Enables the confirm command without any pre-configured scope.",
+          "description": "notification:allow-check-permissions -> Enables the check_permissions command without any pre-configured scope.",
           "type": "string",
-          "const": "dialog:allow-confirm"
+          "enum": [
+            "notification:allow-check-permissions"
+          ]
         },
         {
-          "description": "Enables the message command without any pre-configured scope.",
+          "description": "notification:allow-create-channel -> Enables the create_channel command without any pre-configured scope.",
           "type": "string",
-          "const": "dialog:allow-message"
+          "enum": [
+            "notification:allow-create-channel"
+          ]
         },
         {
-          "description": "Enables the open command without any pre-configured scope.",
+          "description": "notification:allow-delete-channel -> Enables the delete_channel command without any pre-configured scope.",
           "type": "string",
-          "const": "dialog:allow-open"
+          "enum": [
+            "notification:allow-delete-channel"
+          ]
         },
         {
-          "description": "Enables the save command without any pre-configured scope.",
+          "description": "notification:allow-get-active -> Enables the get_active command without any pre-configured scope.",
           "type": "string",
-          "const": "dialog:allow-save"
+          "enum": [
+            "notification:allow-get-active"
+          ]
         },
         {
-          "description": "Denies the ask command without any pre-configured scope.",
+          "description": "notification:allow-get-pending -> Enables the get_pending command without any pre-configured scope.",
           "type": "string",
-          "const": "dialog:deny-ask"
+          "enum": [
+            "notification:allow-get-pending"
+          ]
         },
         {
-          "description": "Denies the confirm command without any pre-configured scope.",
+          "description": "notification:allow-is-permission-granted -> Enables the is_permission_granted command without any pre-configured scope.",
           "type": "string",
-          "const": "dialog:deny-confirm"
+          "enum": [
+            "notification:allow-is-permission-granted"
+          ]
         },
         {
-          "description": "Denies the message command without any pre-configured scope.",
+          "description": "notification:allow-list-channels -> Enables the list_channels command without any pre-configured scope.",
           "type": "string",
-          "const": "dialog:deny-message"
+          "enum": [
+            "notification:allow-list-channels"
+          ]
         },
         {
-          "description": "Denies the open command without any pre-configured scope.",
+          "description": "notification:allow-notify -> Enables the notify command without any pre-configured scope.",
           "type": "string",
-          "const": "dialog:deny-open"
+          "enum": [
+            "notification:allow-notify"
+          ]
         },
         {
-          "description": "Denies the save command without any pre-configured scope.",
+          "description": "notification:allow-permission-state -> Enables the permission_state command without any pre-configured scope.",
           "type": "string",
-          "const": "dialog:deny-save"
+          "enum": [
+            "notification:allow-permission-state"
+          ]
         },
         {
-          "description": "Allows the log command",
+          "description": "notification:allow-register-action-types -> Enables the register_action_types command without any pre-configured scope.",
           "type": "string",
-          "const": "log:default"
+          "enum": [
+            "notification:allow-register-action-types"
+          ]
         },
         {
-          "description": "Enables the log command without any pre-configured scope.",
+          "description": "notification:allow-register-listener -> Enables the register_listener command without any pre-configured scope.",
           "type": "string",
-          "const": "log:allow-log"
+          "enum": [
+            "notification:allow-register-listener"
+          ]
         },
         {
-          "description": "Denies the log command without any pre-configured scope.",
+          "description": "notification:allow-remove-active -> Enables the remove_active command without any pre-configured scope.",
           "type": "string",
-          "const": "log:deny-log"
+          "enum": [
+            "notification:allow-remove-active"
+          ]
         },
         {
-          "description": "This permission set configures which\nnotification features are by default exposed.\n\n#### Granted Permissions\n\nIt allows all notification related features.\n\n",
+          "description": "notification:allow-request-permission -> Enables the request_permission command without any pre-configured scope.",
           "type": "string",
-          "const": "notification:default"
+          "enum": [
+            "notification:allow-request-permission"
+          ]
         },
         {
-          "description": "Enables the batch command without any pre-configured scope.",
+          "description": "notification:allow-show -> Enables the show command without any pre-configured scope.",
           "type": "string",
-          "const": "notification:allow-batch"
+          "enum": [
+            "notification:allow-show"
+          ]
         },
         {
-          "description": "Enables the cancel command without any pre-configured scope.",
+          "description": "notification:deny-batch -> Denies the batch command without any pre-configured scope.",
           "type": "string",
-          "const": "notification:allow-cancel"
+          "enum": [
+            "notification:deny-batch"
+          ]
         },
         {
-          "description": "Enables the check_permissions command without any pre-configured scope.",
+          "description": "notification:deny-cancel -> Denies the cancel command without any pre-configured scope.",
           "type": "string",
-          "const": "notification:allow-check-permissions"
+          "enum": [
+            "notification:deny-cancel"
+          ]
         },
         {
-          "description": "Enables the create_channel command without any pre-configured scope.",
+          "description": "notification:deny-check-permissions -> Denies the check_permissions command without any pre-configured scope.",
           "type": "string",
-          "const": "notification:allow-create-channel"
+          "enum": [
+            "notification:deny-check-permissions"
+          ]
         },
         {
-          "description": "Enables the delete_channel command without any pre-configured scope.",
+          "description": "notification:deny-create-channel -> Denies the create_channel command without any pre-configured scope.",
           "type": "string",
-          "const": "notification:allow-delete-channel"
+          "enum": [
+            "notification:deny-create-channel"
+          ]
         },
         {
-          "description": "Enables the get_active command without any pre-configured scope.",
+          "description": "notification:deny-delete-channel -> Denies the delete_channel command without any pre-configured scope.",
           "type": "string",
-          "const": "notification:allow-get-active"
+          "enum": [
+            "notification:deny-delete-channel"
+          ]
         },
         {
-          "description": "Enables the get_pending command without any pre-configured scope.",
+          "description": "notification:deny-get-active -> Denies the get_active command without any pre-configured scope.",
           "type": "string",
-          "const": "notification:allow-get-pending"
+          "enum": [
+            "notification:deny-get-active"
+          ]
         },
         {
-          "description": "Enables the is_permission_granted command without any pre-configured scope.",
+          "description": "notification:deny-get-pending -> Denies the get_pending command without any pre-configured scope.",
           "type": "string",
-          "const": "notification:allow-is-permission-granted"
+          "enum": [
+            "notification:deny-get-pending"
+          ]
         },
         {
-          "description": "Enables the list_channels command without any pre-configured scope.",
+          "description": "notification:deny-is-permission-granted -> Denies the is_permission_granted command without any pre-configured scope.",
           "type": "string",
-          "const": "notification:allow-list-channels"
+          "enum": [
+            "notification:deny-is-permission-granted"
+          ]
         },
         {
-          "description": "Enables the notify command without any pre-configured scope.",
+          "description": "notification:deny-list-channels -> Denies the list_channels command without any pre-configured scope.",
           "type": "string",
-          "const": "notification:allow-notify"
+          "enum": [
+            "notification:deny-list-channels"
+          ]
         },
         {
-          "description": "Enables the permission_state command without any pre-configured scope.",
+          "description": "notification:deny-notify -> Denies the notify command without any pre-configured scope.",
           "type": "string",
-          "const": "notification:allow-permission-state"
+          "enum": [
+            "notification:deny-notify"
+          ]
         },
         {
-          "description": "Enables the register_action_types command without any pre-configured scope.",
+          "description": "notification:deny-permission-state -> Denies the permission_state command without any pre-configured scope.",
           "type": "string",
-          "const": "notification:allow-register-action-types"
+          "enum": [
+            "notification:deny-permission-state"
+          ]
         },
         {
-          "description": "Enables the register_listener command without any pre-configured scope.",
+          "description": "notification:deny-register-action-types -> Denies the register_action_types command without any pre-configured scope.",
           "type": "string",
-          "const": "notification:allow-register-listener"
+          "enum": [
+            "notification:deny-register-action-types"
+          ]
         },
         {
-          "description": "Enables the remove_active command without any pre-configured scope.",
+          "description": "notification:deny-register-listener -> Denies the register_listener command without any pre-configured scope.",
           "type": "string",
-          "const": "notification:allow-remove-active"
+          "enum": [
+            "notification:deny-register-listener"
+          ]
         },
         {
-          "description": "Enables the request_permission command without any pre-configured scope.",
+          "description": "notification:deny-remove-active -> Denies the remove_active command without any pre-configured scope.",
           "type": "string",
-          "const": "notification:allow-request-permission"
+          "enum": [
+            "notification:deny-remove-active"
+          ]
         },
         {
-          "description": "Enables the show command without any pre-configured scope.",
+          "description": "notification:deny-request-permission -> Denies the request_permission command without any pre-configured scope.",
           "type": "string",
-          "const": "notification:allow-show"
+          "enum": [
+            "notification:deny-request-permission"
+          ]
         },
         {
-          "description": "Denies the batch command without any pre-configured scope.",
+          "description": "notification:deny-show -> Denies the show command without any pre-configured scope.",
           "type": "string",
-          "const": "notification:deny-batch"
+          "enum": [
+            "notification:deny-show"
+          ]
         },
         {
-          "description": "Denies the cancel command without any pre-configured scope.",
+          "description": "os:default -> This permission set configures which\noperating system information are available\nto gather from the frontend.\n\n#### Granted Permissions\n\nAll information except the host name are available.\n\n",
           "type": "string",
-          "const": "notification:deny-cancel"
+          "enum": [
+            "os:default"
+          ]
         },
         {
-          "description": "Denies the check_permissions command without any pre-configured scope.",
+          "description": "os:allow-arch -> Enables the arch command without any pre-configured scope.",
           "type": "string",
-          "const": "notification:deny-check-permissions"
+          "enum": [
+            "os:allow-arch"
+          ]
         },
         {
-          "description": "Denies the create_channel command without any pre-configured scope.",
+          "description": "os:allow-exe-extension -> Enables the exe_extension command without any pre-configured scope.",
           "type": "string",
-          "const": "notification:deny-create-channel"
+          "enum": [
+            "os:allow-exe-extension"
+          ]
         },
         {
-          "description": "Denies the delete_channel command without any pre-configured scope.",
+          "description": "os:allow-family -> Enables the family command without any pre-configured scope.",
           "type": "string",
-          "const": "notification:deny-delete-channel"
+          "enum": [
+            "os:allow-family"
+          ]
         },
         {
-          "description": "Denies the get_active command without any pre-configured scope.",
+          "description": "os:allow-hostname -> Enables the hostname command without any pre-configured scope.",
           "type": "string",
-          "const": "notification:deny-get-active"
+          "enum": [
+            "os:allow-hostname"
+          ]
         },
         {
-          "description": "Denies the get_pending command without any pre-configured scope.",
+          "description": "os:allow-locale -> Enables the locale command without any pre-configured scope.",
           "type": "string",
-          "const": "notification:deny-get-pending"
+          "enum": [
+            "os:allow-locale"
+          ]
         },
         {
-          "description": "Denies the is_permission_granted command without any pre-configured scope.",
+          "description": "os:allow-os-type -> Enables the os_type command without any pre-configured scope.",
           "type": "string",
-          "const": "notification:deny-is-permission-granted"
+          "enum": [
+            "os:allow-os-type"
+          ]
         },
         {
-          "description": "Denies the list_channels command without any pre-configured scope.",
+          "description": "os:allow-platform -> Enables the platform command without any pre-configured scope.",
           "type": "string",
-          "const": "notification:deny-list-channels"
+          "enum": [
+            "os:allow-platform"
+          ]
         },
         {
-          "description": "Denies the notify command without any pre-configured scope.",
+          "description": "os:allow-version -> Enables the version command without any pre-configured scope.",
           "type": "string",
-          "const": "notification:deny-notify"
+          "enum": [
+            "os:allow-version"
+          ]
         },
         {
-          "description": "Denies the permission_state command without any pre-configured scope.",
+          "description": "os:deny-arch -> Denies the arch command without any pre-configured scope.",
           "type": "string",
-          "const": "notification:deny-permission-state"
+          "enum": [
+            "os:deny-arch"
+          ]
         },
         {
-          "description": "Denies the register_action_types command without any pre-configured scope.",
+          "description": "os:deny-exe-extension -> Denies the exe_extension command without any pre-configured scope.",
           "type": "string",
-          "const": "notification:deny-register-action-types"
+          "enum": [
+            "os:deny-exe-extension"
+          ]
         },
         {
-          "description": "Denies the register_listener command without any pre-configured scope.",
+          "description": "os:deny-family -> Denies the family command without any pre-configured scope.",
           "type": "string",
-          "const": "notification:deny-register-listener"
+          "enum": [
+            "os:deny-family"
+          ]
         },
         {
-          "description": "Denies the remove_active command without any pre-configured scope.",
+          "description": "os:deny-hostname -> Denies the hostname command without any pre-configured scope.",
           "type": "string",
-          "const": "notification:deny-remove-active"
+          "enum": [
+            "os:deny-hostname"
+          ]
         },
         {
-          "description": "Denies the request_permission command without any pre-configured scope.",
+          "description": "os:deny-locale -> Denies the locale command without any pre-configured scope.",
           "type": "string",
-          "const": "notification:deny-request-permission"
+          "enum": [
+            "os:deny-locale"
+          ]
         },
         {
-          "description": "Denies the show command without any pre-configured scope.",
+          "description": "os:deny-os-type -> Denies the os_type command without any pre-configured scope.",
           "type": "string",
-          "const": "notification:deny-show"
+          "enum": [
+            "os:deny-os-type"
+          ]
         },
         {
-          "description": "This permission set configures which\noperating system information are available\nto gather from the frontend.\n\n#### Granted Permissions\n\nAll information except the host name are available.\n\n",
+          "description": "os:deny-platform -> Denies the platform command without any pre-configured scope.",
           "type": "string",
-          "const": "os:default"
+          "enum": [
+            "os:deny-platform"
+          ]
         },
         {
-          "description": "Enables the arch command without any pre-configured scope.",
+          "description": "os:deny-version -> Denies the version command without any pre-configured scope.",
           "type": "string",
-          "const": "os:allow-arch"
+          "enum": [
+            "os:deny-version"
+          ]
         },
         {
-          "description": "Enables the exe_extension command without any pre-configured scope.",
+          "description": "shell:default -> This permission set configures which\nshell functionality is exposed by default.\n\n#### Granted Permissions\n\nIt allows to use the `open` functionality without any specific\nscope pre-configured. It will allow opening `http(s)://`,\n`tel:` and `mailto:` links.\n",
           "type": "string",
-          "const": "os:allow-exe-extension"
+          "enum": [
+            "shell:default"
+          ]
         },
         {
-          "description": "Enables the family command without any pre-configured scope.",
+          "description": "shell:allow-execute -> Enables the execute command without any pre-configured scope.",
           "type": "string",
-          "const": "os:allow-family"
+          "enum": [
+            "shell:allow-execute"
+          ]
         },
         {
-          "description": "Enables the hostname command without any pre-configured scope.",
+          "description": "shell:allow-kill -> Enables the kill command without any pre-configured scope.",
           "type": "string",
-          "const": "os:allow-hostname"
+          "enum": [
+            "shell:allow-kill"
+          ]
         },
         {
-          "description": "Enables the locale command without any pre-configured scope.",
+          "description": "shell:allow-open -> Enables the open command without any pre-configured scope.",
           "type": "string",
-          "const": "os:allow-locale"
+          "enum": [
+            "shell:allow-open"
+          ]
         },
         {
-          "description": "Enables the os_type command without any pre-configured scope.",
+          "description": "shell:allow-spawn -> Enables the spawn command without any pre-configured scope.",
           "type": "string",
-          "const": "os:allow-os-type"
+          "enum": [
+            "shell:allow-spawn"
+          ]
         },
         {
-          "description": "Enables the platform command without any pre-configured scope.",
+          "description": "shell:allow-stdin-write -> Enables the stdin_write command without any pre-configured scope.",
           "type": "string",
-          "const": "os:allow-platform"
+          "enum": [
+            "shell:allow-stdin-write"
+          ]
         },
         {
-          "description": "Enables the version command without any pre-configured scope.",
+          "description": "shell:deny-execute -> Denies the execute command without any pre-configured scope.",
           "type": "string",
-          "const": "os:allow-version"
+          "enum": [
+            "shell:deny-execute"
+          ]
         },
         {
-          "description": "Denies the arch command without any pre-configured scope.",
+          "description": "shell:deny-kill -> Denies the kill command without any pre-configured scope.",
           "type": "string",
-          "const": "os:deny-arch"
+          "enum": [
+            "shell:deny-kill"
+          ]
         },
         {
-          "description": "Denies the exe_extension command without any pre-configured scope.",
+          "description": "shell:deny-open -> Denies the open command without any pre-configured scope.",
           "type": "string",
-          "const": "os:deny-exe-extension"
+          "enum": [
+            "shell:deny-open"
+          ]
         },
         {
-          "description": "Denies the family command without any pre-configured scope.",
+          "description": "shell:deny-spawn -> Denies the spawn command without any pre-configured scope.",
           "type": "string",
-          "const": "os:deny-family"
+          "enum": [
+            "shell:deny-spawn"
+          ]
         },
         {
-          "description": "Denies the hostname command without any pre-configured scope.",
+          "description": "shell:deny-stdin-write -> Denies the stdin_write command without any pre-configured scope.",
           "type": "string",
-          "const": "os:deny-hostname"
+          "enum": [
+            "shell:deny-stdin-write"
+          ]
         },
         {
-          "description": "Denies the locale command without any pre-configured scope.",
+          "description": "window-state:default -> This permission set configures what kind of\noperations are available from the window state plugin.\n\n#### Granted Permissions\n\nAll operations are enabled by default.\n\n",
           "type": "string",
-          "const": "os:deny-locale"
+          "enum": [
+            "window-state:default"
+          ]
         },
         {
-          "description": "Denies the os_type command without any pre-configured scope.",
+          "description": "window-state:allow-filename -> Enables the filename command without any pre-configured scope.",
           "type": "string",
-          "const": "os:deny-os-type"
+          "enum": [
+            "window-state:allow-filename"
+          ]
         },
         {
-          "description": "Denies the platform command without any pre-configured scope.",
+          "description": "window-state:allow-restore-state -> Enables the restore_state command without any pre-configured scope.",
           "type": "string",
-          "const": "os:deny-platform"
+          "enum": [
+            "window-state:allow-restore-state"
+          ]
         },
         {
-          "description": "Denies the version command without any pre-configured scope.",
+          "description": "window-state:allow-save-window-state -> Enables the save_window_state command without any pre-configured scope.",
           "type": "string",
-          "const": "os:deny-version"
+          "enum": [
+            "window-state:allow-save-window-state"
+          ]
         },
         {
-          "description": "This permission set configures which\nshell functionality is exposed by default.\n\n#### Granted Permissions\n\nIt allows to use the `open` functionality without any specific\nscope pre-configured. It will allow opening `http(s)://`,\n`tel:` and `mailto:` links.\n",
+          "description": "window-state:deny-filename -> Denies the filename command without any pre-configured scope.",
           "type": "string",
-          "const": "shell:default"
+          "enum": [
+            "window-state:deny-filename"
+          ]
         },
         {
-          "description": "Enables the execute command without any pre-configured scope.",
+          "description": "window-state:deny-restore-state -> Denies the restore_state command without any pre-configured scope.",
           "type": "string",
-          "const": "shell:allow-execute"
+          "enum": [
+            "window-state:deny-restore-state"
+          ]
         },
         {
-          "description": "Enables the kill command without any pre-configured scope.",
+          "description": "window-state:deny-save-window-state -> Denies the save_window_state command without any pre-configured scope.",
           "type": "string",
-          "const": "shell:allow-kill"
-        },
-        {
-          "description": "Enables the open command without any pre-configured scope.",
-          "type": "string",
-          "const": "shell:allow-open"
-        },
-        {
-          "description": "Enables the spawn command without any pre-configured scope.",
-          "type": "string",
-          "const": "shell:allow-spawn"
-        },
-        {
-          "description": "Enables the stdin_write command without any pre-configured scope.",
-          "type": "string",
-          "const": "shell:allow-stdin-write"
-        },
-        {
-          "description": "Denies the execute command without any pre-configured scope.",
-          "type": "string",
-          "const": "shell:deny-execute"
-        },
-        {
-          "description": "Denies the kill command without any pre-configured scope.",
-          "type": "string",
-          "const": "shell:deny-kill"
-        },
-        {
-          "description": "Denies the open command without any pre-configured scope.",
-          "type": "string",
-          "const": "shell:deny-open"
-        },
-        {
-          "description": "Denies the spawn command without any pre-configured scope.",
-          "type": "string",
-          "const": "shell:deny-spawn"
-        },
-        {
-          "description": "Denies the stdin_write command without any pre-configured scope.",
-          "type": "string",
-          "const": "shell:deny-stdin-write"
-        },
-        {
-          "description": "This permission set configures what kind of\noperations are available from the window state plugin.\n\n#### Granted Permissions\n\nAll operations are enabled by default.\n\n",
-          "type": "string",
-          "const": "window-state:default"
-        },
-        {
-          "description": "Enables the filename command without any pre-configured scope.",
-          "type": "string",
-          "const": "window-state:allow-filename"
-        },
-        {
-          "description": "Enables the restore_state command without any pre-configured scope.",
-          "type": "string",
-          "const": "window-state:allow-restore-state"
-        },
-        {
-          "description": "Enables the save_window_state command without any pre-configured scope.",
-          "type": "string",
-          "const": "window-state:allow-save-window-state"
-        },
-        {
-          "description": "Denies the filename command without any pre-configured scope.",
-          "type": "string",
-          "const": "window-state:deny-filename"
-        },
-        {
-          "description": "Denies the restore_state command without any pre-configured scope.",
-          "type": "string",
-          "const": "window-state:deny-restore-state"
-        },
-        {
-          "description": "Denies the save_window_state command without any pre-configured scope.",
-          "type": "string",
-          "const": "window-state:deny-save-window-state"
+          "enum": [
+            "window-state:deny-save-window-state"
+          ]
         }
       ]
     },
@@ -2426,7 +3027,7 @@
         }
       ]
     },
-    "ShellScopeEntryAllowedArg": {
+    "ShellAllowedArg": {
       "description": "A command argument allowed to be executed by the webview API.",
       "anyOf": [
         {
@@ -2454,18 +3055,18 @@
         }
       ]
     },
-    "ShellScopeEntryAllowedArgs": {
-      "description": "A set of command arguments allowed to be executed by the webview API.\n\nA value of `true` will allow any arguments to be passed to the command. `false` will disable all arguments. A list of [`ShellScopeEntryAllowedArg`] will set those arguments as the only valid arguments to be passed to the attached command configuration.",
+    "ShellAllowedArgs": {
+      "description": "A set of command arguments allowed to be executed by the webview API.\n\nA value of `true` will allow any arguments to be passed to the command. `false` will disable all arguments. A list of [`ShellAllowedArg`] will set those arguments as the only valid arguments to be passed to the attached command configuration.",
       "anyOf": [
         {
           "description": "Use a simple boolean to allow all or disable all arguments to this command configuration.",
           "type": "boolean"
         },
         {
-          "description": "A specific set of [`ShellScopeEntryAllowedArg`] that are valid to call for the command configuration.",
+          "description": "A specific set of [`ShellAllowedArg`] that are valid to call for the command configuration.",
           "type": "array",
           "items": {
-            "$ref": "#/definitions/ShellScopeEntryAllowedArg"
+            "$ref": "#/definitions/ShellAllowedArg"
           }
         }
       ]
diff --git a/desktop/tauri/src-tauri/src/cli.rs b/desktop/tauri/src-tauri/src/cli.rs
deleted file mode 100644
index d11c2aed..00000000
--- a/desktop/tauri/src-tauri/src/cli.rs
+++ /dev/null
@@ -1,107 +0,0 @@
-use log::LevelFilter;
-
-#[cfg(not(debug_assertions))]
-const DEFAULT_LOG_LEVEL: log::LevelFilter = log::LevelFilter::Warn;
-
-#[cfg(debug_assertions)]
-const DEFAULT_LOG_LEVEL: log::LevelFilter = log::LevelFilter::Debug;
-
-#[derive(Debug)]
-pub struct CliArguments {
-    // Path to the installation directory
-    pub data: Option<String>,
-
-    // Log level to use: off, error, warn, info, debug, trace
-    pub log_level: log::LevelFilter,
-
-    // Start in the background without opening a window
-    pub background: bool,
-
-    // Enable experimental notifications via Tauri. Replaces the notifier app.
-    pub with_prompts: bool,
-
-    // Enable experimental prompt support via Tauri. Replaces the notifier app.
-    pub with_notifications: bool,
-}
-
-impl CliArguments {
-    fn parse_log(&mut self, level: String) {
-        self.log_level = match level.as_ref() {
-            "off" => LevelFilter::Off,
-            "error" => LevelFilter::Error,
-            "warn" => LevelFilter::Warn,
-            "info" => LevelFilter::Info,
-            "debug" => LevelFilter::Debug,
-            "trace" => LevelFilter::Trace,
-            _ => DEFAULT_LOG_LEVEL,
-        }
-    }
-}
-
-pub fn parse(raw: impl IntoIterator<Item = impl Into<std::ffi::OsString>>) -> CliArguments {
-    let mut cli = CliArguments {
-        data: None,
-        log_level: DEFAULT_LOG_LEVEL,
-        background: false,
-        with_prompts: false,
-        with_notifications: false,
-    };
-
-    let raw = clap_lex::RawArgs::new(raw);
-    let mut cursor = raw.cursor();
-    raw.next(&mut cursor); // Skip the bin
-
-    while let Some(arg) = raw.next(&mut cursor) {
-        if let Some((long, value)) = arg.to_long() {
-            match long {
-                Ok("data") => {
-                    if let Some(value) = value {
-                        cli.data = Some(value.to_string_lossy().into_owned());
-                    }
-                }
-                Ok("log") => {
-                    if let Some(value) = value {
-                        cli.parse_log(value.to_string_lossy().into_owned());
-                    }
-                }
-                Ok("background") => {
-                    cli.background = true;
-                }
-                Ok("with_prompts") => {
-                    cli.with_prompts = true;
-                }
-                Ok("with_notifications") => {
-                    cli.with_notifications = true;
-                }
-                _ => {
-                    // Ignore unexpected flags
-                }
-            }
-        } else if let Some(mut shorts) = arg.to_short() {
-            while let Some(short) = shorts.next() {
-                match short {
-                    Ok('l') => {
-                        if let Some(value) = shorts.next_value_os() {
-                            let mut str = value.to_string_lossy().into_owned();
-                            _ = str.remove(0); // remove first "=" from value (in -l=warn value will be "=warn")
-                            cli.parse_log(str);
-                        }
-                    }
-                    Ok('d') => {
-                        if let Some(value) = shorts.next_value_os() {
-                            let mut str = value.to_string_lossy().into_owned();
-                            _ = str.remove(0); // remove first "=" from value (in -d=/data value will be "=/data")
-                            cli.data = Some(str);
-                        }
-                    }
-                    Ok('b') => cli.background = true,
-                    _ => {
-                        // Ignore unexpected flags
-                    }
-                }
-            }
-        }
-    }
-
-    cli
-}
diff --git a/desktop/tauri/src-tauri/src/config.rs b/desktop/tauri/src-tauri/src/config.rs
index 6857ec99..60005295 100644
--- a/desktop/tauri/src-tauri/src/config.rs
+++ b/desktop/tauri/src-tauri/src/config.rs
@@ -16,7 +16,7 @@ pub struct Config {
     pub theme: Theme,
 }
 
-const CONFIG_FILE_NAME: &str = "config.json";
+const CONFIG_FILE_NAME: &'static str = "config.json";
 
 pub fn save(app: &AppHandle, config: Config) -> tauri::Result<()> {
     let config_dir = app.path().app_config_dir()?;
diff --git a/desktop/tauri/src-tauri/src/main.rs b/desktop/tauri/src-tauri/src/main.rs
index cce1b266..d269c4f3 100644
--- a/desktop/tauri/src-tauri/src/main.rs
+++ b/desktop/tauri/src-tauri/src/main.rs
@@ -3,6 +3,7 @@
 
 use std::{env, path::Path, time::Duration};
 
+use clap::{Arg, Command};
 use tauri::{AppHandle, Emitter, Listener, Manager, RunEvent, WindowEvent};
 
 // Library crates
@@ -13,13 +14,12 @@ mod service;
 mod xdg;
 
 // App modules
-mod cli;
 mod config;
 mod portmaster;
 mod traymenu;
 mod window;
 
-use log::{debug, error, info};
+use log::{debug, error, info, LevelFilter};
 use portmaster::PortmasterExt;
 use tauri_plugin_log::RotationStrategy;
 use traymenu::setup_tray_menu;
@@ -30,6 +30,12 @@ extern crate lazy_static;
 
 const FALLBACK_TO_OLD_UI_EXIT_CODE: i32 = 77;
 
+#[cfg(not(debug_assertions))]
+const LOG_LEVEL: LevelFilter = LevelFilter::Warn;
+
+#[cfg(debug_assertions)]
+const LOG_LEVEL: LevelFilter = LevelFilter::Debug;
+
 #[derive(Clone, serde::Serialize)]
 struct Payload {
     args: Vec<String>,
@@ -43,12 +49,29 @@ struct WsHandler {
     is_first_connect: bool,
 }
 
+struct CliArguments {
+    // Path to the installation directory
+    data: Option<String>,
+
+    // Log level to use: off, error, warn, info, debug, trace
+    log: String,
+
+    // Start in the background without opening a window
+    background: bool,
+
+    // Enable experimental notifications via Tauri. Replaces the notifier app.
+    with_prompts: bool,
+
+    // Enable experimental prompt support via Tauri. Replaces the notifier app.
+    with_notifications: bool,
+}
+
 impl portmaster::Handler for WsHandler {
     fn name(&self) -> String {
         "main-handler".to_string()
     }
 
-    fn on_connect(&mut self, cli: portapi::client::PortAPI) {
+    fn on_connect(&mut self, cli: portapi::client::PortAPI) -> () {
         info!("connection established, creating main window");
 
         // we successfully connected to Portmaster. Set is_first_connect to false
@@ -116,18 +139,79 @@ fn show_webview_not_installed_dialog() -> i32 {
         }
     }
 
-    FALLBACK_TO_OLD_UI_EXIT_CODE
+    return FALLBACK_TO_OLD_UI_EXIT_CODE;
 }
 
 fn main() {
-    if tauri::webview_version().is_err() {
+    if let Err(_) = tauri::webview_version() {
         std::process::exit(show_webview_not_installed_dialog());
     }
 
-    let cli_args = cli::parse(std::env::args());
+    let matches = Command::new("Portmaster")
+        .ignore_errors(true)
+        .arg(
+            Arg::new("data")
+                .short('d')
+                .long("data")
+                .required(false)
+                .help("Path to the installation directory."),
+        )
+        .arg(
+            Arg::new("log")
+                .short('l')
+                .long("log")
+                .required(false)
+                .help("Log level to use: off, error, warn, info, debug, trace."),
+        )
+        .arg(
+            Arg::new("background")
+                .short('b')
+                .long("background")
+                .required(false)
+                .help("Start in the background without opening a window."),
+        )
+        .arg(
+            Arg::new("with_prompts")
+                .long("with_prompts")
+                .required(false)
+                .action(clap::ArgAction::SetTrue)
+                .help("Enable experimental notifications via Tauri. Replaces the notifier app."),
+        )
+        .arg(
+            Arg::new("with_notifications")
+                .long("with_notifications")
+                .required(false)
+                .action(clap::ArgAction::SetTrue)
+                .help("Enable experimental prompt support via Tauri. Replaces the notifier app."),
+        )
+        .get_matches();
+
+    let mut cli = CliArguments {
+        data: None,
+        log: LOG_LEVEL.to_string(),
+        background: false,
+        with_prompts: false,
+        with_notifications: false,
+    };
+
+    if let Some(data) = matches.get_one::<String>("data") {
+        cli.data = Some(data.to_string());
+    }
+
+    if let Some(log) = matches.get_one::<String>("log") {
+        cli.log = log.to_string();
+    }
+
+    if let Some(value) = matches.get_one::<bool>("with_prompts") {
+        cli.with_prompts = *value;
+    }
+
+    if let Some(value) = matches.get_one::<bool>("with_notifications") {
+        cli.with_notifications = *value;
+    }
 
     #[cfg(target_os = "linux")]
-    let log_target = if let Some(data_dir) = cli_args.data {
+    let log_target = if let Some(data_dir) = cli.data {
         tauri_plugin_log::Target::new(tauri_plugin_log::TargetKind::Folder {
             path: Path::new(&format!("{}/logs/app2", data_dir)).into(),
             file_name: None,
@@ -138,19 +222,30 @@ fn main() {
 
     // TODO(vladimir): Permission for logs/app2 folder are not guaranteed. Use the default location for now.
     #[cfg(target_os = "windows")]
-    let log_target = if let Some(data_dir) = cli_args.data {
+    let log_target = if let Some(data_dir) = cli.data {
         tauri_plugin_log::Target::new(tauri_plugin_log::TargetKind::LogDir { file_name: None })
     } else {
         tauri_plugin_log::Target::new(tauri_plugin_log::TargetKind::Stdout)
     };
 
+    let mut log_level = LOG_LEVEL;
+    match cli.log.as_ref() {
+        "off" => log_level = LevelFilter::Off,
+        "error" => log_level = LevelFilter::Error,
+        "warn" => log_level = LevelFilter::Warn,
+        "info" => log_level = LevelFilter::Info,
+        "debug" => log_level = LevelFilter::Debug,
+        "trace" => log_level = LevelFilter::Trace,
+        _ => {}
+    }
+
     let app = tauri::Builder::default()
         // Shell plugin for open_external support
         .plugin(tauri_plugin_shell::init())
         // Initialize Logging plugin.
         .plugin(
             tauri_plugin_log::Builder::default()
-                .level(cli_args.log_level)
+                .level(log_level)
                 .rotation_strategy(RotationStrategy::KeepAll)
                 .clear_targets()
                 .target(log_target)
@@ -192,18 +287,16 @@ fn main() {
             });
 
             // Handle cli flags:
+            app.portmaster().set_show_after_bootstrap(!cli.background);
             app.portmaster()
-                .set_show_after_bootstrap(!cli_args.background);
-            app.portmaster()
-                .with_notification_support(cli_args.with_notifications);
-            app.portmaster()
-                .with_connection_prompts(cli_args.with_prompts);
+                .with_notification_support(cli.with_notifications);
+            app.portmaster().with_connection_prompts(cli.with_prompts);
 
             // prepare a custom portmaster plugin handler that will show the splash-screen
             // (if not in --background) and launch the tray-icon handler.
             let handler = WsHandler {
                 handle: app.handle().clone(),
-                background: cli_args.background,
+                background: cli.background,
                 is_first_connect: true,
             };
 
@@ -216,8 +309,8 @@ fn main() {
         .build(tauri::generate_context!())
         .expect("error while running tauri application");
 
-    app.run(|handle, e| {
-        if let RunEvent::WindowEvent { label, event, .. } = e {
+    app.run(|handle, e| match e {
+        RunEvent::WindowEvent { label, event, .. } => {
             if label != "main" {
                 // We only have one window at most so any other label is unexpected
                 return;
@@ -231,22 +324,32 @@ fn main() {
             //
             // Note: the above javascript does NOT trigger the CloseRequested event so
             // there's no need to handle that case here.
-            if let WindowEvent::CloseRequested { api, .. } = event {
-                debug!(
-                    "window (label={}) close request received, forwarding to user-interface.",
-                    label
-                );
+            //
+            match event {
+                WindowEvent::CloseRequested { api, .. } => {
+                    debug!(
+                        "window (label={}) close request received, forwarding to user-interface.",
+                        label
+                    );
 
-                api.prevent_close();
-                if let Some(window) = handle.get_webview_window(label.as_str()) {
-                    let result = window.emit("exit-requested", "");
-                    if let Err(err) = result {
-                        error!("failed to emit event: {}", err.to_string());
+                    api.prevent_close();
+                    if let Some(window) = handle.get_webview_window(label.as_str()) {
+                        let result = window.emit("exit-requested", "");
+                        if let Err(err) = result {
+                            error!("failed to emit event: {}", err.to_string());
+                        }
+                    } else {
+                        error!("window was None");
                     }
-                } else {
-                    error!("window was None");
                 }
+                _ => {}
             }
         }
+
+        // TODO(vladimir): why was this needed?
+        // RunEvent::ExitRequested { api, .. } => {
+        //     api.prevent_exit();
+        // }
+        _ => {}
     });
 }
diff --git a/desktop/tauri/src-tauri/src/portapi/message.rs b/desktop/tauri/src-tauri/src/portapi/message.rs
index 6230bc55..46eb7c77 100644
--- a/desktop/tauri/src-tauri/src/portapi/message.rs
+++ b/desktop/tauri/src-tauri/src/portapi/message.rs
@@ -26,46 +26,48 @@ pub enum MessageError {
     InvalidPayload(#[from] serde_json::Error),
 }
 
+
 /// Payload defines the payload type and content of a PortAPI message.
-///
+/// 
 /// For the time being, only JSON payloads (indicated by a prefixed 'J' of the payload content)
 /// is directly supported in `Payload::parse()`.
-///
+/// 
 /// For other payload types (like CBOR, BSON, ...) it's the user responsibility to figure out
 /// appropriate decoding from the `Payload::UNKNOWN` variant.
 #[derive(PartialEq, Debug, Clone)]
 pub enum Payload {
-    Json(String),
-    Unknown(String),
+    JSON(String),
+    UNKNOWN(String),
 }
 
 /// ParseError is returned from `Payload::parse()`.
 #[derive(Debug, Error)]
 pub enum ParseError {
     #[error(transparent)]
-    Json(#[from] serde_json::Error),
+    JSON(#[from] serde_json::Error),
 
     #[error("unknown error while parsing")]
-    Unknown,
+    UNKNOWN
 }
 
+
 impl Payload {
     /// Parse the payload into T.
-    ///
+    /// 
     /// Only JSON parsing is supported for now. See [Payload] for more information.
-    pub fn parse<'a, T>(&'a self) -> std::result::Result<T, ParseError>
+    pub fn parse<'a, T>(self: &'a Self) -> std::result::Result<T, ParseError> 
     where
-        T: serde::de::Deserialize<'a>,
-    {
+        T: serde::de::Deserialize<'a> {
+
         match self {
-            Payload::Json(blob) => Ok(serde_json::from_str::<T>(blob.as_str())?),
-            Payload::Unknown(_) => Err(ParseError::Unknown),
+            Payload::JSON(blob) => Ok(serde_json::from_str::<T>(blob.as_str())?),
+            Payload::UNKNOWN(_) => Err(ParseError::UNKNOWN),
         }
     }
 }
 
 /// Supports creating a Payload instance from a String.
-///
+/// 
 /// See [Payload] for more information.
 impl std::convert::From<String> for Payload {
     fn from(value: String) -> Payload {
@@ -75,10 +77,10 @@ impl std::convert::From<String> for Payload {
 
         match first {
             Some(c) => match c {
-                'J' => Payload::Json(rest),
-                _ => Payload::Unknown(value),
+                'J' => Payload::JSON(rest),
+                _ => Payload::UNKNOWN(value),
             },
-            None => Payload::Unknown("".to_string()),
+            None => Payload::UNKNOWN("".to_string())
         }
     }
 }
@@ -87,10 +89,10 @@ impl std::convert::From<String> for Payload {
 impl std::fmt::Display for Payload {
     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
         match self {
-            Payload::Json(payload) => {
+            Payload::JSON(payload) => {
                 write!(f, "J{}", payload)
-            }
-            Payload::Unknown(payload) => {
+            },
+            Payload::UNKNOWN(payload) => {
                 write!(f, "{}", payload)
             }
         }
@@ -98,9 +100,9 @@ impl std::fmt::Display for Payload {
 }
 
 /// Message is an internal representation of a PortAPI message.
-/// Users should more likely use `portapi::types::Request` and `portapi::types::Response`
+/// Users should more likely use `portapi::types::Request` and `portapi::types::Response` 
 /// instead of directly using `Message`.
-///
+/// 
 /// The struct is still public since it might be useful for debugging or to implement new
 /// commands not yet supported by the `portapi::types` crate.
 #[derive(PartialEq, Debug, Clone)]
@@ -113,23 +115,23 @@ pub struct Message {
 
 /// Implementation to marshal a PortAPI message into it's wire-format representation
 /// (which is a string).
-///
+/// 
 /// Note that this conversion does not check for invalid messages!
 impl std::convert::From<Message> for String {
     fn from(value: Message) -> Self {
-        let mut result = String::new();
+        let mut result = "".to_owned();
 
         result.push_str(value.id.to_string().as_str());
-        result.push('|');
+        result.push_str("|");
         result.push_str(&value.cmd);
 
         if let Some(key) = value.key {
-            result.push('|');
+            result.push_str("|");
             result.push_str(key.as_str());
         }
 
         if let Some(payload) = value.payload {
-            result.push('|');
+            result.push_str("|");
             result.push_str(payload.to_string().as_str())
         }
 
@@ -139,15 +141,15 @@ impl std::convert::From<Message> for String {
 
 /// An implementation for `String::parse()` to convert a wire-format representation
 /// of a PortAPI message to a Message instance.
-///
+/// 
 /// Any errors returned from `String::parse()` will be of type `MessageError`
 impl std::str::FromStr for Message {
     type Err = MessageError;
 
     fn from_str(line: &str) -> Result<Self, Self::Err> {
-        let parts = line.split('|').collect::<Vec<&str>>();
+        let parts = line.split("|").collect::<Vec<&str>>();
 
-        let id = match parts.first() {
+        let id = match parts.get(0) {
             Some(s) => match (*s).parse::<usize>() {
                 Ok(id) => Ok(id),
                 Err(_) => Err(MessageError::InvalidID),
@@ -161,15 +163,18 @@ impl std::str::FromStr for Message {
         }?
         .to_string();
 
-        let key = parts.get(2).map(|key| key.to_string());
-        let payload: Option<Payload> = parts.get(3).map(|p| p.to_string().into());
+        let key = parts.get(2)
+            .and_then(|key| Some(key.to_string()));
 
-        Ok(Message {
+        let payload : Option<Payload> = parts.get(3)
+            .and_then(|p| Some(p.to_string().into()));
+
+        return Ok(Message {
             id,
             cmd,
             key,
-            payload,
-        })
+            payload: payload
+        });
     }
 }
 
@@ -186,79 +191,67 @@ mod tests {
 
     #[test]
     fn payload_to_string() {
-        let p = Payload::Json("{}".to_string());
+        let p = Payload::JSON("{}".to_string());
         assert_eq!(p.to_string(), "J{}");
 
-        let p = Payload::Unknown("some unknown content".to_string());
+        let p = Payload::UNKNOWN("some unknown content".to_string());
         assert_eq!(p.to_string(), "some unknown content");
     }
 
     #[test]
     fn payload_from_string() {
         let p: Payload = "J{}".to_string().into();
-        assert_eq!(p, Payload::Json("{}".to_string()));
+        assert_eq!(p, Payload::JSON("{}".to_string()));
 
         let p: Payload = "some unknown content".to_string().into();
-        assert_eq!(p, Payload::Unknown("some unknown content".to_string()));
+        assert_eq!(p, Payload::UNKNOWN("some unknown content".to_string()));
     }
 
     #[test]
     fn payload_parse() {
         let p: Payload = "J{\"a\": 100, \"s\": \"string\"}".to_string().into();
 
-        let t: Test = p.parse().expect("Expected payload parsing to work");
+        let t: Test = p.parse()
+            .expect("Expected payload parsing to work");
 
-        assert_eq!(
-            t,
-            Test {
-                a: 100,
-                s: "string".to_string(),
-            }
-        );
+        assert_eq!(t, Test{
+            a: 100,
+            s: "string".to_string(),
+        });
     }
 
     #[test]
     fn parse_message() {
-        let m = "10|insert|some:key|J{}"
-            .parse::<Message>()
+        let m = "10|insert|some:key|J{}".parse::<Message>()
             .expect("Expected message to parse");
 
-        assert_eq!(
-            m,
-            Message {
-                id: 10,
-                cmd: "insert".to_string(),
-                key: Some("some:key".to_string()),
-                payload: Some(Payload::Json("{}".to_string())),
-            }
-        );
+        assert_eq!(m, Message{
+            id: 10,
+            cmd: "insert".to_string(),
+            key: Some("some:key".to_string()),
+            payload: Some(Payload::JSON("{}".to_string())),
+        });
 
-        let m = "1|done"
-            .parse::<Message>()
+        let m = "1|done".parse::<Message>()
             .expect("Expected message to parse");
 
-        assert_eq!(
-            m,
-            Message {
-                id: 1,
-                cmd: "done".to_string(),
-                key: None,
-                payload: None
-            }
-        );
+        assert_eq!(m, Message{
+            id: 1,
+            cmd: "done".to_string(),
+            key: None,
+            payload: None
+        });
 
-        let m = "".parse::<Message>().expect_err("Expected parsing to fail");
-        if let MessageError::InvalidID = m {
-        } else {
+        let m = "".parse::<Message>()
+            .expect_err("Expected parsing to fail");
+        if let MessageError::InvalidID = m {} else {
             panic!("unexpected error value: {}", m)
         }
 
-        let m = "1"
-            .parse::<Message>()
+        let m = "1".parse::<Message>()
             .expect_err("Expected parsing to fail");
 
-        if let MessageError::MissingCommand = m {
-        } else {
+        if let MessageError::MissingCommand = m {} else {
             panic!("unexpected error value: {}", m)
         }
     }
diff --git a/desktop/tauri/src-tauri/src/portapi/models/config.rs b/desktop/tauri/src-tauri/src/portapi/models/config.rs
index 72b8a1d8..e29474a8 100644
--- a/desktop/tauri/src-tauri/src/portapi/models/config.rs
+++ b/desktop/tauri/src-tauri/src/portapi/models/config.rs
@@ -1,5 +1,5 @@
-use super::super::message::Payload;
 use serde::*;
+use super::super::message::Payload;
 
 #[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
 pub struct BooleanValue {
@@ -13,6 +13,6 @@ impl TryInto<Payload> for BooleanValue {
     fn try_into(self) -> Result<Payload, Self::Error> {
         let str = serde_json::to_string(&self)?;
 
-        Ok(Payload::Json(str))
+        Ok(Payload::JSON(str))
     }
-}
+}
\ No newline at end of file
diff --git a/desktop/tauri/src-tauri/src/portmaster/commands.rs b/desktop/tauri/src-tauri/src/portmaster/commands.rs
index 39c05788..d8e9db08 100644
--- a/desktop/tauri/src-tauri/src/portmaster/commands.rs
+++ b/desktop/tauri/src-tauri/src/portmaster/commands.rs
@@ -86,7 +86,7 @@ pub fn get_app_info<R: Runtime>(
         matching_path,
     };
 
-    if id.is_empty() {
+    if id == "" {
         id = uuid::Uuid::new_v4().to_string()
     }
     let cloned = id.clone();
@@ -137,7 +137,7 @@ pub fn get_app_info<R: Runtime>(
 pub fn get_service_manager_status<R: Runtime>(window: Window<R>, response_id: String) -> Result {
     let mut id = response_id;
 
-    if id.is_empty() {
+    if id == "" {
         id = uuid::Uuid::new_v4().to_string();
     }
     let cloned = id.clone();
@@ -161,7 +161,7 @@ pub fn get_service_manager_status<R: Runtime>(window: Window<R>, response_id: St
 pub fn start_service<R: Runtime>(window: Window<R>, response_id: String) -> Result {
     let mut id = response_id;
 
-    if id.is_empty() {
+    if id == "" {
         id = uuid::Uuid::new_v4().to_string();
     }
     let cloned = id.clone();
diff --git a/desktop/tauri/src-tauri/src/portmaster/mod.rs b/desktop/tauri/src-tauri/src/portmaster/mod.rs
index 0941f306..80049860 100644
--- a/desktop/tauri/src-tauri/src/portmaster/mod.rs
+++ b/desktop/tauri/src-tauri/src/portmaster/mod.rs
@@ -32,13 +32,14 @@ use std::{
 };
 
 use log::{debug, error};
+use serde;
 use std::sync::Mutex;
 use tauri::{AppHandle, Emitter, Manager, Runtime};
 
-const PORTMASTER_BASE_URL: &str = "http://127.0.0.1:817/api/v1/";
+const PORTMASTER_BASE_URL: &'static str = "http://127.0.0.1:817/api/v1/";
 
 pub trait Handler {
-    fn on_connect(&mut self, cli: PortAPI);
+    fn on_connect(&mut self, cli: PortAPI) -> ();
     fn on_disconnect(&mut self);
     fn name(&self) -> String;
 }
@@ -80,7 +81,10 @@ impl<R: Runtime> PortmasterInterface<R> {
         let map = self.state.lock();
 
         if let Ok(map) = map {
-            map.get(&key).cloned()
+            match map.get(&key) {
+                Some(value) => Some(value.clone()),
+                None => None,
+            }
         } else {
             None
         }
@@ -125,8 +129,11 @@ impl<R: Runtime> PortmasterInterface<R> {
 
     /// Returns the current portapi client.
     pub fn get_api(&self) -> Option<PortAPI> {
-        if let Ok(api) = self.api.lock() {
-            (*api).clone()
+        if let Ok(mut api) = self.api.lock() {
+            match &mut *api {
+                Some(api) => Some(api.clone()),
+                None => None,
+            }
         } else {
             None
         }
diff --git a/desktop/tauri/src-tauri/src/portmaster/notifications.rs b/desktop/tauri/src-tauri/src/portmaster/notifications.rs
index 452db996..bf851230 100644
--- a/desktop/tauri/src-tauri/src/portmaster/notifications.rs
+++ b/desktop/tauri/src-tauri/src/portmaster/notifications.rs
@@ -29,13 +29,13 @@ pub async fn notification_handler(cli: PortAPI) {
                         }
 
                         // Skip if this action has already been acted on
-                        if n.selected_action_id.is_empty() {
+                        if n.selected_action_id != "" {
                             return;
                         }
                         show_notification(&cli, key, n).await;
                     }
                     Err(err) => match err {
-                        ParseError::Json(err) => {
+                        ParseError::JSON(err) => {
                             error!("failed to parse notification: {}", err);
                         }
                         _ => {
@@ -81,7 +81,7 @@ pub async fn show_notification(cli: &PortAPI, key: String, n: Notification) {
                                     let _ = cli_clone
                                         .request(Request::Update(
                                             key,
-                                            Payload::Json(
+                                            Payload::JSON(
                                                 json!({
                                                     "SelectedActionID": value
                                                 })
@@ -125,7 +125,7 @@ pub async fn show_notification(cli: &PortAPI, key: String, n: Notification) {
                     let _ = cli
                         .request(Request::Update(
                             key,
-                            Payload::Json(
+                            Payload::JSON(
                                 json!({
                                     "SelectedActionID": value
                                 })
diff --git a/desktop/tauri/src-tauri/src/service/systemd.rs b/desktop/tauri/src-tauri/src/service/systemd.rs
index c316f375..770a9139 100644
--- a/desktop/tauri/src-tauri/src/service/systemd.rs
+++ b/desktop/tauri/src-tauri/src/service/systemd.rs
@@ -26,7 +26,7 @@ impl From<std::process::Output> for ServiceManagerError {
                     .ok()
                     .filter(|s| !s.trim().is_empty())
             })
-            .unwrap_or_else(|| "Failed to run `systemctl`".to_string());
+            .unwrap_or_else(|| format!("Failed to run `systemctl`"));
 
         ServiceManagerError::Other(output.status, msg)
     }
@@ -231,11 +231,11 @@ fn trim_newline(s: &mut String) {
 }
 
 fn get_sudo_cmd() -> std::result::Result<SudoCommand, std::io::Error> {
-    if fs::metadata("/usr/bin/pkexec").is_ok() {
+    if let Ok(_) = fs::metadata("/usr/bin/pkexec") {
         return Ok(SudoCommand::Pkexec);
     }
 
-    if fs::metadata("/usr/bin/gksudo").is_ok() {
+    if let Ok(_) = fs::metadata("/usr/bin/gksudo") {
         return Ok(SudoCommand::Gksu);
     }
 
diff --git a/desktop/tauri/src-tauri/src/traymenu.rs b/desktop/tauri/src-tauri/src/traymenu.rs
index 0983db28..9d775475 100644
--- a/desktop/tauri/src-tauri/src/traymenu.rs
+++ b/desktop/tauri/src-tauri/src/traymenu.rs
@@ -1,18 +1,17 @@
 use std::ops::Deref;
 use std::sync::atomic::AtomicBool;
-use std::sync::RwLock;
+use std::sync::{Mutex, RwLock};
 use std::{collections::HashMap, sync::atomic::Ordering};
 
 use log::{debug, error};
-use tauri::menu::{Menu, MenuItemKind};
 use tauri::tray::{MouseButton, MouseButtonState};
+use tauri::Manager;
 use tauri::{
     image::Image,
-    menu::{MenuBuilder, MenuItemBuilder, PredefinedMenuItem, SubmenuBuilder},
+    menu::{MenuBuilder, MenuItem, MenuItemBuilder, PredefinedMenuItem, SubmenuBuilder},
     tray::{TrayIcon, TrayIconBuilder},
     Wry,
 };
-use tauri::{Manager, Runtime};
 use tauri_plugin_window_state::{AppHandleExt, StateFlags};
 
 use crate::config;
@@ -47,11 +46,13 @@ enum IconColor {
 static CURRENT_ICON_COLOR: RwLock<IconColor> = RwLock::new(IconColor::Red);
 pub static USER_THEME: RwLock<dark_light::Mode> = RwLock::new(dark_light::Mode::Default);
 
-static SPN_STATUS_KEY: &str = "spn_status";
-static SPN_BUTTON_KEY: &str = "spn_toggle";
-static GLOBAL_STATUS_KEY: &str = "global_status";
+lazy_static! {
+    static ref SPN_STATUS: Mutex<Option<MenuItem<Wry>>> = Mutex::new(None);
+    static ref SPN_BUTTON: Mutex<Option<MenuItem<Wry>>> = Mutex::new(None);
+    static ref GLOBAL_STATUS: Mutex<Option<MenuItem<Wry>>> = Mutex::new(None);
+}
 
-const PM_TRAY_ICON_ID: &str = "pm_icon";
+const PM_TRAY_ICON_ID: &'static str = "pm_icon";
 
 // Icons
 
@@ -59,13 +60,13 @@ fn get_theme_mode() -> dark_light::Mode {
     if let Ok(value) = USER_THEME.read() {
         return *value.deref();
     }
-    dark_light::detect()
+    return dark_light::detect();
 }
 
 fn get_green_icon() -> &'static [u8] {
-    const LIGHT_GREEN_ICON: &[u8] =
+    const LIGHT_GREEN_ICON: &'static [u8] =
         include_bytes!("../../../../assets/data/icons/pm_light_green_64.png");
-    const DARK_GREEN_ICON: &[u8] =
+    const DARK_GREEN_ICON: &'static [u8] =
         include_bytes!("../../../../assets/data/icons/pm_dark_green_64.png");
 
     match get_theme_mode() {
@@ -75,9 +76,9 @@ fn get_green_icon() -> &'static [u8] {
 }
 
 fn get_blue_icon() -> &'static [u8] {
-    const LIGHT_BLUE_ICON: &[u8] =
+    const LIGHT_BLUE_ICON: &'static [u8] =
         include_bytes!("../../../../assets/data/icons/pm_light_blue_64.png");
-    const DARK_BLUE_ICON: &[u8] =
+    const DARK_BLUE_ICON: &'static [u8] =
         include_bytes!("../../../../assets/data/icons/pm_dark_blue_64.png");
     match get_theme_mode() {
         dark_light::Mode::Light => DARK_BLUE_ICON,
@@ -86,9 +87,10 @@ fn get_blue_icon() -> &'static [u8] {
 }
 
 fn get_red_icon() -> &'static [u8] {
-    const LIGHT_RED_ICON: &[u8] =
+    const LIGHT_RED_ICON: &'static [u8] =
         include_bytes!("../../../../assets/data/icons/pm_light_red_64.png");
-    const DARK_RED_ICON: &[u8] = include_bytes!("../../../../assets/data/icons/pm_dark_red_64.png");
+    const DARK_RED_ICON: &'static [u8] =
+        include_bytes!("../../../../assets/data/icons/pm_dark_red_64.png");
     match get_theme_mode() {
         dark_light::Mode::Light => DARK_RED_ICON,
         _ => LIGHT_RED_ICON,
@@ -96,9 +98,9 @@ fn get_red_icon() -> &'static [u8] {
 }
 
 fn get_yellow_icon() -> &'static [u8] {
-    const LIGHT_YELLOW_ICON: &[u8] =
+    const LIGHT_YELLOW_ICON: &'static [u8] =
         include_bytes!("../../../../assets/data/icons/pm_light_yellow_64.png");
-    const DARK_YELLOW_ICON: &[u8] =
+    const DARK_YELLOW_ICON: &'static [u8] =
         include_bytes!("../../../../assets/data/icons/pm_dark_yellow_64.png");
     match get_theme_mode() {
         dark_light::Mode::Light => DARK_YELLOW_ICON,
@@ -128,17 +130,29 @@ pub fn setup_tray_menu(
         .enabled(false)
         .build(app)
         .unwrap();
+    {
+        let mut button_ref = GLOBAL_STATUS.lock()?;
+        *button_ref = Some(global_status.clone());
+    }
 
     // Setup SPN status
-    let spn_status = MenuItemBuilder::with_id(SPN_STATUS_KEY, "SPN: Disabled")
+    let spn_status = MenuItemBuilder::with_id("spn_status", "SPN: Disabled")
         .enabled(false)
         .build(app)
         .unwrap();
+    {
+        let mut button_ref = SPN_STATUS.lock()?;
+        *button_ref = Some(spn_status.clone());
+    }
 
     // Setup SPN button
-    let spn_button = MenuItemBuilder::with_id(SPN_BUTTON_KEY, "Enable SPN")
+    let spn = MenuItemBuilder::with_id("spn_toggle", "Enable SPN")
         .build(app)
         .unwrap();
+    {
+        let mut button_ref = SPN_BUTTON.lock()?;
+        *button_ref = Some(spn.clone());
+    }
 
     let system_theme = MenuItemBuilder::with_id("system_theme", "System")
         .build(app)
@@ -166,7 +180,7 @@ pub fn setup_tray_menu(
             &global_status,
             &PredefinedMenuItem::separator(app)?,
             &spn_status,
-            &spn_button,
+            &spn,
             &PredefinedMenuItem::separator(app)?,
             &theme_menu,
             &PredefinedMenuItem::separator(app)?,
@@ -185,10 +199,8 @@ pub fn setup_tray_menu(
                 app.dialog()
                     .message("This does not stop the Portmaster system service")
                     .title("Do you really want to quit the user interface?")
-                    .buttons(tauri_plugin_dialog::MessageDialogButtons::OkCancelCustom(
-                        "Yes, exit".to_owned(),
-                        "No".to_owned(),
-                    ))
+                    .ok_button_label("Yes, exit")
+                    .cancel_button_label("No")
                     .show(move |answer| {
                         if answer {
                             // let _ = handle.emit("exit-requested", "");
@@ -245,8 +257,10 @@ pub fn setup_tray_menu(
                 button_state,
             } = event
             {
-                if let (MouseButton::Left, MouseButtonState::Down) = (button, button_state) {
-                    let _ = open_window(tray.app_handle());
+                if let MouseButton::Left = button {
+                    if let MouseButtonState::Down = button_state {
+                        let _ = open_window(tray.app_handle());
+                    }
                 }
             }
         })
@@ -254,32 +268,28 @@ pub fn setup_tray_menu(
     Ok(icon)
 }
 
-pub fn update_icon<R: Runtime>(
-    icon: AppIcon,
-    menu: Option<Menu<R>>,
-    subsystems: HashMap<String, Subsystem>,
-    spn_status: String,
-) {
-    // iterate over the subsystems and check if there's a module failure
-    let failure = subsystems.values().map(|s| &s.module_status).fold(
-        (subsystem::FAILURE_NONE, "".to_string()),
-        |mut acc, s| {
+pub fn update_icon(icon: AppIcon, subsystems: HashMap<String, Subsystem>, spn_status: String) {
+    // iterate over the subsytems and check if there's a module failure
+    let failure = subsystems
+        .values()
+        .into_iter()
+        .map(|s| &s.module_status)
+        .fold((subsystem::FAILURE_NONE, "".to_string()), |mut acc, s| {
             for m in s {
                 if m.failure_status > acc.0 {
                     acc = (m.failure_status, m.failure_msg.clone())
                 }
             }
             acc
-        },
-    );
+        });
 
-    if let Some(menu) = menu {
-        if let Some(MenuItemKind::MenuItem(global_status)) = menu.get(GLOBAL_STATUS_KEY) {
-            if failure.0 == subsystem::FAILURE_NONE {
-                _ = global_status.set_text("Status: Secured");
-            } else {
-                _ = global_status.set_text(format!("Status: {}", failure.1));
-            }
+    if failure.0 == subsystem::FAILURE_NONE {
+        if let Some(global_status) = &mut *(GLOBAL_STATUS.lock().unwrap()) {
+            _ = global_status.set_text("Status: Secured");
+        }
+    } else {
+        if let Some(global_status) = &mut *(GLOBAL_STATUS.lock().unwrap()) {
+            _ = global_status.set_text(format!("Status: {}", failure.1));
         }
     }
 
@@ -392,10 +402,10 @@ pub async fn tray_handler(cli: PortAPI, app: tauri::AppHandle) {
                         Ok(n) => {
                             subsystems.insert(n.id.clone(), n);
 
-                            update_icon(icon.clone(), app.menu(), subsystems.clone(), spn_status.clone());
+                            update_icon(icon.clone(), subsystems.clone(), spn_status.clone());
                         },
                         Err(err) => match err {
-                            ParseError::Json(err) => {
+                            ParseError::JSON(err) => {
                                 error!("failed to parse subsystem: {}", err);
                             }
                             _ => {
@@ -422,12 +432,12 @@ pub async fn tray_handler(cli: PortAPI, app: tauri::AppHandle) {
                     match payload.parse::<SPNStatus>() {
                         Ok(value) => {
                             debug!("SPN status update: {}", value.status);
-                            spn_status.clone_from(&value.status);
+                            spn_status = value.status.clone();
 
-                            update_icon(icon.clone(), app.menu(), subsystems.clone(), spn_status.clone());
+                            update_icon(icon.clone(), subsystems.clone(), spn_status.clone());
                         },
                         Err(err) => match err {
-                            ParseError::Json(err) => {
+                            ParseError::JSON(err) => {
                                 error!("failed to parse spn status value: {}", err)
                             },
                             _ => {
@@ -453,12 +463,10 @@ pub async fn tray_handler(cli: PortAPI, app: tauri::AppHandle) {
                 if let Some((_, payload)) = res {
                     match payload.parse::<BooleanValue>() {
                         Ok(value) => {
-                            if let Some(menu) = app.menu() {
-                                update_spn_ui_state(menu, value.value.unwrap_or(false));
-                            }
+                            update_spn_ui_state(value.value.unwrap_or(false));
                         },
                         Err(err) => match err {
-                            ParseError::Json(err) => {
+                            ParseError::JSON(err) => {
                                 error!("failed to parse config value: {}", err)
                             },
                             _ => {
@@ -487,9 +495,7 @@ pub async fn tray_handler(cli: PortAPI, app: tauri::AppHandle) {
             }
         }
     }
-    if let Some(menu) = app.menu() {
-        update_spn_ui_state(menu, false);
-    }
+    update_spn_ui_state(false);
     update_icon_color(&icon, IconColor::Red);
 }
 
@@ -554,22 +560,23 @@ fn save_theme(app: &tauri::AppHandle, mode: dark_light::Mode) {
         }
         Err(err) => error!("failed to load config file: {}", err),
     }
-    if let Some(menu) = app.menu() {
-        update_spn_ui_state(menu, false);
-    }
 }
 
-fn update_spn_ui_state<R: Runtime>(menu: Menu<R>, enabled: bool) {
-    if let (Some(MenuItemKind::MenuItem(spn_status)), Some(MenuItemKind::MenuItem(spn_btn))) =
-        (menu.get(SPN_STATUS_KEY), menu.get(SPN_BUTTON_KEY))
-    {
-        if enabled {
-            _ = spn_status.set_text("SPN: Connected");
-            _ = spn_btn.set_text("Disable SPN");
-        } else {
-            _ = spn_status.set_text("SPN: Disabled");
-            _ = spn_btn.set_text("Enable SPN");
-        }
-        SPN_STATE.store(enabled, Ordering::Release);
+fn update_spn_ui_state(enabled: bool) {
+    let mut spn_status = SPN_STATUS.lock().unwrap();
+    let Some(spn_status_ref) = &mut *spn_status else {
+        return;
+    };
+    let mut spn_btn = SPN_BUTTON.lock().unwrap();
+    let Some(spn_btn_ref) = &mut *spn_btn else {
+        return;
+    };
+    if enabled {
+        _ = spn_status_ref.set_text("SPN: Connected");
+        _ = spn_btn_ref.set_text("Disable SPN");
+    } else {
+        _ = spn_status_ref.set_text("SPN: Disabled");
+        _ = spn_btn_ref.set_text("Enable SPN");
     }
+    SPN_STATE.store(enabled, Ordering::SeqCst);
 }
diff --git a/desktop/tauri/src-tauri/src/window.rs b/desktop/tauri/src-tauri/src/window.rs
index d2ddb1fd..b5e526a4 100644
--- a/desktop/tauri/src-tauri/src/window.rs
+++ b/desktop/tauri/src-tauri/src/window.rs
@@ -6,8 +6,9 @@ use tauri::{
 
 use crate::{portmaster::PortmasterExt, traymenu};
 
-const LIGHT_PM_ICON: &[u8] = include_bytes!("../../../../assets/data/icons/pm_light_512.png");
-const DARK_PM_ICON: &[u8] = include_bytes!("../../../../assets/data/icons/pm_dark_512.png");
+const LIGHT_PM_ICON: &'static [u8] =
+    include_bytes!("../../../../assets/data/icons/pm_light_512.png");
+const DARK_PM_ICON: &'static [u8] = include_bytes!("../../../../assets/data/icons/pm_dark_512.png");
 
 /// Either returns the existing "main" window or creates a new one.
 ///
@@ -53,7 +54,7 @@ pub fn create_main_window(app: &AppHandle) -> Result<WebviewWindow> {
     set_window_icon(&window);
 
     #[cfg(debug_assertions)]
-    if std::env::var("TAURI_SHOW_IMMEDIATELY").is_ok() {
+    if let Ok(_) = std::env::var("TAURI_SHOW_IMMEDIATELY") {
         debug!("[tauri] TAURI_SHOW_IMMEDIATELY is set, opening window");
 
         if let Err(err) = window.show() {
@@ -91,14 +92,14 @@ pub fn close_splash_window(app: &AppHandle) -> Result<()> {
         let _ = window.hide();
         return window.destroy();
     }
-    Err(tauri::Error::WindowNotFound)
+    return Err(tauri::Error::WindowNotFound);
 }
 
 pub fn hide_splash_window(app: &AppHandle) -> Result<()> {
     if let Some(window) = app.get_webview_window("splash") {
         return window.hide();
     }
-    Err(tauri::Error::WindowNotFound)
+    return Err(tauri::Error::WindowNotFound);
 }
 
 pub fn set_window_icon(window: &WebviewWindow) {
diff --git a/desktop/tauri/src-tauri/src/xdg/mod.rs b/desktop/tauri/src-tauri/src/xdg/mod.rs
index 3db67942..8ff6fd11 100644
--- a/desktop/tauri/src-tauri/src/xdg/mod.rs
+++ b/desktop/tauri/src-tauri/src/xdg/mod.rs
@@ -18,6 +18,7 @@ use std::{
 };
 use thiserror::Error;
 
+use dirs;
 use ini::{Ini, ParseOption};
 
 static mut GTK_DEFAULT_THEME: Option<*mut GtkIconTheme> = None;
@@ -145,7 +146,7 @@ pub fn get_app_info(process_info: ProcessInfo) -> Result<AppInfo> {
             .unwrap()
             .insert(process_info.exec_path, None);
 
-        Err(Error::new(ErrorKind::NotFound, "failed to find app info".to_string()).into())
+        Err(Error::new(ErrorKind::NotFound, format!("failed to find app info")).into())
     } else {
         // sort matches by length
         matches.sort_by(|a, b| a.1.cmp(&b.1));
@@ -177,7 +178,7 @@ pub fn get_app_info(process_info: ProcessInfo) -> Result<AppInfo> {
             };
         }
 
-        Err(Error::new(ErrorKind::NotFound, "failed to find app info".to_string()).into())
+        Err(Error::new(ErrorKind::NotFound, format!("failed to find app info")).into())
     }
 }
 
@@ -335,7 +336,7 @@ fn try_get_app_info(
         }
     }
 
-    if !result.is_empty() {
+    if result.len() > 0 {
         Ok(result)
     } else {
         Err(Error::new(ErrorKind::NotFound, "no matching .desktop files found").into())
@@ -392,7 +393,7 @@ fn get_icon_as_png_dataurl(name: &str, size: i8) -> Result<(String, String)> {
     //      - network
     //
     name_without_ext
-        .split('-')
+        .split("-")
         .for_each(|part| icons.push(part));
 
     for name in icons {
@@ -553,7 +554,15 @@ mod tests {
                         matching_path: bin.clone(),
                         pid: 0,
                     })
-                    .unwrap_or_else(|_| panic!("expected to find app info for {} ({})", bin, cmd));
+                    .expect(
+                        format!(
+                            "expected to find app info for {} ({})",
+                            bin,
+                            cmd.to_string()
+                        )
+                        .as_str(),
+                    );
+
                     let empty_string = String::from("");
 
                     // just make sure all fields are populated
diff --git a/desktop/tauri/src-tauri/tauri.conf.json5 b/desktop/tauri/src-tauri/tauri.conf.json5
index 5bd85972..7363808c 100644
--- a/desktop/tauri/src-tauri/tauri.conf.json5
+++ b/desktop/tauri/src-tauri/tauri.conf.json5
@@ -59,25 +59,7 @@
         ],
         "desktopTemplate": "../../../packaging/linux/portmaster.desktop",
         "files": {
-          // Service file
           "/usr/lib/systemd/system/portmaster.service": "../../../packaging/linux/portmaster.service",
-
-          // Binary files
-          "/usr/lib/portmaster/bin-index.json": "binary/bin-index.json",
-          "/usr/lib/portmaster/portmaster-core": "binary/portmaster-core",
-          "/usr/lib/portmaster/portmaster.zip": "binary/portmaster.zip",
-          "/usr/lib/portmaster/assets.zip": "binary/assets.zip",
-
-          // Intel files
-          "/var/lib/portmaster/intel/intel-index.json": "intel/intel-index.json",
-          "/var/lib/portmaster/intel/base.dsdl": "intel/base.dsdl",
-          "/var/lib/portmaster/intel/geoipv4.mmdb": "intel/geoipv4.mmdb",
-          "/var/lib/portmaster/intel/geoipv6.mmdb": "intel/geoipv6.mmdb",
-          "/var/lib/portmaster/intel/index.dsd": "intel/index.dsd",
-          "/var/lib/portmaster/intel/intermediate.dsdl": "intel/intermediate.dsdl",
-          "/var/lib/portmaster/intel/urgent.dsdl": "intel/urgent.dsdl",
-
-          // Shortcut
           "/etc/xdg/autostart/portmaster.desktop": "../../../packaging/linux/portmaster-autostart.desktop"
         },
         "postInstallScript": "../../../packaging/linux/postinst",
@@ -90,25 +72,7 @@
         "desktopTemplate": "../../../packaging/linux/portmaster.desktop",
         "release": "1",
         "files": {
-          // Service file
           "/usr/lib/systemd/system/portmaster.service": "../../../packaging/linux/portmaster.service",
-
-          // Binary files
-          "/usr/lib/portmaster/bin-index.json": "binary/bin-index.json",
-          "/usr/lib/portmaster/portmaster-core": "binary/portmaster-core",
-          "/usr/lib/portmaster/portmaster.zip": "binary/portmaster.zip",
-          "/usr/lib/portmaster/assets.zip": "binary/assets.zip",
-
-          // Intel files
-          "/var/lib/portmaster/intel/intel-index.json": "intel/intel-index.json",
-          "/var/lib/portmaster/intel/base.dsdl": "intel/base.dsdl",
-          "/var/lib/portmaster/intel/geoipv4.mmdb": "intel/geoipv4.mmdb",
-          "/var/lib/portmaster/intel/geoipv6.mmdb": "intel/geoipv6.mmdb",
-          "/var/lib/portmaster/intel/index.dsd": "intel/index.dsd",
-          "/var/lib/portmaster/intel/intermediate.dsdl": "intel/intermediate.dsdl",
-          "/var/lib/portmaster/intel/urgent.dsdl": "intel/urgent.dsdl",
-
-          // Shortcut
           "/etc/xdg/autostart/portmaster.desktop": "../../../packaging/linux/portmaster-autostart.desktop"
         },
         "postInstallScript": "../../../packaging/linux/postinst",
@@ -123,10 +87,8 @@
       },
       "wix": {
         "fragmentPaths": [
-          "templates/service.wxs",
-          "templates/files.wxs"
+          "templates/service.wxs"
         ],
-        "componentGroupRefs": ["BinaryAndIntelFiles"],
         "template": "templates/main.wxs"
       }
     },
@@ -143,4 +105,4 @@
       "../../../assets/data/icons/pm_light.ico"
     ]
   }
-}
+}
\ No newline at end of file
diff --git a/desktop/tauri/src-tauri/templates/files.wxs b/desktop/tauri/src-tauri/templates/files.wxs
deleted file mode 100644
index 4828178f..00000000
--- a/desktop/tauri/src-tauri/templates/files.wxs
+++ /dev/null
@@ -1,39 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi">
-  <Fragment>
-    <DirectoryRef Id="TARGETDIR">
-      <Directory Id="CommonAppDataFolder" Name="CommonAppData">
-        <Directory Id="PortmasterDir" Name="Portmaster">
-          <Directory Id="IntelDir" Name="intel" />
-        </Directory>
-      </Directory>
-    </DirectoryRef>
-  </Fragment>
-
-  <Fragment>
-    <Component Id="BinaryFiles" Directory="INSTALLDIR" Guid="850cdd31-424d-45f5-b8f0-95df950ebd0d">
-      <File Id="BinIndexJson" Source="..\..\..\..\binary\bin-index.json" />
-      <File Id="PortmasterCoreExe" Source="..\..\..\..\binary\portmaster-core.exe" />
-      <File Id="PortmasterKextSys" Source="..\..\..\..\binary\portmaster-kext.sys" />
-      <File Id="PortmasterZip" Source="..\..\..\..\binary\portmaster.zip" />
-      <File Id="AssetsZip" Source="..\..\..\..\binary\assets.zip" />
-    </Component>
-
-    <Component Id="IntelFiles" Directory="IntelDir" Guid="0bb439f1-2075-45b0-95bf-78ed3dffeb69">
-      <File Id="IntelIndexJson" Source="..\..\..\..\intel\intel-index.json" />
-      <File Id="BaseDsdl" Source="..\..\..\..\intel\base.dsdl" />
-      <File Id="Geoipv4Mmdb" Source="..\..\..\..\intel\geoipv4.mmdb" />
-      <File Id="Geoipv6Mmdb" Source="..\..\..\..\intel\geoipv6.mmdb" />
-      <File Id="IndexDsd" Source="..\..\..\..\intel\index.dsd" />
-      <File Id="IntermediateDsdl" Source="..\..\..\..\intel\intermediate.dsdl" />
-      <File Id="UrgentDsdl" Source="..\..\..\..\intel\urgent.dsdl" />
-    </Component>
-  </Fragment>
-
-  <Fragment>
-    <ComponentGroup Id="BinaryAndIntelFiles">
-      <ComponentRef Id="BinaryFiles" />
-      <ComponentRef Id="IntelFiles" />
-    </ComponentGroup>
-  </Fragment>
-</Wix>
\ No newline at end of file
diff --git a/desktop/tauri/src-tauri/templates/main.wxs b/desktop/tauri/src-tauri/templates/main.wxs
index f62f27a3..249eccf0 100644
--- a/desktop/tauri/src-tauri/templates/main.wxs
+++ b/desktop/tauri/src-tauri/templates/main.wxs
@@ -139,7 +139,7 @@
                 {{/each~}}
             </Component>
             <Component Id="Path" Guid="{{path_component_guid}}" Win64="$(var.Win64)">
-                <File Id="Path" Source="{{main_binary_path}}" KeyPath="yes" Checksum="yes"/>
+                <File Id="Path" Source="{{app_exe_source}}" KeyPath="yes" Checksum="yes"/>
                 {{#each file_associations as |association| ~}}
                 {{#each association.ext as |ext| ~}}
                 <ProgId Id="{{../../product_name}}.{{ext}}" Advertise="yes" Description="{{association.description}}">
@@ -341,16 +341,14 @@
         </InstallExecuteSequence>
         {{/if}}
 
+        <CustomActionRef Id='InstallPortmasterService' />
+        <CustomActionRef Id='StopPortmasterService' />
+        <CustomActionRef Id='DeletePortmasterService' />
+
         <InstallExecuteSequence>
           <Custom Action="LaunchApplication" After="InstallFinalize">AUTOLAUNCHAPP AND NOT Installed</Custom>
         </InstallExecuteSequence>
 
         <SetProperty Id="ARPINSTALLLOCATION" Value="[INSTALLDIR]" After="CostFinalize"/>
-
-        <!-- Service fragments -->
-        <CustomActionRef Id='InstallPortmasterService' />
-        <CustomActionRef Id='StopPortmasterService' />
-        <CustomActionRef Id='DeletePortmasterService' />
-        <!-- End Service fragments -->
     </Product>
 </Wix>
\ No newline at end of file
diff --git a/desktop/tauri/src-tauri/templates/main_original.wxs b/desktop/tauri/src-tauri/templates/main_original.wxs
index b1d2672a..1b8116ed 100644
--- a/desktop/tauri/src-tauri/templates/main_original.wxs
+++ b/desktop/tauri/src-tauri/templates/main_original.wxs
@@ -139,7 +139,7 @@
                 {{/each~}}
             </Component>
             <Component Id="Path" Guid="{{path_component_guid}}" Win64="$(var.Win64)">
-                <File Id="Path" Source="{{main_binary_path}}" KeyPath="yes" Checksum="yes"/>
+                <File Id="Path" Source="{{app_exe_source}}" KeyPath="yes" Checksum="yes"/>
                 {{#each file_associations as |association| ~}}
                 {{#each association.ext as |ext| ~}}
                 <ProgId Id="{{../../product_name}}.{{ext}}" Advertise="yes" Description="{{association.description}}">
diff --git a/desktop/tauri/src-tauri/templates/nsis_install_hooks.nsh b/desktop/tauri/src-tauri/templates/nsis_install_hooks.nsh
index 6b5afb58..9d7bd94a 100644
--- a/desktop/tauri/src-tauri/templates/nsis_install_hooks.nsh
+++ b/desktop/tauri/src-tauri/templates/nsis_install_hooks.nsh
@@ -1,34 +1,13 @@
-!macro NSIS_HOOK_PREINSTALL
-  ; Current working directory is <project-dir>\desktop\tauri\src-tauri\target\release\nsis\x64
-
-  SetOutPath "$INSTDIR"
-
-  File "..\..\..\..\binary\bin-index.json"
-  File "..\..\..\..\binary\portmaster-core.exe"
-  File "..\..\..\..\binary\portmaster-kext.sys"
-  File "..\..\..\..\binary\portmaster.zip"
-  File "..\..\..\..\binary\assets.zip"
-
-  SetOutPath "$COMMONPROGRAMDATA\Portmaster\intel"
-
-  File "..\..\..\..\intel\intel-index.json"
-  File "..\..\..\..\intel\base.dsdl"
-  File "..\..\..\..\intel\geoipv4.mmdb"
-  File "..\..\..\..\intel\geoipv6.mmdb"
-  File "..\..\..\..\intel\index.dsd"
-  File "..\..\..\..\intel\intermediate.dsdl"
-  File "..\..\..\..\intel\urgent.dsdl"
-
-  ; restire previous state
-  SetOutPath "$INSTDIR"
+!define NSIS_HOOK_POSTINSTALL "NSIS_HOOK_POSTINSTALL_"
 
+!macro NSIS_HOOK_POSTINSTALL_
+  ExecWait '"$INSTDIR\portmaster-start.exe" install core-service --data="$INSTDIR\data"'
 !macroend
 
-!macro NSIS_HOOK_POSTINSTALL
-  ExecWait 'sc.exe create PortmasterCore binPath= "$INSTDIR\portmaster-core.exe" --data="$COMMONPROGRAMDATA\Portmaster\data"'
-!macroend
 
-!macro NSIS_HOOK_PREUNINSTALL
+!define NSIS_HOOK_PREUNINSTALL "NSIS_HOOK_PREUNINSTALL_"
+
+!macro NSIS_HOOK_PREUNINSTALL_
   ExecWait 'sc.exe stop PortmasterCore'
   ExecWait 'sc.exe delete PortmasterCore'
 !macroend
diff --git a/desktop/tauri/src-tauri/templates/service.wxs b/desktop/tauri/src-tauri/templates/service.wxs
index 4ade87cd..31ff064c 100644
--- a/desktop/tauri/src-tauri/templates/service.wxs
+++ b/desktop/tauri/src-tauri/templates/service.wxs
@@ -3,7 +3,7 @@
 	<Fragment>
     <CustomAction Id="InstallPortmasterService"
               Directory="INSTALLDIR"
-              ExeCommand="sc.exe create PortmasterCore binPath= &quot;[INSTALLDIR]portmaster-core.exe --data [CommonAppDataFolder]Portmaster\data&quot;"
+              ExeCommand="&quot;[INSTALLDIR]portmaster-start.exe&quot; install core-service --data=&quot;[INSTALLDIR]data&quot;"
               Execute="commit"
               Return="check"
               Impersonate="no"
diff --git a/go.mod b/go.mod
index eb548caa..436df094 100644
--- a/go.mod
+++ b/go.mod
@@ -6,6 +6,7 @@ go 1.22.0
 replace github.com/tc-hib/winres => github.com/dhaavi/winres v0.2.2
 
 require (
+	fyne.io/systray v1.11.0
 	github.com/VictoriaMetrics/metrics v1.35.1
 	github.com/Xuanwo/go-locale v1.1.1
 	github.com/aead/serpent v0.0.0-20160714141033-fba169763ea6
@@ -18,11 +19,11 @@ require (
 	github.com/coreos/go-iptables v0.7.0
 	github.com/davecgh/go-spew v1.1.1
 	github.com/dgraph-io/badger v1.6.2
+	github.com/dhaavi/go-notify v0.0.0-20190209221809-c404b1f22435
 	github.com/florianl/go-conntrack v0.4.0
 	github.com/florianl/go-nfqueue v1.3.2
 	github.com/fogleman/gg v1.3.0
 	github.com/ghodss/yaml v1.0.0
-	github.com/gobwas/glob v0.2.3
 	github.com/godbus/dbus/v5 v5.1.0
 	github.com/gofrs/uuid v4.4.0+incompatible
 	github.com/google/gopacket v1.1.19
@@ -33,8 +34,9 @@ require (
 	github.com/hashicorp/go-version v1.7.0
 	github.com/jackc/puddle/v2 v2.2.1
 	github.com/lmittmann/tint v1.0.5
-	github.com/maruel/panicparse/v2 v2.3.1
 	github.com/mat/besticon v3.12.0+incompatible
+	github.com/mattn/go-colorable v0.1.13
+	github.com/mattn/go-isatty v0.0.20
 	github.com/miekg/dns v1.1.62
 	github.com/mitchellh/copystructure v1.2.0
 	github.com/mitchellh/go-server-timing v1.0.1
@@ -42,7 +44,7 @@ require (
 	github.com/oschwald/maxminddb-golang v1.13.1
 	github.com/r3labs/diff/v3 v3.0.1
 	github.com/rot256/pblind v0.0.0-20240730113005-f3275049ead5
-	github.com/safing/jess v0.3.5
+	github.com/safing/jess v0.3.4
 	github.com/safing/structures v1.1.0
 	github.com/seehuhn/fortuna v1.0.1
 	github.com/shirou/gopsutil v3.21.11+incompatible
@@ -69,12 +71,15 @@ require (
 require (
 	github.com/AndreasBriese/bbloom v0.0.0-20190825152654-46b345b51c96 // indirect
 	github.com/aead/ecdh v0.2.0 // indirect
+	github.com/alessio/shellescape v1.4.2 // indirect
 	github.com/cespare/xxhash/v2 v2.3.0 // indirect
+	github.com/danieljoos/wincred v1.2.1 // indirect
 	github.com/dgraph-io/ristretto v0.1.1 // indirect
 	github.com/dustin/go-humanize v1.0.1 // indirect
 	github.com/felixge/httpsnoop v1.0.4 // indirect
 	github.com/fxamacker/cbor/v2 v2.7.0 // indirect
 	github.com/go-ole/go-ole v1.3.0 // indirect
+	github.com/godbus/dbus v4.1.0+incompatible // indirect
 	github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect
 	github.com/golang/gddo v0.0.0-20210115222349-20d68f94ee1f // indirect
 	github.com/golang/glog v1.2.1 // indirect
@@ -85,7 +90,7 @@ require (
 	github.com/inconshreveable/mousetrap v1.1.0 // indirect
 	github.com/josharian/native v1.1.0 // indirect
 	github.com/klauspost/cpuid/v2 v2.2.8 // indirect
-	github.com/mattn/go-isatty v0.0.20 // indirect
+	github.com/maruel/panicparse/v2 v2.3.1 // indirect
 	github.com/mdlayher/netlink v1.7.2 // indirect
 	github.com/mdlayher/socket v0.5.1 // indirect
 	github.com/mitchellh/reflectwalk v1.0.2 // indirect
@@ -107,6 +112,7 @@ require (
 	github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
 	github.com/x448/float16 v0.8.4 // indirect
 	github.com/yusufpapurcu/wmi v1.2.4 // indirect
+	github.com/zalando/go-keyring v0.2.5 // indirect
 	github.com/zeebo/blake3 v0.2.4 // indirect
 	golang.org/x/crypto v0.26.0 // indirect
 	golang.org/x/mod v0.20.0 // indirect
diff --git a/go.sum b/go.sum
index ef8feec4..50df69d6 100644
--- a/go.sum
+++ b/go.sum
@@ -1,4 +1,6 @@
 cloud.google.com/go v0.16.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
+fyne.io/systray v1.11.0 h1:D9HISlxSkx+jHSniMBR6fCFOUjk1x/OOOJLa9lJYAKg=
+fyne.io/systray v1.11.0/go.mod h1:RVwqP9nYMo7h5zViCBHri2FgjXF7H2cub7MAq4NSoLs=
 github.com/AndreasBriese/bbloom v0.0.0-20190825152654-46b345b51c96 h1:cTp8I5+VIoKjsnZuH8vjyaysT/ses3EvZeaV/1UkF2M=
 github.com/AndreasBriese/bbloom v0.0.0-20190825152654-46b345b51c96/go.mod h1:bOvUY6CB00SOBii9/FifXqc0awNKxLFCL/+pkDPuyl8=
 github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
@@ -14,6 +16,8 @@ github.com/aead/serpent v0.0.0-20160714141033-fba169763ea6 h1:5L8Mj9Co9sJVgW3TpY
 github.com/aead/serpent v0.0.0-20160714141033-fba169763ea6/go.mod h1:3HgLJ9d18kXMLQlJvIY3+FszZYMxCz8WfE2MQ7hDY0w=
 github.com/agext/levenshtein v1.2.3 h1:YB2fHEn0UJagG8T1rrWknE3ZQzWM06O8AMAatNn7lmo=
 github.com/agext/levenshtein v1.2.3/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558=
+github.com/alessio/shellescape v1.4.2 h1:MHPfaU+ddJ0/bYWpgIeUnQUqKrlJ1S7BfEYPM4uEoM0=
+github.com/alessio/shellescape v1.4.2/go.mod h1:PZAiSCk0LJaZkiCSkPv8qIobYglO3FPpyFjDCtHLS30=
 github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
 github.com/armon/go-radix v1.0.0 h1:F4z6KzEeeQIMeLFa97iZU6vupzoecKdU5TX24SNppXI=
 github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
@@ -39,6 +43,8 @@ github.com/coreos/go-iptables v0.7.0/go.mod h1:Qe8Bv2Xik5FyTXwgIbLAnv2sWSBmvWdFE
 github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
 github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE=
 github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
+github.com/danieljoos/wincred v1.2.1 h1:dl9cBrupW8+r5250DYkYxocLeZ1Y4vB1kxgtjxw8GQs=
+github.com/danieljoos/wincred v1.2.1/go.mod h1:uGaFL9fDn3OLTvzCGulzE+SzjEe5NGlh5FdCcyfPwps=
 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
 github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@@ -49,6 +55,8 @@ github.com/dgraph-io/ristretto v0.1.1 h1:6CWw5tJNgpegArSHpNHJKldNeq03FQCwYvfMVWa
 github.com/dgraph-io/ristretto v0.1.1/go.mod h1:S1GPSBCYCIhmVNfcth17y2zZtQT6wzkzgwUve0VDWWA=
 github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2 h1:tdlZCpZ/P9DhczCTSixgIKmwPv6+wP5DGjqLYw5SUiA=
 github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw=
+github.com/dhaavi/go-notify v0.0.0-20190209221809-c404b1f22435 h1:AnwbdEI8eV3GzLM3SlrJlYmYa6OB5X8RwY4A8QJOCP0=
+github.com/dhaavi/go-notify v0.0.0-20190209221809-c404b1f22435/go.mod h1:EMJ8XWTopp8OLRBMUm9vHE8Wn48CNpU21HM817OKNrc=
 github.com/dhaavi/winres v0.2.2 h1:SUago7FwhgLSMyDdeuV6enBZ+ZQSl0KwcnbWzvlfBls=
 github.com/dhaavi/winres v0.2.2/go.mod h1:1NTs+/DtKP1BplIL1+XQSoq4X1PUfLczexS7gf3x9T4=
 github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
@@ -77,8 +85,8 @@ github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW
 github.com/go-quicktest/qt v1.101.0 h1:O1K29Txy5P2OK0dGo59b7b0LR6wKfIhttaAhHUyn7eI=
 github.com/go-quicktest/qt v1.101.0/go.mod h1:14Bz/f7NwaXPtdYEgzsx46kqSxVwTbzVZsDC26tQJow=
 github.com/go-stack/stack v1.6.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
-github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=
-github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
+github.com/godbus/dbus v4.1.0+incompatible h1:WqqLRTsQic3apZUK9qC5sGNfXthmPXzUZ7nQPrNITa4=
+github.com/godbus/dbus v4.1.0+incompatible/go.mod h1:/YcGZj5zSblfDWMMoOzV4fas9FZnQYTkDnsGvmh2Grw=
 github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk=
 github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
 github.com/gofrs/uuid v4.4.0+incompatible h1:3qXRTX8/NbyulANqlc0lchS1gqAVxRgsuW1YrTJupqA=
@@ -171,8 +179,11 @@ github.com/mat/besticon v3.12.0+incompatible h1:1KTD6wisfjfnX+fk9Kx/6VEZL+MAW1Lh
 github.com/mat/besticon v3.12.0+incompatible/go.mod h1:mA1auQYHt6CW5e7L9HJLmqVQC8SzNk2gVwouO0AbiEU=
 github.com/mattn/go-colorable v0.0.10-0.20170816031813-ad5389df28cd/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
 github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
+github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
+github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
 github.com/mattn/go-isatty v0.0.2/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
 github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
+github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
 github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
 github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
 github.com/mdlayher/ethtool v0.0.0-20210210192532-2b88debcdd43/go.mod h1:+t7E0lkKfbBsebllff1xdTmyJt8lH37niI6kwFk9OTo=
@@ -237,8 +248,6 @@ github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR
 github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
 github.com/safing/jess v0.3.4 h1:/p6ensqEUn2jI/z1EB9JUdwH4MJQirh/C9jEwNBzxw8=
 github.com/safing/jess v0.3.4/go.mod h1:+B6UJnXVxi406Wk08SDnoC5NNBL7t3N0vZGokEbkVQI=
-github.com/safing/jess v0.3.5 h1:KS5elTKfWcDUow8SUoCj5QdyyGJNoExJNySerNkbxUU=
-github.com/safing/jess v0.3.5/go.mod h1:+B6UJnXVxi406Wk08SDnoC5NNBL7t3N0vZGokEbkVQI=
 github.com/safing/structures v1.1.0 h1:QzHBQBjaZSLzw2f6PM4ibSmPcfBHAOB5CKJ+k4FYkhQ=
 github.com/safing/structures v1.1.0/go.mod h1:QUrB74FcU41ahQ5oy3YNFCoSq+twE/n3+vNZc2K35II=
 github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww=
@@ -269,6 +278,8 @@ github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DM
 github.com/spkg/zipfs v0.7.1 h1:+2X5lvNHTybnDMQZAIHgedRXZK1WXdc+94R/P5v2XWE=
 github.com/spkg/zipfs v0.7.1/go.mod h1:48LW+/Rh1G7aAav1ew1PdlYn52T+LM+ARmSHfDNJvg8=
 github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
+github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
 github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
 github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
 github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
@@ -314,6 +325,8 @@ github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9dec
 github.com/yuin/goldmark v1.4.0/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
 github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
 github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
+github.com/zalando/go-keyring v0.2.5 h1:Bc2HHpjALryKD62ppdEzaFG6VxL6Bc+5v0LYpN8Lba8=
+github.com/zalando/go-keyring v0.2.5/go.mod h1:HL4k+OXQfJUWaMnqyuSOc0drfGPX2b51Du6K+MRgZMk=
 github.com/zeebo/assert v1.1.0 h1:hU1L1vLTHsnO8x8c9KAR5GmM5QscxHg5RNU5z5qbUWY=
 github.com/zeebo/assert v1.1.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0=
 github.com/zeebo/blake3 v0.2.4 h1:KYQPkhpRtcqh0ssGYcKLG1JYvddkEA8QwCM/yBqhaZI=
@@ -403,6 +416,7 @@ golang.org/x/sys v0.0.0-20211210111614-af8b64212486/go.mod h1:oPkhp1MJrh7nUepCBc
 golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220128215802-99c3d69c2c27/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220408201424-a24fb2fb8a0f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20221010170243-090e33056c14/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
diff --git a/packaging/linux/portmaster.service b/packaging/linux/portmaster.service
index c48c8a24..7541d499 100644
--- a/packaging/linux/portmaster.service
+++ b/packaging/linux/portmaster.service
@@ -30,13 +30,12 @@ ProtectKernelTunables=yes
 ProtectKernelLogs=yes
 ProtectControlGroups=yes
 PrivateDevices=yes
-AmbientCapabilities=cap_chown cap_kill cap_net_admin cap_net_bind_service cap_net_broadcast cap_net_raw cap_sys_module cap_sys_ptrace cap_dac_override cap_fowner cap_fsetid cap_sys_resource cap_bpf cap_perfmon
-CapabilityBoundingSet=cap_chown cap_kill cap_net_admin cap_net_bind_service cap_net_broadcast cap_net_raw cap_sys_module cap_sys_ptrace cap_dac_override cap_fowner cap_fsetid cap_sys_resource cap_bpf cap_perfmon
+AmbientCapabilities=cap_chown cap_kill cap_net_admin cap_net_bind_service cap_net_broadcast cap_net_raw cap_sys_module cap_sys_ptrace cap_dac_override cap_fowner cap_fsetid
+CapabilityBoundingSet=cap_chown cap_kill cap_net_admin cap_net_bind_service cap_net_broadcast cap_net_raw cap_sys_module cap_sys_ptrace cap_dac_override cap_fowner cap_fsetid
 StateDirectory=portmaster
 # TODO(ppacher): add --disable-software-updates once it's merged and the release process changed.
-WorkingDirectory=/var/lib/portmaster/data
-ExecStart=/usr/lib/portmaster/portmaster-core --data /var/lib/portmaster/data -devmode -- $PORTMASTER_ARGS
-ExecStopPost=-/usr/lib/portmaster/portmaster-core -recover-iptables
+ExecStart=/usr/bin/portmaster-start --data /opt/safing/portmaster core -- $PORTMASTER_ARGS
+ExecStopPost=-/usr/bin/portmaster-start recover-iptables
 
 [Install]
 WantedBy=multi-user.target
diff --git a/packaging/linux/postinst b/packaging/linux/postinst
index 60e3d97e..8f727403 100644
--- a/packaging/linux/postinst
+++ b/packaging/linux/postinst
@@ -1,19 +1,5 @@
 #!/bin/bash
 
-chmod +x /usr/lib/portmaster/portmaster-core
-
-#
-# Fix selinux permissions for portmaster-core if we have semanage
-# available.
-#
-if command -V semanage >/dev/null 2>&1; then
-    semanage fcontext -a -t bin_t -s system_u $(realpath /usr/lib)'/portmaster/portmaster-core' || :
-    restorecon -R /usr/lib/portmaster/portmaster-core 2>/dev/null >&2 || :
-fi
-
-mv /usr/bin/portmaster /usr/lib/portmaster/portmaster
-ln -s /usr/lib/portmaster/portmaster /usr/bin/portmaster 
-
 systemctl daemon-reload
 systemctl enable portmaster.service
 
diff --git a/packaging/linux/postrm b/packaging/linux/postrm
index 9dddf8e9..a9bf588e 100644
--- a/packaging/linux/postrm
+++ b/packaging/linux/postrm
@@ -1,10 +1 @@
 #!/bin/bash
-
-#
-# Remove selinux permissions for portmaster-core if we have semanage
-# available.
-#
-if command -V semanage >/dev/null 2>&1; then
-    semanage fcontext --delete $(realpath /usr/lib)'/portmaster/portmaster-core' || :
-    restorecon -R /usr/lib/portmaster/portmaster-core 2>/dev/null >&2 || :
-fi
\ No newline at end of file
diff --git a/packaging/windows/generate_windows_installers.ps1 b/packaging/windows/generate_windows_installers.ps1
deleted file mode 100644
index c18a0d1b..00000000
--- a/packaging/windows/generate_windows_installers.ps1
+++ /dev/null
@@ -1,74 +0,0 @@
-# Tested with docker image 'abrarov/msvc-2022:latest'
-# sha256:f49435d194108cd56f173ad5bc6a27c70eed98b7e8cd54488f5acd85efbd51c9
-
-# Run: 
-# Start powershell and cd to the root of the project. Then run:
-# $path = Convert-Path .  # Get the absolute path of the current directory
-# docker run -it --rm -v "${path}:C:/app" -w "C:/app" abrarov/msvc-2022 powershell -NoProfile -File C:/app/packaging/windows/generate_windows_installer.ps1
-
-# Save the current directory
-$originalDirectory = Get-Location
-
-$destinationDir = "desktop/tauri/src-tauri"
-$binaryDir = "$destinationDir/binary"
-$intelDir = "$destinationDir/intel"
-$targetDir = "$destinationDir/target/release"
-
-# Make sure binary folder exists.
-if (-not (Test-Path -Path $binaryDir)) {
-    New-Item -ItemType Directory -Path $binaryDir > $null
-}
-
-
-Write-Output "Copying binary files"
-Copy-Item -Force -Path "dist/binary/index.json" -Destination "$binaryDir/index.json"
-Copy-Item -Force -Path "dist/binary/windows_amd64/portmaster-core.exe" -Destination "$binaryDir/portmaster-core.exe"
-Copy-Item -Force -Path "dist/binary/windows_amd64/portmaster-kext.sys" -Destination "$binaryDir/portmaster-kext.sys"
-Copy-Item -Force -Path "dist/binary/all/portmaster.zip" -Destination "$binaryDir/portmaster.zip"
-Copy-Item -Force -Path "dist/binary/all/assets.zip" -Destination "$binaryDir/assets.zip"
-
-# Make sure target folder exists.
-if (-not (Test-Path -Path $targetDir)) {
-    New-Item -ItemType Directory -Path $targetDir > $null
-}
-Copy-Item -Force -Path "dist/binary/windows_amd64/portmaster.exe" -Destination "$targetDir/portmaster.exe"
-
-# Make sure intel folder exists.
-if (-not (Test-Path -Path $intelDir)) {
-    New-Item -ItemType Directory -Path $intelDir > $null
-}
-
-Write-Output "Copying intel files"
-Copy-Item -Force -Path "dist/intel_decompressed/*" -Destination "$intelDir/"
-
-Set-Location $destinationDir
-
-if (-not (Get-Command cargo -ErrorAction SilentlyContinue)) {
-    Write-Output "Installing rust toolchain..."
-    Invoke-WebRequest -Uri https://win.rustup.rs/x86_64 -OutFile rustup.exe
-    ./rustup.exe install stable
-    $env:PATH += ";C:\Users\ContainerAdministrator\.rustup\toolchains\stable-x86_64-pc-windows-msvc\bin\"
-} else {
-    Write-Output "'cargo' command is already available"
-}
-
-Write-Output "Downloading tauri-cli"
-Invoke-WebRequest -Uri https://github.com/tauri-apps/tauri/releases/download/tauri-cli-v2.0.1/cargo-tauri-x86_64-pc-windows-msvc.zip -OutFile tauri-cli.zip
-Expand-Archive -Force tauri-cli.zip
-./tauri-cli/cargo-tauri.exe bundle
-
-
-Write-Output "Copying generated bundles"
-$installerDist = "..\..\..\dist\windows_amd64\"
-# Make sure distination folder exists.
-if (-not (Test-Path -Path $installerDist)) {
-    New-Item -ItemType Directory -Path $installerDist > $null
-}
-
-Copy-Item -Path ".\target\release\bundle\msi\*" -Destination $installerDist
-Copy-Item -Path ".\target\release\bundle\nsis\*" -Destination $installerDist
-
-
-# Restore the original directory
-Set-Location $originalDirectory
-
diff --git a/service/broadcasts/data.go b/service/broadcasts/data.go
index e92ce84f..2b59e4e6 100644
--- a/service/broadcasts/data.go
+++ b/service/broadcasts/data.go
@@ -7,6 +7,7 @@ import (
 	"github.com/safing/portmaster/base/config"
 	"github.com/safing/portmaster/service/intel/geoip"
 	"github.com/safing/portmaster/service/netenv"
+	"github.com/safing/portmaster/service/updates"
 	"github.com/safing/portmaster/spn/access"
 	"github.com/safing/portmaster/spn/access/account"
 	"github.com/safing/portmaster/spn/captain"
@@ -17,19 +18,18 @@ var portmasterStarted = time.Now()
 func collectData() interface{} {
 	data := make(map[string]interface{})
 
-	// TODO(vladimir)
 	// Get data about versions.
-	// versions := updates.GetSimpleVersions()
-	// data["Updates"] = versions
-	// data["Version"] = versions.Build.Version
-	// numericVersion, err := MakeNumericVersion(versions.Build.Version)
-	// if err != nil {
-	// 	data["NumericVersion"] = &DataError{
-	// 		Error: err,
-	// 	}
-	// } else {
-	// 	data["NumericVersion"] = numericVersion
-	// }
+	versions := updates.GetSimpleVersions()
+	data["Updates"] = versions
+	data["Version"] = versions.Build.Version
+	numericVersion, err := MakeNumericVersion(versions.Build.Version)
+	if err != nil {
+		data["NumericVersion"] = &DataError{
+			Error: err,
+		}
+	} else {
+		data["NumericVersion"] = numericVersion
+	}
 
 	// Get data about install.
 	installInfo, err := GetInstallInfo()
diff --git a/service/broadcasts/install_info.go b/service/broadcasts/install_info.go
index 7f95de53..42969946 100644
--- a/service/broadcasts/install_info.go
+++ b/service/broadcasts/install_info.go
@@ -4,7 +4,6 @@ import (
 	"errors"
 	"fmt"
 	"strconv"
-	"strings"
 	"sync"
 	"time"
 
@@ -112,9 +111,6 @@ func (ii *InstallInfo) checkVersion() {
 // MakeNumericVersion makes a numeric version with the first three version
 // segment always using three digits.
 func MakeNumericVersion(version string) (numericVersion int64, err error) {
-	// Remove any comments.
-	version = strings.SplitN(version, " ", 2)[0]
-
 	// Parse version string.
 	ver, err := semver.NewVersion(version)
 	if err != nil {
diff --git a/service/broadcasts/module.go b/service/broadcasts/module.go
index a9a87074..a3968933 100644
--- a/service/broadcasts/module.go
+++ b/service/broadcasts/module.go
@@ -8,7 +8,6 @@ import (
 
 	"github.com/safing/portmaster/base/database"
 	"github.com/safing/portmaster/service/mgr"
-	"github.com/safing/portmaster/service/updates"
 )
 
 type Broadcasts struct {
@@ -92,6 +91,4 @@ func New(instance instance) (*Broadcasts, error) {
 	return module, nil
 }
 
-type instance interface {
-	IntelUpdates() *updates.Updater
-}
+type instance interface{}
diff --git a/service/broadcasts/notify.go b/service/broadcasts/notify.go
index 235a1bf0..a010f249 100644
--- a/service/broadcasts/notify.go
+++ b/service/broadcasts/notify.go
@@ -18,6 +18,7 @@ import (
 	"github.com/safing/portmaster/base/log"
 	"github.com/safing/portmaster/base/notifications"
 	"github.com/safing/portmaster/service/mgr"
+	"github.com/safing/portmaster/service/updates"
 )
 
 const (
@@ -67,7 +68,7 @@ type BroadcastNotification struct {
 
 func broadcastNotify(ctx *mgr.WorkerCtx) error {
 	// Get broadcast notifications file, load it from disk and parse it.
-	broadcastsResource, err := module.instance.IntelUpdates().GetFile(broadcastsResourcePath)
+	broadcastsResource, err := updates.GetFile(broadcastsResourcePath)
 	if err != nil {
 		return fmt.Errorf("failed to get broadcast notifications update: %w", err)
 	}
diff --git a/service/config.go b/service/config.go
index 7b7e0f08..85a98603 100644
--- a/service/config.go
+++ b/service/config.go
@@ -1,111 +1,3 @@
 package service
 
-import (
-	"errors"
-	"fmt"
-	"os"
-	"path/filepath"
-	"runtime"
-
-	"github.com/safing/jess"
-	"github.com/safing/portmaster/base/log"
-)
-
-type ServiceConfig struct {
-	BinDir  string
-	DataDir string
-
-	LogToStdout bool
-	LogDir      string
-	LogLevel    string
-
-	BinariesIndexURLs   []string
-	IntelIndexURLs      []string
-	VerifyBinaryUpdates jess.TrustStore
-	VerifyIntelUpdates  jess.TrustStore
-}
-
-func (sc *ServiceConfig) Init() error {
-	// Check directories.
-	switch runtime.GOOS {
-	case "windows":
-		// Fall back to defaults.
-		if sc.BinDir == "" {
-			exeDir, err := getCurrentBinaryFolder() // Default: C:/Program Files/Portmaster
-			if err != nil {
-				return fmt.Errorf("derive bin dir from runnning exe: %w", err)
-			}
-			sc.BinDir = exeDir
-		}
-		if sc.DataDir == "" {
-			sc.DataDir = filepath.FromSlash("$ProgramData/Portmaster")
-		}
-		if sc.LogDir == "" {
-			sc.LogDir = filepath.Join(sc.DataDir, "logs")
-		}
-
-	case "linux":
-		// Fall back to defaults.
-		if sc.BinDir == "" {
-			sc.BinDir = "/usr/lib/portmaster"
-		}
-		if sc.DataDir == "" {
-			sc.DataDir = "/var/lib/portmaster"
-		}
-		if sc.LogDir == "" {
-			sc.LogDir = "/var/log/portmaster"
-		}
-
-	default:
-		// Fail if not configured on other platforms.
-		if sc.BinDir == "" {
-			return errors.New("binary directory must be configured - auto-detection not supported on this platform")
-		}
-		if sc.DataDir == "" {
-			return errors.New("binary directory must be configured - auto-detection not supported on this platform")
-		}
-		if !sc.LogToStdout && sc.LogDir == "" {
-			return errors.New("logging directory must be configured - auto-detection not supported on this platform")
-		}
-	}
-
-	// Expand path variables.
-	sc.BinDir = os.ExpandEnv(sc.BinDir)
-	sc.DataDir = os.ExpandEnv(sc.DataDir)
-	sc.LogDir = os.ExpandEnv(sc.LogDir)
-
-	// Apply defaults for required fields.
-	if len(sc.BinariesIndexURLs) == 0 {
-		// FIXME: Select based on setting.
-		sc.BinariesIndexURLs = DefaultStableBinaryIndexURLs
-	}
-	if len(sc.IntelIndexURLs) == 0 {
-		sc.IntelIndexURLs = DefaultIntelIndexURLs
-	}
-
-	// Check log level.
-	if sc.LogLevel != "" && log.ParseLevel(sc.LogLevel) == 0 {
-		return fmt.Errorf("invalid log level %q", sc.LogLevel)
-	}
-
-	return nil
-}
-
-func getCurrentBinaryFolder() (string, error) {
-	// Get the path of the currently running executable
-	exePath, err := os.Executable()
-	if err != nil {
-		return "", fmt.Errorf("failed to get executable path: %w", err)
-	}
-
-	// Get the absolute path
-	absPath, err := filepath.Abs(exePath)
-	if err != nil {
-		return "", fmt.Errorf("failed to get absolute path: %w", err)
-	}
-
-	// Get the directory of the executable
-	installDir := filepath.Dir(absPath)
-
-	return installDir, nil
-}
+type ServiceConfig struct{}
diff --git a/service/core/api.go b/service/core/api.go
index ea4f18d1..c4758cda 100644
--- a/service/core/api.go
+++ b/service/core/api.go
@@ -19,6 +19,7 @@ import (
 	"github.com/safing/portmaster/service/process"
 	"github.com/safing/portmaster/service/resolver"
 	"github.com/safing/portmaster/service/status"
+	"github.com/safing/portmaster/service/updates"
 	"github.com/safing/portmaster/spn/captain"
 )
 
@@ -105,50 +106,6 @@ func registerAPIEndpoints() error {
 		return err
 	}
 
-	if err := api.RegisterEndpoint(api.Endpoint{
-		Path:        "updates/check",
-		WriteMethod: "POST",
-		Write:       api.PermitUser,
-		ActionFunc: func(ar *api.Request) (string, error) {
-			module.instance.BinaryUpdates().TriggerUpdateCheck()
-			module.instance.IntelUpdates().TriggerUpdateCheck()
-			return "update check triggered", nil
-		},
-		Name: "Trigger updates check event",
-	}); err != nil {
-		return err
-	}
-
-	if err := api.RegisterEndpoint(api.Endpoint{
-		Path:        "updates/apply",
-		WriteMethod: "POST",
-		Write:       api.PermitUser,
-		ActionFunc: func(ar *api.Request) (string, error) {
-			module.instance.BinaryUpdates().TriggerApplyUpdates()
-			module.instance.IntelUpdates().TriggerApplyUpdates()
-			return "upgrade triggered", nil
-		},
-		Name: "Trigger updates apply event",
-	}); err != nil {
-		return err
-	}
-
-	if err := api.RegisterEndpoint(api.Endpoint{
-		Path:        "updates/from-url",
-		WriteMethod: "POST",
-		Write:       api.PermitAnyone,
-		ActionFunc: func(ar *api.Request) (string, error) {
-			err := module.instance.BinaryUpdates().UpdateFromURL(string(ar.InputData))
-			if err != nil {
-				return err.Error(), err
-			}
-			return "upgrade triggered", nil
-		},
-		Name: "Replace current version from the version supplied in the URL",
-	}); err != nil {
-		return err
-	}
-
 	return nil
 }
 
@@ -164,8 +121,8 @@ func shutdown(_ *api.Request) (msg string, err error) {
 func restart(_ *api.Request) (msg string, err error) {
 	log.Info("core: user requested restart via action")
 
-	// Trigger restart
-	module.instance.Restart()
+	// Let the updates module handle restarting.
+	updates.RestartNow()
 
 	return "restart initiated", nil
 }
@@ -192,7 +149,7 @@ func debugInfo(ar *api.Request) (data []byte, err error) {
 	config.AddToDebugInfo(di)
 
 	// Detailed information.
-	// TODO(vladimir): updates.AddToDebugInfo(di)
+	updates.AddToDebugInfo(di)
 	compat.AddToDebugInfo(di)
 	module.instance.AddWorkerInfoToDebugInfo(di)
 	di.AddGoroutineStack()
diff --git a/service/core/base/global.go b/service/core/base/global.go
index 912e975a..3b1cc82f 100644
--- a/service/core/base/global.go
+++ b/service/core/base/global.go
@@ -6,6 +6,7 @@ import (
 	"fmt"
 
 	"github.com/safing/portmaster/base/api"
+	"github.com/safing/portmaster/base/dataroot"
 	"github.com/safing/portmaster/base/info"
 	"github.com/safing/portmaster/service/mgr"
 )
@@ -14,10 +15,14 @@ import (
 var (
 	DefaultAPIListenAddress = "127.0.0.1:817"
 
+	dataDir     string
+	databaseDir string
 	showVersion bool
 )
 
 func init() {
+	flag.StringVar(&dataDir, "data", "", "set data directory")
+	flag.StringVar(&databaseDir, "db", "", "alias to --data (deprecated)")
 	flag.BoolVar(&showVersion, "version", false, "show version and exit")
 }
 
@@ -34,6 +39,27 @@ func prep(instance instance) error {
 		return mgr.ErrExecuteCmdLineOp
 	}
 
+	// check data root
+	if dataroot.Root() == nil {
+		// initialize data dir
+
+		// backwards compatibility
+		if dataDir == "" {
+			dataDir = databaseDir
+		}
+
+		// check data dir
+		if dataDir == "" {
+			return errors.New("please set the data directory using --data=/path/to/data/dir")
+		}
+
+		// initialize structure
+		err := dataroot.Initialize(dataDir, 0o0755)
+		if err != nil {
+			return err
+		}
+	}
+
 	// set api listen address
 	api.SetDefaultAPIListenAddress(DefaultAPIListenAddress)
 
diff --git a/service/core/base/logs.go b/service/core/base/logs.go
index 921d7f3a..8ceab9cd 100644
--- a/service/core/base/logs.go
+++ b/service/core/base/logs.go
@@ -7,6 +7,7 @@ import (
 	"strings"
 	"time"
 
+	"github.com/safing/portmaster/base/dataroot"
 	"github.com/safing/portmaster/base/log"
 	"github.com/safing/portmaster/service/mgr"
 )
@@ -25,7 +26,7 @@ func logCleaner(_ *mgr.WorkerCtx) error {
 	ageThreshold := time.Now().Add(-logTTL)
 
 	return filepath.Walk(
-		filepath.Join(module.instance.DataDir(), logFileDir),
+		filepath.Join(dataroot.Root().Path, logFileDir),
 		func(path string, info os.FileInfo, err error) error {
 			if err != nil {
 				if !errors.Is(err, os.ErrNotExist) {
diff --git a/service/core/base/module.go b/service/core/base/module.go
index 0bb7aba2..66492015 100644
--- a/service/core/base/module.go
+++ b/service/core/base/module.go
@@ -58,6 +58,5 @@ func New(instance instance) (*Base, error) {
 }
 
 type instance interface {
-	DataDir() string
 	SetCmdLineOperation(f func() error)
 }
diff --git a/service/core/core.go b/service/core/core.go
index 356bd2eb..ecbcf948 100644
--- a/service/core/core.go
+++ b/service/core/core.go
@@ -16,7 +16,6 @@ import (
 	_ "github.com/safing/portmaster/service/status"
 	_ "github.com/safing/portmaster/service/sync"
 	_ "github.com/safing/portmaster/service/ui"
-	"github.com/safing/portmaster/service/updates"
 )
 
 // Core is the core service module.
@@ -114,8 +113,5 @@ func New(instance instance) (*Core, error) {
 
 type instance interface {
 	Shutdown()
-	Restart()
 	AddWorkerInfoToDebugInfo(di *debug.Info)
-	BinaryUpdates() *updates.Updater
-	IntelUpdates() *updates.Updater
 }
diff --git a/service/firewall/api.go b/service/firewall/api.go
index 07153263..134f6f74 100644
--- a/service/firewall/api.go
+++ b/service/firewall/api.go
@@ -11,11 +11,14 @@ import (
 	"time"
 
 	"github.com/safing/portmaster/base/api"
+	"github.com/safing/portmaster/base/dataroot"
 	"github.com/safing/portmaster/base/log"
+	"github.com/safing/portmaster/base/utils"
 	"github.com/safing/portmaster/service/netenv"
 	"github.com/safing/portmaster/service/network/netutils"
 	"github.com/safing/portmaster/service/network/packet"
 	"github.com/safing/portmaster/service/process"
+	"github.com/safing/portmaster/service/updates"
 )
 
 const (
@@ -36,12 +39,15 @@ For production use please create an API key in the settings.`
 )
 
 var (
+	dataRoot *utils.DirStructure
+
 	apiPortSet bool
 	apiIP      net.IP
 	apiPort    uint16
 )
 
 func prepAPIAuth() error {
+	dataRoot = dataroot.Root()
 	return api.SetAuthenticator(apiAuthenticator)
 }
 
@@ -127,7 +133,7 @@ func authenticateAPIRequest(ctx context.Context, pktInfo *packet.Info) (retry bo
 	var originalPid int
 
 	// Get authenticated path.
-	authenticatedPath := module.instance.BinaryUpdates().GetMainDir()
+	authenticatedPath := updates.RootPath()
 	if authenticatedPath == "" {
 		return false, fmt.Errorf(deniedMsgMisconfigured, api.ErrAPIAccessDeniedMessage) //nolint:stylecheck // message for user
 	}
@@ -209,7 +215,7 @@ func authenticateAPIRequest(ctx context.Context, pktInfo *packet.Info) (retry bo
 		return false, fmt.Errorf(deniedMsgSystem, api.ErrAPIAccessDeniedMessage) //nolint:stylecheck // message for user
 
 	default: // normal process
-		log.Tracer(ctx).Warningf("filter: denying api access to %s - also checked %s (trusted root is %s)", procsChecked[0], strings.Join(procsChecked[1:], " "), module.instance.BinDir())
+		log.Tracer(ctx).Warningf("filter: denying api access to %s - also checked %s (trusted root is %s)", procsChecked[0], strings.Join(procsChecked[1:], " "), dataRoot.Path)
 		return false, fmt.Errorf( //nolint:stylecheck // message for user
 			deniedMsgUnauthorized,
 			api.ErrAPIAccessDeniedMessage,
diff --git a/service/firewall/interception/interception_windows.go b/service/firewall/interception/interception_windows.go
index 3f9df6f8..cb97376b 100644
--- a/service/firewall/interception/interception_windows.go
+++ b/service/firewall/interception/interception_windows.go
@@ -10,13 +10,14 @@ import (
 	"github.com/safing/portmaster/service/mgr"
 	"github.com/safing/portmaster/service/network"
 	"github.com/safing/portmaster/service/network/packet"
+	"github.com/safing/portmaster/service/updates"
 )
 
 var useOldKext = false
 
 // start starts the interception.
 func startInterception(packets chan packet.Packet) error {
-	kextFile, err := module.instance.BinaryUpdates().GetFile("portmaster-kext.sys")
+	kextFile, err := updates.GetPlatformFile("kext/portmaster-kext.sys")
 	if err != nil {
 		return fmt.Errorf("interception: could not get kext sys: %s", err)
 	}
@@ -76,6 +77,7 @@ func startInterception(packets chan packet.Packet) error {
 				case <-w.Done():
 					return nil
 				}
+
 			}
 		})
 
@@ -93,6 +95,7 @@ func startInterception(packets chan packet.Packet) error {
 				case <-w.Done():
 					return nil
 				}
+
 			}
 		})
 
@@ -109,6 +112,7 @@ func startInterception(packets chan packet.Packet) error {
 				case <-w.Done():
 					return nil
 				}
+
 			}
 		})
 	}
@@ -155,4 +159,5 @@ func GetKextVersion() (string, error) {
 		}
 		return version.String(), nil
 	}
+
 }
diff --git a/service/firewall/interception/module.go b/service/firewall/interception/module.go
index 63994c90..072eb3b5 100644
--- a/service/firewall/interception/module.go
+++ b/service/firewall/interception/module.go
@@ -8,7 +8,6 @@ import (
 	"github.com/safing/portmaster/base/log"
 	"github.com/safing/portmaster/service/mgr"
 	"github.com/safing/portmaster/service/network/packet"
-	"github.com/safing/portmaster/service/updates"
 )
 
 // Interception is the packet interception module.
@@ -98,6 +97,4 @@ func New(instance instance) (*Interception, error) {
 	return module, nil
 }
 
-type instance interface {
-	BinaryUpdates() *updates.Updater
-}
+type instance interface{}
diff --git a/service/firewall/interception/windowskext2/kext.go b/service/firewall/interception/windowskext2/kext.go
index 43be43cd..ed15476e 100644
--- a/service/firewall/interception/windowskext2/kext.go
+++ b/service/firewall/interception/windowskext2/kext.go
@@ -62,9 +62,6 @@ func GetKextServiceHandle() windows.Handle {
 
 // Stop intercepting.
 func Stop() error {
-	if kextFile == nil {
-		return fmt.Errorf("kextfile is nil")
-	}
 	// Prepare kernel for shutdown
 	err := shutdownRequest()
 	if err != nil {
diff --git a/service/firewall/module.go b/service/firewall/module.go
index 70226e55..131d4cac 100644
--- a/service/firewall/module.go
+++ b/service/firewall/module.go
@@ -16,7 +16,6 @@ import (
 	"github.com/safing/portmaster/service/netquery"
 	"github.com/safing/portmaster/service/network"
 	"github.com/safing/portmaster/service/profile"
-	"github.com/safing/portmaster/service/updates"
 	"github.com/safing/portmaster/spn/access"
 	"github.com/safing/portmaster/spn/captain"
 )
@@ -160,9 +159,7 @@ func New(instance instance) (*Firewall, error) {
 }
 
 type instance interface {
-	BinDir() string
 	Config() *config.Config
-	BinaryUpdates() *updates.Updater
 	Profile() *profile.ProfileModule
 	Captain() *captain.Captain
 	Access() *access.Access
diff --git a/service/instance.go b/service/instance.go
index ddb23671..ad6e9dab 100644
--- a/service/instance.go
+++ b/service/instance.go
@@ -3,7 +3,6 @@ package service
 import (
 	"context"
 	"fmt"
-	"os"
 	"sync/atomic"
 	"time"
 
@@ -49,17 +48,10 @@ import (
 
 // Instance is an instance of a Portmaster service.
 type Instance struct {
-	ctx       context.Context
-	cancelCtx context.CancelFunc
-
-	shutdownCtx       context.Context
-	cancelShutdownCtx context.CancelFunc
-
+	ctx          context.Context
+	cancelCtx    context.CancelFunc
 	serviceGroup *mgr.Group
 
-	binDir  string
-	dataDir string
-
 	exitCode atomic.Int32
 
 	database      *dbmodule.DBModule
@@ -71,26 +63,25 @@ type Instance struct {
 	rng           *rng.Rng
 	base          *base.Base
 
-	core          *core.Core
-	binaryUpdates *updates.Updater
-	intelUpdates  *updates.Updater
-	geoip         *geoip.GeoIP
-	netenv        *netenv.NetEnv
-	ui            *ui.UI
-	profile       *profile.ProfileModule
-	network       *network.Network
-	netquery      *netquery.NetQuery
-	firewall      *firewall.Firewall
-	filterLists   *filterlists.FilterLists
-	interception  *interception.Interception
-	customlist    *customlists.CustomList
-	status        *status.Status
-	broadcasts    *broadcasts.Broadcasts
-	compat        *compat.Compat
-	nameserver    *nameserver.NameServer
-	process       *process.ProcessModule
-	resolver      *resolver.ResolverModule
-	sync          *sync.Sync
+	core         *core.Core
+	updates      *updates.Updates
+	geoip        *geoip.GeoIP
+	netenv       *netenv.NetEnv
+	ui           *ui.UI
+	profile      *profile.ProfileModule
+	network      *network.Network
+	netquery     *netquery.NetQuery
+	firewall     *firewall.Firewall
+	filterLists  *filterlists.FilterLists
+	interception *interception.Interception
+	customlist   *customlists.CustomList
+	status       *status.Status
+	broadcasts   *broadcasts.Broadcasts
+	compat       *compat.Compat
+	nameserver   *nameserver.NameServer
+	process      *process.ProcessModule
+	resolver     *resolver.ResolverModule
+	sync         *sync.Sync
 
 	access *access.Access
 
@@ -107,30 +98,15 @@ type Instance struct {
 	terminal  *terminal.TerminalModule
 
 	CommandLineOperation func() error
-	ShouldRestart        bool
 }
 
 // New returns a new Portmaster service instance.
 func New(svcCfg *ServiceConfig) (*Instance, error) { //nolint:maintidx
-	// Initialize config.
-	err := svcCfg.Init()
-	if err != nil {
-		return nil, fmt.Errorf("internal service config error: %w", err)
-	}
-
-	// Make sure data dir exists, so that child directories don't dictate the permissions.
-	err = os.MkdirAll(svcCfg.DataDir, 0o0755)
-	if err != nil {
-		return nil, fmt.Errorf("data directory %s is not accessible: %w", svcCfg.DataDir, err)
-	}
-
 	// Create instance to pass it to modules.
-	instance := &Instance{
-		binDir:  svcCfg.BinDir,
-		dataDir: svcCfg.DataDir,
-	}
+	instance := &Instance{}
 	instance.ctx, instance.cancelCtx = context.WithCancel(context.Background())
-	instance.shutdownCtx, instance.cancelShutdownCtx = context.WithCancel(context.Background())
+
+	var err error
 
 	// Base modules
 	instance.base, err = base.New(instance)
@@ -171,15 +147,7 @@ func New(svcCfg *ServiceConfig) (*Instance, error) { //nolint:maintidx
 	if err != nil {
 		return instance, fmt.Errorf("create core module: %w", err)
 	}
-	binaryUpdateConfig, intelUpdateConfig, err := MakeUpdateConfigs(svcCfg)
-	if err != nil {
-		return instance, fmt.Errorf("create updates config: %w", err)
-	}
-	instance.binaryUpdates, err = updates.New(instance, "Binary Updater", *binaryUpdateConfig)
-	if err != nil {
-		return instance, fmt.Errorf("create updates module: %w", err)
-	}
-	instance.intelUpdates, err = updates.New(instance, "Intel Updater", *intelUpdateConfig)
+	instance.updates, err = updates.New(instance)
 	if err != nil {
 		return instance, fmt.Errorf("create updates module: %w", err)
 	}
@@ -223,7 +191,6 @@ func New(svcCfg *ServiceConfig) (*Instance, error) { //nolint:maintidx
 	if err != nil {
 		return instance, fmt.Errorf("create customlist module: %w", err)
 	}
-
 	instance.status, err = status.New(instance)
 	if err != nil {
 		return instance, fmt.Errorf("create status module: %w", err)
@@ -307,8 +274,7 @@ func New(svcCfg *ServiceConfig) (*Instance, error) { //nolint:maintidx
 		instance.notifications,
 
 		instance.core,
-		instance.binaryUpdates,
-		instance.intelUpdates,
+		instance.updates,
 		instance.geoip,
 		instance.netenv,
 
@@ -367,18 +333,6 @@ func (i *Instance) SetSleep(enabled bool) {
 	}
 }
 
-// BinDir returns the directory for binaries.
-// This directory may be read-only.
-func (i *Instance) BinDir() string {
-	return i.binDir
-}
-
-// DataDir returns the directory for variable data.
-// This directory is expected to be read/writeable.
-func (i *Instance) DataDir() string {
-	return i.dataDir
-}
-
 // Database returns the database module.
 func (i *Instance) Database() *dbmodule.DBModule {
 	return i.database
@@ -419,14 +373,9 @@ func (i *Instance) Base() *base.Base {
 	return i.base
 }
 
-// BinaryUpdates returns the updates module.
-func (i *Instance) BinaryUpdates() *updates.Updater {
-	return i.binaryUpdates
-}
-
-// IntelUpdates returns the updates module.
-func (i *Instance) IntelUpdates() *updates.Updater {
-	return i.intelUpdates
+// Updates returns the updates module.
+func (i *Instance) Updates() *updates.Updates {
+	return i.updates
 }
 
 // GeoIP returns the geoip module.
@@ -612,13 +561,20 @@ func (i *Instance) Ready() bool {
 	return i.serviceGroup.Ready()
 }
 
-// Start starts the instance modules.
+// Ctx returns the instance context.
+// It is only canceled on shutdown.
+func (i *Instance) Ctx() context.Context {
+	return i.ctx
+}
+
+// Start starts the instance.
 func (i *Instance) Start() error {
 	return i.serviceGroup.Start()
 }
 
-// Stop stops the instance modules.
+// Stop stops the instance and cancels the instance context when done.
 func (i *Instance) Stop() error {
+	defer i.cancelCtx()
 	return i.serviceGroup.Stop()
 }
 
@@ -632,8 +588,6 @@ func (i *Instance) Restart() {
 	i.core.EventRestart.Submit(struct{}{})
 	time.Sleep(10 * time.Millisecond)
 
-	// Set the restart flag and shutdown.
-	i.ShouldRestart = true
 	i.shutdown(RestartExitCode)
 }
 
@@ -647,63 +601,32 @@ func (i *Instance) Shutdown() {
 }
 
 func (i *Instance) shutdown(exitCode int) {
-	// Only shutdown once.
-	if i.IsShuttingDown() {
-		return
-	}
-
-	// Cancel main  context.
-	i.cancelCtx()
-
 	// Set given exit code.
 	i.exitCode.Store(int32(exitCode))
 
-	// Start shutdown asynchronously in a separate manager.
 	m := mgr.New("instance")
 	m.Go("shutdown", func(w *mgr.WorkerCtx) error {
-		// Stop all modules.
-		if err := i.Stop(); err != nil {
-			w.Error("failed to shutdown", "err", err)
+		for {
+			if err := i.Stop(); err != nil {
+				w.Error("failed to shutdown", "err", err, "retry", "1s")
+				time.Sleep(1 * time.Second)
+			} else {
+				return nil
+			}
 		}
-
-		// Cancel shutdown process context.
-		i.cancelShutdownCtx()
-		return nil
 	})
 }
 
-// Ctx returns the instance context.
-// It is canceled when shutdown is started.
-func (i *Instance) Ctx() context.Context {
-	return i.ctx
-}
-
-// IsShuttingDown returns whether the instance is shutting down.
-func (i *Instance) IsShuttingDown() bool {
+// Stopping returns whether the instance is shutting down.
+func (i *Instance) Stopping() bool {
 	return i.ctx.Err() != nil
 }
 
-// ShuttingDown returns a channel that is triggered when the instance starts shutting down.
-func (i *Instance) ShuttingDown() <-chan struct{} {
+// Stopped returns a channel that is triggered when the instance has shut down.
+func (i *Instance) Stopped() <-chan struct{} {
 	return i.ctx.Done()
 }
 
-// ShutdownCtx returns the instance shutdown context.
-// It is canceled when shutdown is complete.
-func (i *Instance) ShutdownCtx() context.Context {
-	return i.shutdownCtx
-}
-
-// IsShutDown returns whether the instance has stopped.
-func (i *Instance) IsShutDown() bool {
-	return i.shutdownCtx.Err() != nil
-}
-
-// ShutDownComplete returns a channel that is triggered when the instance has shut down.
-func (i *Instance) ShutdownComplete() <-chan struct{} {
-	return i.shutdownCtx.Done()
-}
-
 // ExitCode returns the set exit code of the instance.
 func (i *Instance) ExitCode() int {
 	return int(i.exitCode.Load())
diff --git a/service/intel/filterlists/database.go b/service/intel/filterlists/database.go
index 4d5c5c93..5f55323c 100644
--- a/service/intel/filterlists/database.go
+++ b/service/intel/filterlists/database.go
@@ -14,14 +14,15 @@ import (
 	"github.com/safing/portmaster/base/database"
 	"github.com/safing/portmaster/base/database/record"
 	"github.com/safing/portmaster/base/log"
+	"github.com/safing/portmaster/base/updater"
 	"github.com/safing/portmaster/service/updates"
 )
 
 const (
-	baseListFilePath         = "base.dsdl"
-	intermediateListFilePath = "intermediate.dsdl"
-	urgentListFilePath       = "urgent.dsdl"
-	listIndexFilePath        = "index.dsd"
+	baseListFilePath         = "intel/lists/base.dsdl"
+	intermediateListFilePath = "intel/lists/intermediate.dsdl"
+	urgentListFilePath       = "intel/lists/urgent.dsdl"
+	listIndexFilePath        = "intel/lists/index.dsd"
 )
 
 // default bloomfilter element sizes (estimated).
@@ -39,9 +40,9 @@ var (
 	filterListLock sync.RWMutex
 
 	// Updater files for tracking upgrades.
-	baseFile         *updates.Artifact
-	intermediateFile *updates.Artifact
-	urgentFile       *updates.Artifact
+	baseFile         *updater.File
+	intermediateFile *updater.File
+	urgentFile       *updater.File
 
 	filterListsLoaded chan struct{}
 )
@@ -55,10 +56,11 @@ var cache = database.NewInterface(&database.Options{
 // getFileFunc is the function used to get a file from
 // the updater. It's basically updates.GetFile and used
 // for unit testing.
+type getFileFunc func(string) (*updater.File, error)
 
 // getFile points to updates.GetFile but may be set to
 // something different during unit testing.
-// var getFile getFileFunc = registry.GetFile
+var getFile getFileFunc = updates.GetFile
 
 func init() {
 	filterListsLoaded = make(chan struct{})
@@ -77,7 +79,7 @@ func isLoaded() bool {
 
 // processListFile opens the latest version of file and decodes it's DSDL
 // content. It calls processEntry for each decoded filterlists entry.
-func processListFile(ctx context.Context, filter *scopedBloom, file *updates.Artifact) error {
+func processListFile(ctx context.Context, filter *scopedBloom, file *updater.File) error {
 	f, err := os.Open(file.Path())
 	if err != nil {
 		return err
diff --git a/service/intel/filterlists/index.go b/service/intel/filterlists/index.go
index 9ba0ec17..4b59adde 100644
--- a/service/intel/filterlists/index.go
+++ b/service/intel/filterlists/index.go
@@ -4,11 +4,13 @@ import (
 	"errors"
 	"fmt"
 	"os"
+	"strings"
 	"sync"
 
 	"github.com/safing/portmaster/base/database"
 	"github.com/safing/portmaster/base/database/record"
 	"github.com/safing/portmaster/base/log"
+	"github.com/safing/portmaster/base/updater"
 	"github.com/safing/portmaster/service/updates"
 	"github.com/safing/structures/dsd"
 )
@@ -162,7 +164,7 @@ func getListIndexFromCache() (*ListIndexFile, error) {
 
 var (
 	// listIndexUpdate must only be used by updateListIndex.
-	listIndexUpdate     *updates.Artifact
+	listIndexUpdate     *updater.File
 	listIndexUpdateLock sync.Mutex
 )
 
@@ -175,24 +177,24 @@ func updateListIndex() error {
 	case listIndexUpdate == nil:
 		// This is the first time this function is run, get updater file for index.
 		var err error
-		listIndexUpdate, err = module.instance.IntelUpdates().GetFile(listIndexFilePath)
+		listIndexUpdate, err = updates.GetFile(listIndexFilePath)
 		if err != nil {
 			return err
 		}
 
 		// Check if the version in the cache is current.
-		_, err = getListIndexFromCache()
+		index, err := getListIndexFromCache()
 		switch {
 		case errors.Is(err, database.ErrNotFound):
 			log.Info("filterlists: index not in cache, starting update")
 		case err != nil:
 			log.Warningf("filterlists: failed to load index from cache, starting update: %s", err)
-		// case !listIndexUpdate.EqualsVersion(strings.TrimPrefix(index.Version, "v")):
-		// 	log.Infof(
-		// 		"filterlists: index from cache is outdated, starting update (%s != %s)",
-		// 		strings.TrimPrefix(index.Version, "v"),
-		// 		listIndexUpdate.Version(),
-		// 	)
+		case !listIndexUpdate.EqualsVersion(strings.TrimPrefix(index.Version, "v")):
+			log.Infof(
+				"filterlists: index from cache is outdated, starting update (%s != %s)",
+				strings.TrimPrefix(index.Version, "v"),
+				listIndexUpdate.Version(),
+			)
 		default:
 			// List is in cache and current, there is nothing to do.
 			log.Debug("filterlists: index is up to date")
@@ -202,8 +204,8 @@ func updateListIndex() error {
 
 			return nil
 		}
-	// case listIndexUpdate.UpgradeAvailable():
-	// 	log.Info("filterlists: index update available, starting update")
+	case listIndexUpdate.UpgradeAvailable():
+		log.Info("filterlists: index update available, starting update")
 	default:
 		// Index is loaded and no update is available, there is nothing to do.
 		return nil
diff --git a/service/intel/filterlists/module.go b/service/intel/filterlists/module.go
index fd780890..92f6576e 100644
--- a/service/intel/filterlists/module.go
+++ b/service/intel/filterlists/module.go
@@ -57,12 +57,11 @@ func init() {
 }
 
 func prep() error {
-	module.instance.IntelUpdates().EventResourcesUpdated.AddCallback("Check for blocklist updates",
+	module.instance.Updates().EventResourcesUpdated.AddCallback("Check for blocklist updates",
 		func(wc *mgr.WorkerCtx, s struct{}) (bool, error) {
 			if ignoreUpdateEvents.IsSet() {
 				return false, nil
 			}
-			log.Debugf("performing filter list update")
 
 			return false, tryListUpdate(wc.Ctx())
 		})
@@ -142,6 +141,6 @@ func New(instance instance) (*FilterLists, error) {
 }
 
 type instance interface {
-	IntelUpdates() *updates.Updater
+	Updates() *updates.Updates
 	NetEnv() *netenv.NetEnv
 }
diff --git a/service/intel/filterlists/updater.go b/service/intel/filterlists/updater.go
index cd7e9200..72f7b82e 100644
--- a/service/intel/filterlists/updater.go
+++ b/service/intel/filterlists/updater.go
@@ -13,8 +13,8 @@ import (
 	"github.com/safing/portmaster/base/database"
 	"github.com/safing/portmaster/base/database/query"
 	"github.com/safing/portmaster/base/log"
+	"github.com/safing/portmaster/base/updater"
 	"github.com/safing/portmaster/service/mgr"
-	"github.com/safing/portmaster/service/updates"
 )
 
 var updateInProgress = abool.New()
@@ -63,7 +63,7 @@ func performUpdate(ctx context.Context) error {
 	// First, update the list index.
 	err := updateListIndex()
 	if err != nil {
-		log.Warningf("intel/filterlists: failed update list index: %s", err)
+		log.Errorf("intel/filterlists: failed update list index: %s", err)
 	}
 
 	upgradables, err := getUpgradableFiles()
@@ -83,7 +83,7 @@ func performUpdate(ctx context.Context) error {
 	// perform the actual upgrade by processing each file
 	// in the returned order.
 	for idx, file := range upgradables {
-		log.Debugf("intel/filterlists: applying update (%d) %s version %s", idx, file.Filename, file.Version)
+		log.Debugf("intel/filterlists: applying update (%d) %s version %s", idx, file.Identifier(), file.Version())
 
 		if file == baseFile {
 			if idx != 0 {
@@ -101,7 +101,7 @@ func performUpdate(ctx context.Context) error {
 		}
 
 		if err := processListFile(ctx, filterToUpdate, file); err != nil {
-			return fmt.Errorf("failed to process upgrade %s version %s: %w", file.Filename, file.Version, err)
+			return fmt.Errorf("failed to process upgrade %s: %w", file.Identifier(), err)
 		}
 	}
 
@@ -145,10 +145,10 @@ func performUpdate(ctx context.Context) error {
 
 	// try to save the highest version of our files.
 	highestVersion := upgradables[len(upgradables)-1]
-	if err := setCacheDatabaseVersion(highestVersion.Version); err != nil {
+	if err := setCacheDatabaseVersion(highestVersion.Version()); err != nil {
 		log.Errorf("intel/filterlists: failed to save cache database version: %s", err)
 	} else {
-		log.Infof("intel/filterlists: successfully migrated cache database to %s", highestVersion.Version)
+		log.Infof("intel/filterlists: successfully migrated cache database to %s", highestVersion.Version())
 	}
 
 	// The list update succeeded, resolve any states.
@@ -174,51 +174,51 @@ func removeAllObsoleteFilterEntries(wc *mgr.WorkerCtx) error {
 // getUpgradableFiles returns a slice of filterlists files
 // that should be updated. The files MUST be updated and
 // processed in the returned order!
-func getUpgradableFiles() ([]*updates.Artifact, error) {
-	var updateOrder []*updates.Artifact
+func getUpgradableFiles() ([]*updater.File, error) {
+	var updateOrder []*updater.File
 
 	cacheDBInUse := isLoaded()
 
-	newBaseFile, err := module.instance.IntelUpdates().GetFile(baseListFilePath)
-	if err != nil {
-		log.Warningf("intel/filterlists: failed to get base update: %s", err)
-	} else if newer, _ := newBaseFile.IsNewerThan(baseFile); newer || !cacheDBInUse {
-		log.Tracef("intel/filterlists: base file needs update to version %s", newBaseFile.Version)
-		if newBaseFile.SemVer() == nil {
-			log.Warningf("intel/filterlists: base file needs update to version %s, but semver is invalid", newBaseFile.Version)
-		} else {
-			updateOrder = append(updateOrder, newBaseFile)
+	if baseFile == nil || baseFile.UpgradeAvailable() || !cacheDBInUse {
+		var err error
+		baseFile, err = getFile(baseListFilePath)
+		if err != nil {
+			return nil, err
+		}
+		log.Tracef("intel/filterlists: base file needs update, selected version %s", baseFile.Version())
+		updateOrder = append(updateOrder, baseFile)
+	}
+
+	if intermediateFile == nil || intermediateFile.UpgradeAvailable() || !cacheDBInUse {
+		var err error
+		intermediateFile, err = getFile(intermediateListFilePath)
+		if err != nil && !errors.Is(err, updater.ErrNotFound) {
+			return nil, err
+		}
+
+		if err == nil {
+			log.Tracef("intel/filterlists: intermediate file needs update, selected version %s", intermediateFile.Version())
+			updateOrder = append(updateOrder, intermediateFile)
 		}
 	}
 
-	newIntermediateFile, err := module.instance.IntelUpdates().GetFile(intermediateListFilePath)
-	if err != nil {
-		log.Warningf("intel/filterlists: failed to get intermediate update: %s", err)
-	} else if newer, _ := newIntermediateFile.IsNewerThan(intermediateFile); newer || !cacheDBInUse {
-		log.Tracef("intel/filterlists: intermediate file needs update to version %s", newIntermediateFile.Version)
-		if newIntermediateFile.SemVer() == nil {
-			log.Warningf("intel/filterlists: intermediate file needs update to version %s, but semver is invalid", newIntermediateFile.Version)
-		} else {
-			updateOrder = append(updateOrder, newIntermediateFile)
+	if urgentFile == nil || urgentFile.UpgradeAvailable() || !cacheDBInUse {
+		var err error
+		urgentFile, err = getFile(urgentListFilePath)
+		if err != nil && !errors.Is(err, updater.ErrNotFound) {
+			return nil, err
 		}
-	}
 
-	newUrgentFile, err := module.instance.IntelUpdates().GetFile(urgentListFilePath)
-	if err != nil {
-		log.Warningf("intel/filterlists: failed to get urgent update: %s", err)
-	} else if newer, _ := newUrgentFile.IsNewerThan(urgentFile); newer || !cacheDBInUse {
-		log.Tracef("intel/filterlists: urgent file needs update to version %s", newUrgentFile.Version)
-		if newUrgentFile.SemVer() == nil {
-			log.Warningf("intel/filterlists: urgent file needs update to version %s, but semver is invalid", newUrgentFile.Version)
-		} else {
-			updateOrder = append(updateOrder, newUrgentFile)
+		if err == nil {
+			log.Tracef("intel/filterlists: urgent file needs update, selected version %s", urgentFile.Version())
+			updateOrder = append(updateOrder, urgentFile)
 		}
 	}
 
 	return resolveUpdateOrder(updateOrder)
 }
 
-func resolveUpdateOrder(updateOrder []*updates.Artifact) ([]*updates.Artifact, error) {
+func resolveUpdateOrder(updateOrder []*updater.File) ([]*updater.File, error) {
 	// sort the update order by ascending version
 	sort.Sort(byAscVersion(updateOrder))
 	log.Tracef("intel/filterlists: order of updates: %v", updateOrder)
@@ -239,8 +239,9 @@ func resolveUpdateOrder(updateOrder []*updates.Artifact) ([]*updates.Artifact, e
 
 	startAtIdx := -1
 	for idx, file := range updateOrder {
-		log.Tracef("intel/filterlists: checking file with version %s against %s", file.SemVer(), cacheDBVersion)
-		if file.SemVer().GreaterThan(cacheDBVersion) && (startAtIdx == -1 || file == baseFile) {
+		ver, _ := version.NewSemver(file.Version())
+		log.Tracef("intel/filterlists: checking file with version %s against %s", ver, cacheDBVersion)
+		if ver.GreaterThan(cacheDBVersion) && (startAtIdx == -1 || file == baseFile) {
 			startAtIdx = idx
 		}
 	}
@@ -257,12 +258,15 @@ func resolveUpdateOrder(updateOrder []*updates.Artifact) ([]*updates.Artifact, e
 	return updateOrder[startAtIdx:], nil
 }
 
-type byAscVersion []*updates.Artifact
+type byAscVersion []*updater.File
 
 func (fs byAscVersion) Len() int { return len(fs) }
 
 func (fs byAscVersion) Less(i, j int) bool {
-	return fs[i].SemVer().LessThan(fs[j].SemVer())
+	vi, _ := version.NewSemver(fs[i].Version())
+	vj, _ := version.NewSemver(fs[j].Version())
+
+	return vi.LessThan(vj)
 }
 
 func (fs byAscVersion) Swap(i, j int) {
diff --git a/service/intel/geoip/database.go b/service/intel/geoip/database.go
index 711fafdf..5f0258a7 100644
--- a/service/intel/geoip/database.go
+++ b/service/intel/geoip/database.go
@@ -8,6 +8,7 @@ import (
 	maxminddb "github.com/oschwald/maxminddb-golang"
 
 	"github.com/safing/portmaster/base/log"
+	"github.com/safing/portmaster/base/updater"
 	"github.com/safing/portmaster/service/mgr"
 	"github.com/safing/portmaster/service/updates"
 )
@@ -17,66 +18,36 @@ var worker *updateWorker
 func init() {
 	worker = &updateWorker{
 		trigger: make(chan struct{}),
-		v4: updateBroadcaster{
-			dbName: v4MMDBResource,
-		},
-		v6: updateBroadcaster{
-			dbName: v6MMDBResource,
-		},
 	}
 }
 
 const (
-	v4MMDBResource = "geoipv4.mmdb"
-	v6MMDBResource = "geoipv6.mmdb"
+	v4MMDBResource = "intel/geoip/geoipv4.mmdb.gz"
+	v6MMDBResource = "intel/geoip/geoipv6.mmdb.gz"
 )
 
 type geoIPDB struct {
 	*maxminddb.Reader
-	update *updates.Artifact
+	file *updater.File
 }
 
 // updateBroadcaster stores a geoIPDB and provides synchronized
 // access to the MMDB reader. It also supports broadcasting to
 // multiple waiters when a new database becomes available.
 type updateBroadcaster struct {
-	rw     sync.RWMutex
-	db     *geoIPDB
-	dbName string
+	rw sync.RWMutex
+	db *geoIPDB
 
 	waiter chan struct{}
 }
 
-// AvailableUpdate returns a new update artifact if the current broadcaster
-// needs a database update.
-func (ub *updateBroadcaster) AvailableUpdate() *updates.Artifact {
+// NeedsUpdate returns true if the current broadcaster needs a
+// database update.
+func (ub *updateBroadcaster) NeedsUpdate() bool {
 	ub.rw.RLock()
 	defer ub.rw.RUnlock()
 
-	// Get artifact.
-	artifact, err := module.instance.IntelUpdates().GetFile(ub.dbName)
-	if err != nil {
-		// Check if the geoip database is included in the binary index instead.
-		// TODO: Remove when intelhub builds the geoip database.
-		if artifact2, err2 := module.instance.BinaryUpdates().GetFile(ub.dbName); err2 == nil {
-			artifact = artifact2
-			err = nil
-		} else {
-			log.Warningf("geoip: failed to get geoip update: %s", err)
-			return nil
-		}
-	}
-
-	// Return artifact if not yet initialized.
-	if ub.db == nil {
-		return artifact
-	}
-
-	// Compare and return artifact only when confirmed newer.
-	if newer, _ := artifact.IsNewerThan(ub.db.update); newer {
-		return artifact
-	}
-	return nil
+	return ub.db == nil || ub.db.file.UpgradeAvailable()
 }
 
 // ReplaceDatabase replaces (or initially sets) the mmdb database.
@@ -183,18 +154,16 @@ func (upd *updateWorker) start() {
 
 func (upd *updateWorker) run(ctx *mgr.WorkerCtx) error {
 	for {
-		update := upd.v4.AvailableUpdate()
-		if update != nil {
-			if v4, err := getGeoIPDB(update); err == nil {
+		if upd.v4.NeedsUpdate() {
+			if v4, err := getGeoIPDB(v4MMDBResource); err == nil {
 				upd.v4.ReplaceDatabase(v4)
 			} else {
 				log.Warningf("geoip: failed to get v4 database: %s", err)
 			}
 		}
 
-		update = upd.v6.AvailableUpdate()
-		if update != nil {
-			if v6, err := getGeoIPDB(update); err == nil {
+		if upd.v6.NeedsUpdate() {
+			if v6, err := getGeoIPDB(v6MMDBResource); err == nil {
 				upd.v6.ReplaceDatabase(v6)
 			} else {
 				log.Warningf("geoip: failed to get v6 database: %s", err)
@@ -209,17 +178,36 @@ func (upd *updateWorker) run(ctx *mgr.WorkerCtx) error {
 	}
 }
 
-func getGeoIPDB(update *updates.Artifact) (*geoIPDB, error) {
-	log.Debugf("geoip: opening database %s", update.Path())
+func getGeoIPDB(resource string) (*geoIPDB, error) {
+	log.Debugf("geoip: opening database %s", resource)
 
-	reader, err := maxminddb.Open(update.Path())
+	file, unpackedPath, err := openAndUnpack(resource)
+	if err != nil {
+		return nil, err
+	}
+
+	reader, err := maxminddb.Open(unpackedPath)
 	if err != nil {
 		return nil, fmt.Errorf("failed to open: %w", err)
 	}
-	log.Debugf("geoip: successfully opened database %s", update.Filename)
+	log.Debugf("geoip: successfully opened database %s", resource)
 
 	return &geoIPDB{
 		Reader: reader,
-		update: update,
+		file:   file,
 	}, nil
 }
+
+func openAndUnpack(resource string) (*updater.File, string, error) {
+	f, err := updates.GetFile(resource)
+	if err != nil {
+		return nil, "", fmt.Errorf("getting file: %w", err)
+	}
+
+	unpacked, err := f.Unpack(".gz", updater.UnpackGZIP)
+	if err != nil {
+		return nil, "", fmt.Errorf("unpacking file: %w", err)
+	}
+
+	return f, unpacked, nil
+}
diff --git a/service/intel/geoip/init_test.go b/service/intel/geoip/init_test.go
index 9d0288f2..b6d722dc 100644
--- a/service/intel/geoip/init_test.go
+++ b/service/intel/geoip/init_test.go
@@ -16,12 +16,12 @@ type testInstance struct {
 	db      *dbmodule.DBModule
 	api     *api.API
 	config  *config.Config
-	updates *updates.Updater
+	updates *updates.Updates
 }
 
 var _ instance = &testInstance{}
 
-func (stub *testInstance) IntelUpdates() *updates.Updater {
+func (stub *testInstance) Updates() *updates.Updates {
 	return stub.updates
 }
 
@@ -54,15 +54,6 @@ func runTest(m *testing.M) error {
 		return fmt.Errorf("failed to initialize dataroot: %w", err)
 	}
 	defer func() { _ = os.RemoveAll(ds) }()
-	installDir, err := os.MkdirTemp("", "geoip_installdir")
-	if err != nil {
-		return fmt.Errorf("failed to create tmp install dir: %w", err)
-	}
-	defer func() { _ = os.RemoveAll(installDir) }()
-	err = updates.GenerateMockFolder(installDir, "Test Intel", "1.0.0")
-	if err != nil {
-		return fmt.Errorf("failed to generate mock installation: %w", err)
-	}
 
 	stub := &testInstance{}
 	stub.db, err = dbmodule.New(stub)
@@ -77,10 +68,7 @@ func runTest(m *testing.M) error {
 	if err != nil {
 		return fmt.Errorf("failed to create api: %w", err)
 	}
-	stub.updates, err = updates.New(stub, "Test Intel", updates.Config{
-		Directory: installDir,
-		IndexFile: "index.json",
-	})
+	stub.updates, err = updates.New(stub)
 	if err != nil {
 		return fmt.Errorf("failed to create updates: %w", err)
 	}
diff --git a/service/intel/geoip/module.go b/service/intel/geoip/module.go
index 770d018b..6c2bb55e 100644
--- a/service/intel/geoip/module.go
+++ b/service/intel/geoip/module.go
@@ -19,7 +19,7 @@ func (g *GeoIP) Manager() *mgr.Manager {
 }
 
 func (g *GeoIP) Start() error {
-	module.instance.IntelUpdates().EventResourcesUpdated.AddCallback(
+	module.instance.Updates().EventResourcesUpdated.AddCallback(
 		"Check for GeoIP database updates",
 		func(_ *mgr.WorkerCtx, _ struct{}) (bool, error) {
 			worker.triggerUpdate()
@@ -66,6 +66,5 @@ func New(instance instance) (*GeoIP, error) {
 }
 
 type instance interface {
-	BinaryUpdates() *updates.Updater
-	IntelUpdates() *updates.Updater
+	Updates() *updates.Updates
 }
diff --git a/service/mgr/worker.go b/service/mgr/worker.go
index 2151a2c8..ff06f05a 100644
--- a/service/mgr/worker.go
+++ b/service/mgr/worker.go
@@ -5,12 +5,11 @@ import (
 	"errors"
 	"fmt"
 	"log/slog"
+	"os"
 	"runtime"
 	"runtime/debug"
 	"strings"
 	"time"
-
-	"github.com/safing/portmaster/base/log"
 )
 
 // workerContextKey is a key used for the context key/value storage.
@@ -304,7 +303,7 @@ func (m *Manager) runWorker(w *WorkerCtx, fn func(w *WorkerCtx) error) (panicInf
 			// Print panic to stderr.
 			stackTrace := string(debug.Stack())
 			fmt.Fprintf(
-				log.GlobalWriter,
+				os.Stderr,
 				"===== PANIC =====\n%s\n\n%s=====  END  =====\n",
 				panicVal,
 				stackTrace,
diff --git a/service/netenv/init_test.go b/service/netenv/init_test.go
index 39c0febd..b747111b 100644
--- a/service/netenv/init_test.go
+++ b/service/netenv/init_test.go
@@ -16,12 +16,12 @@ type testInstance struct {
 	db      *dbmodule.DBModule
 	api     *api.API
 	config  *config.Config
-	updates *updates.Updater
+	updates *updates.Updates
 }
 
 var _ instance = &testInstance{}
 
-func (stub *testInstance) IntelUpdates() *updates.Updater {
+func (stub *testInstance) Updates() *updates.Updates {
 	return stub.updates
 }
 
@@ -54,15 +54,6 @@ func runTest(m *testing.M) error {
 		return fmt.Errorf("failed to initialize dataroot: %w", err)
 	}
 	defer func() { _ = os.RemoveAll(ds) }()
-	installDir, err := os.MkdirTemp("", "netenv_installdir")
-	if err != nil {
-		return fmt.Errorf("failed to create tmp install dir: %w", err)
-	}
-	defer func() { _ = os.RemoveAll(installDir) }()
-	err = updates.GenerateMockFolder(installDir, "Test Intel", "1.0.0")
-	if err != nil {
-		return fmt.Errorf("failed to generate mock installation: %w", err)
-	}
 
 	stub := &testInstance{}
 	stub.db, err = dbmodule.New(stub)
@@ -77,10 +68,7 @@ func runTest(m *testing.M) error {
 	if err != nil {
 		return fmt.Errorf("failed to create api: %w", err)
 	}
-	stub.updates, err = updates.New(stub, "Test Intel", updates.Config{
-		Directory: installDir,
-		IndexFile: "index.json",
-	})
+	stub.updates, err = updates.New(stub)
 	if err != nil {
 		return fmt.Errorf("failed to create updates: %w", err)
 	}
diff --git a/service/netenv/main.go b/service/netenv/main.go
index ed985c4b..e1a68150 100644
--- a/service/netenv/main.go
+++ b/service/netenv/main.go
@@ -8,7 +8,6 @@ import (
 
 	"github.com/safing/portmaster/base/log"
 	"github.com/safing/portmaster/service/mgr"
-	"github.com/safing/portmaster/service/updates"
 )
 
 // Event Names.
@@ -106,6 +105,4 @@ func New(instance instance) (*NetEnv, error) {
 	return module, nil
 }
 
-type instance interface {
-	IntelUpdates() *updates.Updater
-}
+type instance interface{}
diff --git a/service/netenv/online-status.go b/service/netenv/online-status.go
index 1d5feeaf..554fc004 100644
--- a/service/netenv/online-status.go
+++ b/service/netenv/online-status.go
@@ -17,6 +17,7 @@ import (
 	"github.com/safing/portmaster/base/notifications"
 	"github.com/safing/portmaster/service/mgr"
 	"github.com/safing/portmaster/service/network/netutils"
+	"github.com/safing/portmaster/service/updates"
 )
 
 // OnlineStatus represent a state of connectivity to the Internet.
@@ -220,7 +221,7 @@ func updateOnlineStatus(status OnlineStatus, portalURL *url.URL, comment string)
 
 		// Trigger update check when coming (semi) online.
 		if Online() {
-			module.instance.IntelUpdates().EventResourcesUpdated.Submit(struct{}{})
+			_ = updates.TriggerUpdate(false, false)
 		}
 	}
 }
diff --git a/service/netquery/database.go b/service/netquery/database.go
index d66e3222..a1cd6aea 100644
--- a/service/netquery/database.go
+++ b/service/netquery/database.go
@@ -5,7 +5,6 @@ import (
 	"encoding/json"
 	"fmt"
 	"io"
-	"os"
 	"path/filepath"
 	"sort"
 	"strings"
@@ -18,6 +17,7 @@ import (
 	"zombiezen.com/go/sqlite/sqlitex"
 
 	"github.com/safing/portmaster/base/config"
+	"github.com/safing/portmaster/base/dataroot"
 	"github.com/safing/portmaster/base/log"
 	"github.com/safing/portmaster/service/netquery/orm"
 	"github.com/safing/portmaster/service/network"
@@ -127,13 +127,13 @@ type (
 // Note that write connections are serialized by the Database object before being
 // handed over to SQLite.
 func New(dbPath string) (*Database, error) {
-	historyParentDir := filepath.Join(module.instance.DataDir(), "databases")
-	if err := os.MkdirAll(historyParentDir, 0o0700); err != nil {
+	historyParentDir := dataroot.Root().ChildDir("databases", 0o700)
+	if err := historyParentDir.Ensure(); err != nil {
 		return nil, fmt.Errorf("failed to ensure database directory exists: %w", err)
 	}
 
 	// Get file location of history database.
-	historyFile := filepath.Join(historyParentDir, "history.db")
+	historyFile := filepath.Join(historyParentDir.Path, "history.db")
 	// Convert to SQLite URI path.
 	historyURI := "file:///" + strings.TrimPrefix(filepath.ToSlash(historyFile), "/")
 
@@ -225,13 +225,13 @@ func (db *Database) Close() error {
 
 // VacuumHistory rewrites the history database in order to purge deleted records.
 func VacuumHistory(ctx context.Context) (err error) {
-	historyParentDir := filepath.Join(module.instance.DataDir(), "databases")
-	if err := os.MkdirAll(historyParentDir, 0o0700); err != nil {
+	historyParentDir := dataroot.Root().ChildDir("databases", 0o700)
+	if err := historyParentDir.Ensure(); err != nil {
 		return fmt.Errorf("failed to ensure database directory exists: %w", err)
 	}
 
 	// Get file location of history database.
-	historyFile := filepath.Join(historyParentDir, "history.db")
+	historyFile := filepath.Join(historyParentDir.Path, "history.db")
 	// Convert to SQLite URI path.
 	historyURI := "file:///" + strings.TrimPrefix(filepath.ToSlash(historyFile), "/")
 
diff --git a/service/netquery/module_api.go b/service/netquery/module_api.go
index 5beb9044..497226b0 100644
--- a/service/netquery/module_api.go
+++ b/service/netquery/module_api.go
@@ -310,6 +310,5 @@ func NewModule(instance instance) (*NetQuery, error) {
 }
 
 type instance interface {
-	DataDir() string
 	Profile() *profile.ProfileModule
 }
diff --git a/service/network/api.go b/service/network/api.go
index 8af6eb26..82b11ad0 100644
--- a/service/network/api.go
+++ b/service/network/api.go
@@ -16,6 +16,7 @@ import (
 	"github.com/safing/portmaster/service/process"
 	"github.com/safing/portmaster/service/resolver"
 	"github.com/safing/portmaster/service/status"
+	"github.com/safing/portmaster/service/updates"
 )
 
 func registerAPIEndpoints() error {
@@ -93,7 +94,7 @@ func debugInfo(ar *api.Request) (data []byte, err error) {
 	config.AddToDebugInfo(di)
 
 	// Detailed information.
-	// TODO(vladimir): updates.AddToDebugInfo(di)
+	updates.AddToDebugInfo(di)
 	// compat.AddToDebugInfo(di) // TODO: Cannot use due to interception import requirement which we don't want for SPN Hubs.
 	di.AddGoroutineStack()
 
diff --git a/service/process/module.go b/service/process/module.go
index 624b0ba4..563368ab 100644
--- a/service/process/module.go
+++ b/service/process/module.go
@@ -2,9 +2,9 @@ package process
 
 import (
 	"errors"
+	"os"
 	"sync/atomic"
 
-	"github.com/safing/portmaster/base/log"
 	"github.com/safing/portmaster/service/mgr"
 	"github.com/safing/portmaster/service/updates"
 )
@@ -12,8 +12,6 @@ import (
 type ProcessModule struct {
 	mgr      *mgr.Manager
 	instance instance
-
-	portmasterUIPath string
 }
 
 func (pm *ProcessModule) Manager() *mgr.Manager {
@@ -21,11 +19,9 @@ func (pm *ProcessModule) Manager() *mgr.Manager {
 }
 
 func (pm *ProcessModule) Start() error {
-	file, err := pm.instance.BinaryUpdates().GetFile("portmaster")
-	if err != nil {
-		log.Errorf("process: failed to get path of ui: %s", err)
-	} else {
-		pm.portmasterUIPath = file.Path()
+	updatesPath = updates.RootPath()
+	if updatesPath != "" {
+		updatesPath += string(os.PathSeparator)
 	}
 	return nil
 }
@@ -34,6 +30,8 @@ func (pm *ProcessModule) Stop() error {
 	return nil
 }
 
+var updatesPath string
+
 func prep() error {
 	if err := registerConfiguration(); err != nil {
 		return err
@@ -69,6 +67,4 @@ func New(instance instance) (*ProcessModule, error) {
 	return module, nil
 }
 
-type instance interface {
-	BinaryUpdates() *updates.Updater
-}
+type instance interface{}
diff --git a/service/process/process.go b/service/process/process.go
index 4e0eeeae..60dac7eb 100644
--- a/service/process/process.go
+++ b/service/process/process.go
@@ -256,7 +256,7 @@ func loadProcess(ctx context.Context, key string, pInfo *processInfo.Process) (*
 	// Username
 	process.UserName, err = pInfo.UsernameWithContext(ctx)
 	if err != nil {
-		log.Tracer(ctx).Warningf("process: failed to get username (PID %d): %s", pInfo.Pid, err)
+		return nil, fmt.Errorf("process: failed to get Username for p%d: %w", pInfo.Pid, err)
 	}
 
 	// TODO: User Home
diff --git a/service/process/profile.go b/service/process/profile.go
index e97b1d01..e8c766ee 100644
--- a/service/process/profile.go
+++ b/service/process/profile.go
@@ -72,8 +72,18 @@ func (p *Process) getSpecialProfileID() (specialProfileID string) {
 		specialProfileID = profile.PortmasterProfileID
 	default:
 		// Check if this is another Portmaster component.
-		if module.portmasterUIPath != "" && p.Path == module.portmasterUIPath {
-			specialProfileID = profile.PortmasterAppProfileID
+		if updatesPath != "" && strings.HasPrefix(p.Path, updatesPath) {
+			switch {
+			case strings.Contains(p.Path, "portmaster-app"):
+				specialProfileID = profile.PortmasterAppProfileID
+			case strings.Contains(p.Path, "portmaster-notifier"):
+				specialProfileID = profile.PortmasterNotifierProfileID
+			default:
+				// Unexpected binary from within the Portmaster updates directpry.
+				log.Warningf("process: unexpected binary in the updates directory: %s", p.Path)
+				// TODO: Assign a fully restricted profile in the future when we are
+				// sure that we won't kill any of our own things.
+			}
 		}
 		// Check if this is the system resolver.
 		switch runtime.GOOS {
diff --git a/service/profile/endpoints/endpoints_test.go b/service/profile/endpoints/endpoints_test.go
index 7f29cedc..bbc81f6a 100644
--- a/service/profile/endpoints/endpoints_test.go
+++ b/service/profile/endpoints/endpoints_test.go
@@ -23,11 +23,11 @@ type testInstance struct {
 	db      *dbmodule.DBModule
 	api     *api.API
 	config  *config.Config
-	updates *updates.Updater
+	updates *updates.Updates
 	geoip   *geoip.GeoIP
 }
 
-func (stub *testInstance) IntelUpdates() *updates.Updater {
+func (stub *testInstance) Updates() *updates.Updates {
 	return stub.updates
 }
 
@@ -61,16 +61,6 @@ func runTest(m *testing.M) error {
 	}
 	defer func() { _ = os.RemoveAll(ds) }()
 
-	installDir, err := os.MkdirTemp("", "endpoints_installdir")
-	if err != nil {
-		return fmt.Errorf("failed to create tmp install dir: %w", err)
-	}
-	defer func() { _ = os.RemoveAll(installDir) }()
-	err = updates.GenerateMockFolder(installDir, "Test Intel", "1.0.0")
-	if err != nil {
-		return fmt.Errorf("failed to generate mock installation: %w", err)
-	}
-
 	stub := &testInstance{}
 	stub.db, err = dbmodule.New(stub)
 	if err != nil {
@@ -84,10 +74,7 @@ func runTest(m *testing.M) error {
 	if err != nil {
 		return fmt.Errorf("failed to create api: %w", err)
 	}
-	stub.updates, err = updates.New(stub, "Test Intel", updates.Config{
-		Directory: installDir,
-		IndexFile: "index.json",
-	})
+	stub.updates, err = updates.New(stub)
 	if err != nil {
 		return fmt.Errorf("failed to create updates: %w", err)
 	}
diff --git a/service/profile/module.go b/service/profile/module.go
index 68845047..911ef99c 100644
--- a/service/profile/module.go
+++ b/service/profile/module.go
@@ -4,19 +4,23 @@ import (
 	"errors"
 	"fmt"
 	"os"
-	"path/filepath"
 	"sync/atomic"
 
 	"github.com/safing/portmaster/base/config"
 	"github.com/safing/portmaster/base/database"
 	"github.com/safing/portmaster/base/database/migration"
+	"github.com/safing/portmaster/base/dataroot"
 	"github.com/safing/portmaster/base/log"
 	_ "github.com/safing/portmaster/service/core/base"
 	"github.com/safing/portmaster/service/mgr"
 	"github.com/safing/portmaster/service/profile/binmeta"
+	"github.com/safing/portmaster/service/updates"
 )
 
-var migrations = migration.New("core:migrations/profile")
+var (
+	migrations  = migration.New("core:migrations/profile")
+	updatesPath string
+)
 
 // Events.
 const (
@@ -66,16 +70,21 @@ func prep() error {
 	}
 
 	// Setup icon storage location.
-	iconsDir := filepath.Join(module.instance.DataDir(), "databases", "icons")
-	if err := os.MkdirAll(iconsDir, 0o0700); err != nil {
+	iconsDir := dataroot.Root().ChildDir("databases", 0o0700).ChildDir("icons", 0o0700)
+	if err := iconsDir.Ensure(); err != nil {
 		return fmt.Errorf("failed to create/check icons directory: %w", err)
 	}
-	binmeta.ProfileIconStoragePath = iconsDir
+	binmeta.ProfileIconStoragePath = iconsDir.Path
 
 	return nil
 }
 
 func start() error {
+	updatesPath = updates.RootPath()
+	if updatesPath != "" {
+		updatesPath += string(os.PathSeparator)
+	}
+
 	if err := loadProfilesMetadata(); err != nil {
 		if !errors.Is(err, database.ErrNotFound) {
 			log.Warningf("profile: failed to load profiles metadata, falling back to empty state: %s", err)
@@ -152,6 +161,5 @@ func NewModule(instance instance) (*ProfileModule, error) {
 }
 
 type instance interface {
-	DataDir() string
 	Config() *config.Config
 }
diff --git a/service/profile/profile.go b/service/profile/profile.go
index 2db0e3a9..97bdca8c 100644
--- a/service/profile/profile.go
+++ b/service/profile/profile.go
@@ -197,7 +197,7 @@ func (profile *Profile) parseConfig() error {
 	if ok {
 		profile.filterListIDs, err = filterlists.ResolveListIDs(list)
 		if err != nil {
-			log.Warningf("profiles: failed to resolve filter list IDs: %s", err)
+			lastErr = err
 		} else {
 			profile.filterListsSet = true
 		}
diff --git a/service/resolver/main_test.go b/service/resolver/main_test.go
index 048ab4b5..4efc8eb8 100644
--- a/service/resolver/main_test.go
+++ b/service/resolver/main_test.go
@@ -22,11 +22,13 @@ type testInstance struct {
 	base    *base.Base
 	api     *api.API
 	config  *config.Config
-	updates *updates.Updater
+	updates *updates.Updates
 	netenv  *netenv.NetEnv
 }
 
-func (stub *testInstance) IntelUpdates() *updates.Updater {
+// var _ instance = &testInstance{}
+
+func (stub *testInstance) Updates() *updates.Updates {
 	return stub.updates
 }
 
@@ -68,16 +70,6 @@ func runTest(m *testing.M) error {
 	}
 	defer func() { _ = os.RemoveAll(ds) }()
 
-	installDir, err := os.MkdirTemp("", "resolver_installdir")
-	if err != nil {
-		return fmt.Errorf("failed to create tmp install dir: %w", err)
-	}
-	defer func() { _ = os.RemoveAll(installDir) }()
-	err = updates.GenerateMockFolder(installDir, "Test Intel", "1.0.0")
-	if err != nil {
-		return fmt.Errorf("failed to generate mock installation: %w", err)
-	}
-
 	stub := &testInstance{}
 	stub.db, err = dbmodule.New(stub)
 	if err != nil {
@@ -99,10 +91,7 @@ func runTest(m *testing.M) error {
 	if err != nil {
 		return fmt.Errorf("failed to create netenv: %w", err)
 	}
-	stub.updates, err = updates.New(stub, "Test Intel", updates.Config{
-		Directory: installDir,
-		IndexFile: "index.json",
-	})
+	stub.updates, err = updates.New(stub)
 	if err != nil {
 		return fmt.Errorf("failed to create updates: %w", err)
 	}
diff --git a/service/ui/module.go b/service/ui/module.go
index 36fa5ac6..630808e5 100644
--- a/service/ui/module.go
+++ b/service/ui/module.go
@@ -2,14 +2,12 @@ package ui
 
 import (
 	"errors"
-	"os"
-	"path/filepath"
 	"sync/atomic"
 
 	"github.com/safing/portmaster/base/api"
+	"github.com/safing/portmaster/base/dataroot"
 	"github.com/safing/portmaster/base/log"
 	"github.com/safing/portmaster/service/mgr"
-	"github.com/safing/portmaster/service/updates"
 )
 
 func prep() error {
@@ -29,8 +27,7 @@ func start() error {
 	// may seem dangerous, but proper permission on the parent directory provide
 	// (some) protection.
 	// Processes must _never_ read from this directory.
-	execDir := filepath.Join(module.instance.DataDir(), "exec")
-	err := os.MkdirAll(execDir, 0o0777) //nolint:gosec // This is intentional.
+	err := dataroot.Root().ChildDir("exec", 0o0777).Ensure()
 	if err != nil {
 		log.Warningf("ui: failed to create safe exec dir: %s", err)
 	}
@@ -59,10 +56,7 @@ func (ui *UI) Stop() error {
 	return nil
 }
 
-var (
-	shimLoaded atomic.Bool
-	module     *UI
-)
+var shimLoaded atomic.Bool
 
 // New returns a new UI module.
 func New(instance instance) (*UI, error) {
@@ -70,7 +64,7 @@ func New(instance instance) (*UI, error) {
 		return nil, errors.New("only one instance allowed")
 	}
 	m := mgr.New("UI")
-	module = &UI{
+	module := &UI{
 		mgr:      m,
 		instance: instance,
 	}
@@ -83,7 +77,5 @@ func New(instance instance) (*UI, error) {
 }
 
 type instance interface {
-	DataDir() string
 	API() *api.API
-	BinaryUpdates() *updates.Updater
 }
diff --git a/service/ui/serve.go b/service/ui/serve.go
index 68203552..9dca6c30 100644
--- a/service/ui/serve.go
+++ b/service/ui/serve.go
@@ -15,6 +15,7 @@ import (
 
 	"github.com/safing/portmaster/base/api"
 	"github.com/safing/portmaster/base/log"
+	"github.com/safing/portmaster/base/updater"
 	"github.com/safing/portmaster/base/utils"
 	"github.com/safing/portmaster/service/updates"
 )
@@ -91,9 +92,9 @@ func (bs *archiveServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 	}
 
 	// get file from update system
-	zipFile, err := module.instance.BinaryUpdates().GetFile(fmt.Sprintf("%s.zip", moduleName))
+	zipFile, err := updates.GetFile(fmt.Sprintf("ui/modules/%s.zip", moduleName))
 	if err != nil {
-		if errors.Is(err, updates.ErrNotFound) {
+		if errors.Is(err, updater.ErrNotFound) {
 			log.Tracef("ui: requested module %s does not exist", moduleName)
 			http.Error(w, err.Error(), http.StatusNotFound)
 		} else {
diff --git a/service/updates.go b/service/updates.go
deleted file mode 100644
index 3c717951..00000000
--- a/service/updates.go
+++ /dev/null
@@ -1,120 +0,0 @@
-package service
-
-import (
-	"path/filepath"
-	go_runtime "runtime"
-
-	"github.com/safing/jess"
-	"github.com/safing/portmaster/service/updates"
-)
-
-var (
-	DefaultStableBinaryIndexURLs = []string{
-		"https://updates.safing.io/stable.v3.json",
-	}
-	DefaultBetaBinaryIndexURLs = []string{
-		"https://updates.safing.io/beta.v3.json",
-	}
-	DefaultStagingBinaryIndexURLs = []string{
-		"https://updates.safing.io/staging.v3.json",
-	}
-	DefaultSupportBinaryIndexURLs = []string{
-		"https://updates.safing.io/support.v3.json",
-	}
-
-	DefaultIntelIndexURLs = []string{
-		"https://updates.safing.io/intel.v3.json",
-	}
-
-	// BinarySigningKeys holds the signing keys in text format.
-	BinarySigningKeys = []string{
-		// Safing Code Signing Key #1
-		"recipient:public-ed25519-key:safing-code-signing-key-1:92bgBLneQUWrhYLPpBDjqHbpFPuNVCPAaivQ951A4aq72HcTiw7R1QmPJwFM1mdePAvEVDjkeb8S4fp2pmRCsRa8HrCvWQEjd88rfZ6TznJMfY4g7P8ioGFjfpyx2ZJ8WCZJG5Qt4Z9nkabhxo2Nbi3iywBTYDLSbP5CXqi7jryW7BufWWuaRVufFFzhwUC2ryWFWMdkUmsAZcvXwde4KLN9FrkWAy61fGaJ8GCwGnGCSitANnU2cQrsGBXZzxmzxwrYD",
-		// Safing Code Signing Key #2
-		"recipient:public-ed25519-key:safing-code-signing-key-2:92bgBLneQUWrhYLPpBDjqHbPC2d1o5JMyZFdavWBNVtdvbPfzDewLW95ScXfYPHd3QvWHSWCtB4xpthaYWxSkK1kYiGp68DPa2HaU8yQ5dZhaAUuV4Kzv42pJcWkCeVnBYqgGBXobuz52rFqhDJy3rz7soXEmYhJEJWwLwMeioK3VzN3QmGSYXXjosHMMNC76rjufSoLNtUQUWZDSnHmqbuxbKMCCsjFXUGGhtZVyb7bnu7QLTLk6SKHBJDMB6zdL9sw3",
-	}
-
-	// BinarySigningTrustStore is an in-memory trust store with the signing keys.
-	BinarySigningTrustStore = jess.NewMemTrustStore()
-)
-
-func init() {
-	for _, signingKey := range BinarySigningKeys {
-		rcpt, err := jess.RecipientFromTextFormat(signingKey)
-		if err != nil {
-			panic(err)
-		}
-		err = BinarySigningTrustStore.StoreSignet(rcpt)
-		if err != nil {
-			panic(err)
-		}
-	}
-}
-
-func MakeUpdateConfigs(svcCfg *ServiceConfig) (binaryUpdateConfig, intelUpdateConfig *updates.Config, err error) {
-	switch go_runtime.GOOS {
-	case "windows":
-		binaryUpdateConfig = &updates.Config{
-			Name:              "binaries",
-			Directory:         svcCfg.BinDir,
-			DownloadDirectory: filepath.Join(svcCfg.DataDir, "download_binaries"),
-			PurgeDirectory:    filepath.Join(svcCfg.BinDir, "upgrade_obsolete_binaries"),
-			Ignore:            []string{"databases", "intel", "config.json"},
-			IndexURLs:         svcCfg.BinariesIndexURLs,
-			IndexFile:         "index.json",
-			Verify:            svcCfg.VerifyBinaryUpdates,
-			AutoCheck:         true, // FIXME: Get from setting.
-			AutoDownload:      false,
-			AutoApply:         false,
-			NeedsRestart:      true,
-			Notify:            true,
-		}
-		intelUpdateConfig = &updates.Config{
-			Name:              "intel",
-			Directory:         filepath.Join(svcCfg.DataDir, "intel"),
-			DownloadDirectory: filepath.Join(svcCfg.DataDir, "download_intel"),
-			PurgeDirectory:    filepath.Join(svcCfg.DataDir, "upgrade_obsolete_intel"),
-			IndexURLs:         svcCfg.IntelIndexURLs,
-			IndexFile:         "index.json",
-			Verify:            svcCfg.VerifyIntelUpdates,
-			AutoCheck:         true, // FIXME: Get from setting.
-			AutoDownload:      true,
-			AutoApply:         true,
-			NeedsRestart:      false,
-			Notify:            false,
-		}
-
-	case "linux":
-		binaryUpdateConfig = &updates.Config{
-			Name:              "binaries",
-			Directory:         svcCfg.BinDir,
-			DownloadDirectory: filepath.Join(svcCfg.DataDir, "download_binaries"),
-			PurgeDirectory:    filepath.Join(svcCfg.DataDir, "upgrade_obsolete_binaries"),
-			Ignore:            []string{"databases", "intel", "config.json"},
-			IndexURLs:         svcCfg.BinariesIndexURLs,
-			IndexFile:         "index.json",
-			Verify:            svcCfg.VerifyBinaryUpdates,
-			AutoCheck:         true, // FIXME: Get from setting.
-			AutoDownload:      false,
-			AutoApply:         false,
-			NeedsRestart:      true,
-			Notify:            true,
-		}
-		intelUpdateConfig = &updates.Config{
-			Name:              "intel",
-			Directory:         filepath.Join(svcCfg.DataDir, "intel"),
-			DownloadDirectory: filepath.Join(svcCfg.DataDir, "download_intel"),
-			PurgeDirectory:    filepath.Join(svcCfg.DataDir, "upgrade_obsolete_intel"),
-			IndexURLs:         svcCfg.IntelIndexURLs,
-			IndexFile:         "index.json",
-			Verify:            svcCfg.VerifyIntelUpdates,
-			AutoCheck:         true, // FIXME: Get from setting.
-			AutoDownload:      true,
-			AutoApply:         true,
-			NeedsRestart:      false,
-			Notify:            false,
-		}
-	}
-
-	return
-}
diff --git a/service/updates/api.go b/service/updates/api.go
new file mode 100644
index 00000000..88659620
--- /dev/null
+++ b/service/updates/api.go
@@ -0,0 +1,161 @@
+package updates
+
+import (
+	"bytes"
+	"io"
+	"net/http"
+	"os"
+	"path/filepath"
+	"strings"
+
+	"github.com/ghodss/yaml"
+
+	"github.com/safing/portmaster/base/api"
+	"github.com/safing/portmaster/base/log"
+	"github.com/safing/portmaster/base/utils"
+)
+
+const (
+	apiPathCheckForUpdates = "updates/check"
+)
+
+func registerAPIEndpoints() error {
+	if err := api.RegisterEndpoint(api.Endpoint{
+		Name:        "Check for Updates",
+		Description: "Checks if new versions are available. If automatic updates are enabled, they are also downloaded and applied.",
+		Parameters: []api.Parameter{{
+			Method:      http.MethodPost,
+			Field:       "download",
+			Value:       "",
+			Description: "Force downloading and applying of all updates, regardless of auto-update settings.",
+		}},
+		Path:  apiPathCheckForUpdates,
+		Write: api.PermitUser,
+		ActionFunc: func(r *api.Request) (msg string, err error) {
+			// Check if we should also download regardless of settings.
+			downloadAll := r.URL.Query().Has("download")
+
+			// Trigger update task.
+			err = TriggerUpdate(true, downloadAll)
+			if err != nil {
+				return "", err
+			}
+
+			// Report how we triggered.
+			if downloadAll {
+				return "downloading all updates...", nil
+			}
+			return "checking for updates...", nil
+		},
+	}); err != nil {
+		return err
+	}
+
+	if err := api.RegisterEndpoint(api.Endpoint{
+		Name:        "Get Resource",
+		Description: "Returns the requested resource from the udpate system",
+		Path:        `updates/get/{identifier:[A-Za-z0-9/\.\-_]{1,255}}`,
+		Read:        api.PermitUser,
+		ReadMethod:  http.MethodGet,
+		HandlerFunc: func(w http.ResponseWriter, r *http.Request) {
+			// Get identifier from URL.
+			var identifier string
+			if ar := api.GetAPIRequest(r); ar != nil {
+				identifier = ar.URLVars["identifier"]
+			}
+			if identifier == "" {
+				http.Error(w, "no resource speicified", http.StatusBadRequest)
+				return
+			}
+
+			// Get resource.
+			resource, err := registry.GetFile(identifier)
+			if err != nil {
+				http.Error(w, err.Error(), http.StatusNotFound)
+				return
+			}
+
+			// Open file for reading.
+			file, err := os.Open(resource.Path())
+			if err != nil {
+				http.Error(w, err.Error(), http.StatusInternalServerError)
+				return
+			}
+			defer file.Close() //nolint:errcheck,gosec
+
+			// Assign file to reader
+			var reader io.Reader = file
+
+			// Add version to header.
+			w.Header().Set("Resource-Version", resource.Version())
+
+			// Set Content-Type.
+			contentType, _ := utils.MimeTypeByExtension(filepath.Ext(resource.Path()))
+			w.Header().Set("Content-Type", contentType)
+
+			// Check if the content type may be returned.
+			accept := r.Header.Get("Accept")
+			if accept != "" {
+				mimeTypes := strings.Split(accept, ",")
+				// First, clean mime types.
+				for i, mimeType := range mimeTypes {
+					mimeType = strings.TrimSpace(mimeType)
+					mimeType, _, _ = strings.Cut(mimeType, ";")
+					mimeTypes[i] = mimeType
+				}
+				// Second, check if we may return anything.
+				var acceptsAny bool
+				for _, mimeType := range mimeTypes {
+					switch mimeType {
+					case "*", "*/*":
+						acceptsAny = true
+					}
+				}
+				// Third, check if we can convert.
+				if !acceptsAny {
+					var converted bool
+					sourceType, _, _ := strings.Cut(contentType, ";")
+				findConvertiblePair:
+					for _, mimeType := range mimeTypes {
+						switch {
+						case sourceType == "application/yaml" && mimeType == "application/json":
+							yamlData, err := io.ReadAll(reader)
+							if err != nil {
+								http.Error(w, err.Error(), http.StatusInternalServerError)
+								return
+							}
+							jsonData, err := yaml.YAMLToJSON(yamlData)
+							if err != nil {
+								http.Error(w, err.Error(), http.StatusInternalServerError)
+								return
+							}
+							reader = bytes.NewReader(jsonData)
+							converted = true
+							break findConvertiblePair
+						}
+					}
+
+					// If we could not convert to acceptable format, return an error.
+					if !converted {
+						http.Error(w, "conversion to requested format not supported", http.StatusNotAcceptable)
+						return
+					}
+				}
+			}
+
+			// Write file.
+			w.WriteHeader(http.StatusOK)
+			if r.Method != http.MethodHead {
+				_, err = io.Copy(w, reader)
+				if err != nil {
+					log.Errorf("updates: failed to serve resource file: %s", err)
+					return
+				}
+			}
+		},
+	}); err != nil {
+		return err
+	}
+
+	return nil
+}
diff --git a/service/updates/assets/portmaster.service b/service/updates/assets/portmaster.service
new file mode 100644
index 00000000..c69a9ff5
--- /dev/null
+++ b/service/updates/assets/portmaster.service
@@ -0,0 +1,44 @@
+[Unit]
+Description=Portmaster by Safing
+Documentation=https://safing.io
+Documentation=https://docs.safing.io
+Before=nss-lookup.target network.target shutdown.target
+After=systemd-networkd.service
+Conflicts=shutdown.target
+Conflicts=firewalld.service
+Wants=nss-lookup.target
+
+[Service]
+Type=simple
+Restart=on-failure
+RestartSec=10
+LockPersonality=yes
+MemoryDenyWriteExecute=yes
+NoNewPrivileges=yes
+PrivateTmp=yes
+PIDFile=/opt/safing/portmaster/core-lock.pid
+Environment=LOGLEVEL=info
+Environment=PORTMASTER_ARGS=
+EnvironmentFile=-/etc/default/portmaster
+ProtectSystem=true
+#ReadWritePaths=/var/lib/portmaster
+#ReadWritePaths=/run/xtables.lock
+RestrictAddressFamilies=AF_UNIX AF_NETLINK AF_INET AF_INET6
+RestrictNamespaces=yes
+# In future version portmaster will require access to user home
+# directories to verify application permissions.
+ProtectHome=read-only
+ProtectKernelTunables=yes
+ProtectKernelLogs=yes
+ProtectControlGroups=yes
+PrivateDevices=yes
+AmbientCapabilities=cap_chown cap_kill cap_net_admin cap_net_bind_service cap_net_broadcast cap_net_raw cap_sys_module cap_sys_ptrace cap_dac_override cap_fowner cap_fsetid cap_sys_resource cap_bpf cap_perfmon
+CapabilityBoundingSet=cap_chown cap_kill cap_net_admin cap_net_bind_service cap_net_broadcast cap_net_raw cap_sys_module cap_sys_ptrace cap_dac_override cap_fowner cap_fsetid cap_sys_resource cap_bpf cap_perfmon
+# SystemCallArchitectures=native
+# SystemCallFilter=@system-service @module
+# SystemCallErrorNumber=EPERM
+ExecStart=/opt/safing/portmaster/portmaster-start --data /opt/safing/portmaster core -- $PORTMASTER_ARGS
+ExecStopPost=-/opt/safing/portmaster/portmaster-start recover-iptables
+
+[Install]
+WantedBy=multi-user.target
diff --git a/service/updates/config.go b/service/updates/config.go
new file mode 100644
index 00000000..f765fd4c
--- /dev/null
+++ b/service/updates/config.go
@@ -0,0 +1,178 @@
+package updates
+
+import (
+	"github.com/tevino/abool"
+
+	"github.com/safing/portmaster/base/config"
+	"github.com/safing/portmaster/base/log"
+	"github.com/safing/portmaster/service/mgr"
+	"github.com/safing/portmaster/service/updates/helper"
+)
+
+const cfgDevModeKey = "core/devMode"
+
+var (
+	releaseChannel        config.StringOption
+	devMode               config.BoolOption
+	enableSoftwareUpdates config.BoolOption
+	enableIntelUpdates    config.BoolOption
+
+	initialReleaseChannel  string
+	previousReleaseChannel string
+
+	softwareUpdatesCurrentlyEnabled bool
+	intelUpdatesCurrentlyEnabled    bool
+	previousDevMode                 bool
+	forceCheck                      = abool.New()
+	forceDownload                   = abool.New()
+)
+
+func registerConfig() error {
+	err := config.Register(&config.Option{
+		Name:            "Release Channel",
+		Key:             helper.ReleaseChannelKey,
+		Description:     `Use "Stable" for the best experience. The "Beta" channel will have the newest features and fixes, but may also break and cause interruption. Use others only temporarily and when instructed.`,
+		OptType:         config.OptTypeString,
+		ExpertiseLevel:  config.ExpertiseLevelExpert,
+		ReleaseLevel:    config.ReleaseLevelStable,
+		RequiresRestart: true,
+		DefaultValue:    helper.ReleaseChannelStable,
+		PossibleValues: []config.PossibleValue{
+			{
+				Name:        "Stable",
+				Description: "Production releases.",
+				Value:       helper.ReleaseChannelStable,
+			},
+			{
+				Name:        "Beta",
+				Description: "Production releases for testing new features that may break and cause interruption.",
+				Value:       helper.ReleaseChannelBeta,
+			},
+			{
+				Name:        "Support",
+				Description: "Support releases or version changes for troubleshooting. Only use temporarily and when instructed.",
+				Value:       helper.ReleaseChannelSupport,
+			},
+			{
+				Name:        "Staging",
+				Description: "Dangerous development releases for testing random things and experimenting. Only use temporarily and when instructed.",
+				Value:       helper.ReleaseChannelStaging,
+			},
+		},
+		Annotations: config.Annotations{
+			config.DisplayOrderAnnotation: -4,
+			config.DisplayHintAnnotation:  config.DisplayHintOneOf,
+			config.CategoryAnnotation:     "Updates",
+		},
+	})
+	if err != nil {
+		return err
+	}
+
+	err = config.Register(&config.Option{
+		Name:            "Automatic Software Updates",
+		Key:             enableSoftwareUpdatesKey,
+		Description:     "Automatically check for and download software updates. This does not include intelligence data updates.",
+		OptType:         config.OptTypeBool,
+		ExpertiseLevel:  config.ExpertiseLevelExpert,
+		ReleaseLevel:    config.ReleaseLevelStable,
+		RequiresRestart: false,
+		DefaultValue:    true,
+		Annotations: config.Annotations{
+			config.DisplayOrderAnnotation: -12,
+			config.CategoryAnnotation:     "Updates",
+		},
+	})
+	if err != nil {
+		return err
+	}
+
+	err = config.Register(&config.Option{
+		Name:            "Automatic Intelligence Data Updates",
+		Key:             enableIntelUpdatesKey,
+		Description:     "Automatically check for and download intelligence data updates. This includes filter lists, geo-ip data, and more. Does not include software updates.",
+		OptType:         config.OptTypeBool,
+		ExpertiseLevel:  config.ExpertiseLevelExpert,
+		ReleaseLevel:    config.ReleaseLevelStable,
+		RequiresRestart: false,
+		DefaultValue:    true,
+		Annotations: config.Annotations{
+			config.DisplayOrderAnnotation: -11,
+			config.CategoryAnnotation:     "Updates",
+		},
+	})
+	if err != nil {
+		return err
+	}
+
+	return nil
+}
+
+func initConfig() {
+	releaseChannel = config.Concurrent.GetAsString(helper.ReleaseChannelKey, helper.ReleaseChannelStable)
+	initialReleaseChannel = releaseChannel()
+	previousReleaseChannel = releaseChannel()
+
+	enableSoftwareUpdates = config.Concurrent.GetAsBool(enableSoftwareUpdatesKey, true)
+	enableIntelUpdates = config.Concurrent.GetAsBool(enableIntelUpdatesKey, true)
+	softwareUpdatesCurrentlyEnabled = enableSoftwareUpdates()
+	intelUpdatesCurrentlyEnabled = enableIntelUpdates()
+
+	devMode = config.Concurrent.GetAsBool(cfgDevModeKey, false)
+	previousDevMode = devMode()
+}
+
+func updateRegistryConfig(_ *mgr.WorkerCtx, _ struct{}) (cancel bool, err error) {
+	changed := false
+
+	if enableSoftwareUpdates() != softwareUpdatesCurrentlyEnabled {
+		softwareUpdatesCurrentlyEnabled = enableSoftwareUpdates()
+		changed = true
+	}
+
+	if enableIntelUpdates() != intelUpdatesCurrentlyEnabled {
+		intelUpdatesCurrentlyEnabled = enableIntelUpdates()
+		changed = true
+	}
+
+	if devMode() != previousDevMode {
+		registry.SetDevMode(devMode())
+		previousDevMode = devMode()
+		changed = true
+	}
+
+	if releaseChannel() != previousReleaseChannel {
+		previousReleaseChannel = releaseChannel()
+		changed = true
+	}
+
+	if changed {
+		// Update indexes based on new settings.
+		warning := helper.SetIndexes(
+			registry,
+			releaseChannel(),
+			true,
+			softwareUpdatesCurrentlyEnabled,
+			intelUpdatesCurrentlyEnabled,
+		)
+		if warning != nil {
+			log.Warningf("updates: %s", warning)
+		}
+
+		// Select versions depending on new indexes and modes.
+		registry.SelectVersions()
+		module.EventVersionsUpdated.Submit(struct{}{})
+
+		if softwareUpdatesCurrentlyEnabled || intelUpdatesCurrentlyEnabled {
+			module.states.Clear()
+			if err := TriggerUpdate(true, false); err != nil {
+				log.Warningf("updates: failed to trigger update: %s", err)
+			}
+			log.Infof("updates: automatic updates are now enabled")
+		} else {
+			log.Warningf("updates: automatic updates are now completely disabled")
+		}
+	}
+
+	return false, nil
+}
diff --git a/service/updates/downloader.go b/service/updates/downloader.go
deleted file mode 100644
index 30c32261..00000000
--- a/service/updates/downloader.go
+++ /dev/null
@@ -1,307 +0,0 @@
-package updates
-
-import (
-	"archive/zip"
-	"bytes"
-	"compress/gzip"
-	"context"
-	"crypto/sha256"
-	"encoding/hex"
-	"errors"
-	"fmt"
-	"io"
-	"io/fs"
-	"net/http"
-	"os"
-	"path/filepath"
-
-	"github.com/safing/portmaster/base/log"
-)
-
-type Downloader struct {
-	u         *Updater
-	index     *Index
-	indexURLs []string
-
-	existingFiles map[string]string
-
-	httpClient http.Client
-}
-
-func NewDownloader(u *Updater, indexURLs []string) *Downloader {
-	return &Downloader{
-		u:         u,
-		indexURLs: indexURLs,
-	}
-}
-
-func (d *Downloader) updateIndex(ctx context.Context) error {
-	// Make sure dir exists.
-	err := os.MkdirAll(d.u.cfg.DownloadDirectory, defaultDirMode)
-	if err != nil {
-		return fmt.Errorf("create download directory: %s", d.u.cfg.DownloadDirectory)
-	}
-
-	// Try to download the index from one of the index URLs.
-	var (
-		indexData []byte
-		index     *Index
-	)
-	for _, url := range d.indexURLs {
-		// Download and verify index.
-		indexData, index, err = d.getIndex(ctx, url)
-		if err == nil {
-			// Valid index found!
-			break
-		}
-
-		log.Warningf("updates/%s: failed to update index from %q: %s", d.u.cfg.Name, url, err)
-		err = fmt.Errorf("update index file from %q: %w", url, err)
-	}
-	if err != nil {
-		return fmt.Errorf("all index URLs failed, last error: %w", err)
-	}
-	d.index = index
-
-	// Write the index into a file.
-	indexFilepath := filepath.Join(d.u.cfg.DownloadDirectory, d.u.cfg.IndexFile)
-	err = os.WriteFile(indexFilepath, indexData, defaultFileMode)
-	if err != nil {
-		return fmt.Errorf("write index file: %w", err)
-	}
-
-	return nil
-}
-
-func (d *Downloader) getIndex(ctx context.Context, url string) (indexData []byte, bundle *Index, err error) {
-	// Download data from URL.
-	indexData, err = d.downloadData(ctx, url)
-	if err != nil {
-		return nil, nil, fmt.Errorf("GET index: %w", err)
-	}
-
-	// Verify and parse index.
-	bundle, err = ParseIndex(indexData, d.u.cfg.Verify)
-	if err != nil {
-		return nil, nil, fmt.Errorf("parse index: %w", err)
-	}
-
-	return indexData, bundle, nil
-}
-
-// gatherExistingFiles gathers the checksums on existing files.
-func (d *Downloader) gatherExistingFiles(dir string) error {
-	// Make sure map is initialized.
-	if d.existingFiles == nil {
-		d.existingFiles = make(map[string]string)
-	}
-
-	// Walk directory, just log errors.
-	err := filepath.WalkDir(dir, func(fullpath string, entry fs.DirEntry, err error) error {
-		// Fail on access error.
-		if err != nil {
-			return err
-		}
-
-		// Skip folders.
-		if entry.IsDir() {
-			return nil
-		}
-
-		// Read full file.
-		fileData, err := os.ReadFile(fullpath)
-		if err != nil {
-			log.Debugf("updates/%s: failed to read file %q while searching for existing files: %s", d.u.cfg.Name, fullpath, err)
-			return fmt.Errorf("failed to read file %s: %w", fullpath, err)
-		}
-
-		// Calculate checksum and add it to the existing files.
-		hashSum := sha256.Sum256(fileData)
-		d.existingFiles[hex.EncodeToString(hashSum[:])] = fullpath
-
-		return nil
-	})
-	if err != nil {
-		return fmt.Errorf("searching for existing files: %w", err)
-	}
-
-	return nil
-}
-
-func (d *Downloader) downloadArtifacts(ctx context.Context) error {
-	// Make sure dir exists.
-	err := os.MkdirAll(d.u.cfg.DownloadDirectory, defaultDirMode)
-	if err != nil {
-		return fmt.Errorf("create download directory: %s", d.u.cfg.DownloadDirectory)
-	}
-
-artifacts:
-	for _, artifact := range d.index.Artifacts {
-		dstFilePath := filepath.Join(d.u.cfg.DownloadDirectory, artifact.Filename)
-
-		// Check if we can copy the artifact from disk instead.
-		if existingFile, ok := d.existingFiles[artifact.SHA256]; ok {
-			// Check if this is the same file.
-			if existingFile == dstFilePath {
-				continue artifacts
-			}
-			// Copy and check.
-			err = copyAndCheckSHA256Sum(existingFile, dstFilePath, artifact.SHA256, artifact.GetFileMode())
-			if err == nil {
-				continue artifacts
-			}
-			log.Debugf("updates/%s: failed to copy existing file %s: %s", d.u.cfg.Name, artifact.Filename, err)
-		}
-
-		// Check if the artifact has download URLs.
-		if len(artifact.URLs) == 0 {
-			return fmt.Errorf("artifact %s is missing download URLs", artifact.Filename)
-		}
-
-		// Try to download the artifact from one of the URLs.
-		var artifactData []byte
-	artifactURLs:
-		for _, url := range artifact.URLs {
-			// Download and verify index.
-			artifactData, err = d.getArtifact(ctx, artifact, url)
-			if err == nil {
-				// Valid artifact found!
-				break artifactURLs
-			}
-			err = fmt.Errorf("update index file from %q: %w", url, err)
-		}
-		if err != nil {
-			return fmt.Errorf("all artifact URLs for %s failed, last error: %w", artifact.Filename, err)
-		}
-
-		// Write artifact to temporary file.
-		tmpFilename := dstFilePath + ".download"
-		err = os.WriteFile(tmpFilename, artifactData, artifact.GetFileMode())
-		if err != nil {
-			return fmt.Errorf("write %s to temp file: %w", artifact.Filename, err)
-		}
-
-		// Rename/Move to actual location.
-		err = os.Rename(tmpFilename, dstFilePath)
-		if err != nil {
-			return fmt.Errorf("rename %s after write: %w", artifact.Filename, err)
-		}
-
-		log.Infof("updates/%s: downloaded and verified %s", d.u.cfg.Name, artifact.Filename)
-	}
-	return nil
-}
-
-func (d *Downloader) getArtifact(ctx context.Context, artifact Artifact, url string) ([]byte, error) {
-	// Download data from URL.
-	artifactData, err := d.downloadData(ctx, url)
-	if err != nil {
-		return nil, fmt.Errorf("GET artifact: %w", err)
-	}
-
-	// Decompress artifact data, if configured.
-	// TODO: Normally we should do operations on "untrusted" data _after_ verification,
-	// but we really want the checksum to be for the unpacked data. Should we add another checksum, or is HTTPS enough?
-	if artifact.Unpack != "" {
-		artifactData, err = decompress(artifact.Unpack, artifactData)
-		if err != nil {
-			return nil, fmt.Errorf("decompress: %w", err)
-		}
-	}
-
-	// Verify checksum.
-	if err := checkSHA256Sum(artifactData, artifact.SHA256); err != nil {
-		return nil, err
-	}
-
-	return artifactData, nil
-}
-
-func (d *Downloader) downloadData(ctx context.Context, url string) ([]byte, error) {
-	// Setup request.
-	req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, http.NoBody)
-	if err != nil {
-		return nil, fmt.Errorf("failed to create GET request to %s: %w", url, err)
-	}
-	if UserAgent != "" {
-		req.Header.Set("User-Agent", UserAgent)
-	}
-
-	// Start request with shared http client.
-	resp, err := d.httpClient.Do(req)
-	if err != nil {
-		return nil, fmt.Errorf("failed a get file request to: %w", err)
-	}
-	defer func() { _ = resp.Body.Close() }()
-
-	// Check for HTTP status errors.
-	if resp.StatusCode != http.StatusOK {
-		return nil, fmt.Errorf("server returned non-OK status: %d %s", resp.StatusCode, resp.Status)
-	}
-
-	// Read the full body and return it.
-	content, err := io.ReadAll(resp.Body)
-	if err != nil {
-		return nil, fmt.Errorf("failed to read body of response: %w", err)
-	}
-	return content, nil
-}
-
-func decompress(cType string, fileBytes []byte) ([]byte, error) {
-	switch cType {
-	case "zip":
-		return decompressZip(fileBytes)
-	case "gz":
-		return decompressGzip(fileBytes)
-	default:
-		return nil, fmt.Errorf("unsupported compression type")
-	}
-}
-
-func decompressGzip(data []byte) ([]byte, error) {
-	// Create a gzip reader from the byte slice.
-	gzipReader, err := gzip.NewReader(bytes.NewReader(data))
-	if err != nil {
-		return nil, fmt.Errorf("create gzip reader: %w", err)
-	}
-	defer func() { _ = gzipReader.Close() }()
-
-	// Copy from the gzip reader into a new buffer.
-	var buf bytes.Buffer
-	_, err = io.CopyN(&buf, gzipReader, MaxUnpackSize)
-	if err != nil && !errors.Is(err, io.EOF) {
-		return nil, fmt.Errorf("read gzip file: %w", err)
-	}
-
-	return buf.Bytes(), nil
-}
-
-func decompressZip(data []byte) ([]byte, error) {
-	// Create a zip reader from the byte slice.
-	zipReader, err := zip.NewReader(bytes.NewReader(data), int64(len(data)))
-	if err != nil {
-		return nil, fmt.Errorf("create zip reader: %w", err)
-	}
-
-	// Ensure there is only one file in the zip.
-	if len(zipReader.File) != 1 {
-		return nil, fmt.Errorf("zip file must contain exactly one file")
-	}
-
-	// Open single file in the zip.
-	file := zipReader.File[0]
-	fileReader, err := file.Open()
-	if err != nil {
-		return nil, fmt.Errorf("open file in zip: %w", err)
-	}
-	defer func() { _ = fileReader.Close() }()
-
-	// Copy from the zip reader into a new buffer.
-	var buf bytes.Buffer
-	_, err = io.CopyN(&buf, fileReader, MaxUnpackSize)
-	if err != nil && !errors.Is(err, io.EOF) {
-		return nil, fmt.Errorf("read file in zip: %w", err)
-	}
-
-	return buf.Bytes(), nil
-}
diff --git a/service/updates/export.go b/service/updates/export.go
new file mode 100644
index 00000000..c230f367
--- /dev/null
+++ b/service/updates/export.go
@@ -0,0 +1,238 @@
+package updates
+
+import (
+	"fmt"
+	"sort"
+	"strings"
+	"sync"
+
+	"github.com/safing/portmaster/base/database/record"
+	"github.com/safing/portmaster/base/info"
+	"github.com/safing/portmaster/base/log"
+	"github.com/safing/portmaster/base/updater"
+	"github.com/safing/portmaster/base/utils/debug"
+	"github.com/safing/portmaster/service/mgr"
+	"github.com/safing/portmaster/service/updates/helper"
+)
+
+const (
+	// versionsDBKey is the database key for update version information.
+	versionsDBKey = "core:status/versions"
+
+	// versionsDBKey is the database key for simple update version information.
+	simpleVersionsDBKey = "core:status/simple-versions"
+
+	// updateStatusDBKey is the database key for update status information.
+	updateStatusDBKey = "core:status/updates"
+)
+
+// Versions holds update versions and status information.
+type Versions struct {
+	record.Base
+	sync.Mutex
+
+	Core      *info.Info
+	Resources map[string]*updater.Resource
+	Channel   string
+	Beta      bool
+	Staging   bool
+}
+
+// SimpleVersions holds simplified update versions and status information.
+type SimpleVersions struct {
+	record.Base
+	sync.Mutex
+
+	Build     *info.Info
+	Resources map[string]*SimplifiedResourceVersion
+	Channel   string
+}
+
+// SimplifiedResourceVersion holds version information about one resource.
+type SimplifiedResourceVersion struct {
+	Version string
+}
+
+// UpdateStateExport is a wrapper to export the updates state.
+type UpdateStateExport struct {
+	record.Base
+	sync.Mutex
+
+	*updater.UpdateState
+}
+
+// GetVersions returns the update versions and status information.
+// Resources must be locked when accessed.
+func GetVersions() *Versions {
+	return &Versions{
+		Core:      info.GetInfo(),
+		Resources: registry.Export(),
+		Channel:   initialReleaseChannel,
+		Beta:      initialReleaseChannel == helper.ReleaseChannelBeta,
+		Staging:   initialReleaseChannel == helper.ReleaseChannelStaging,
+	}
+}
+
+// GetSimpleVersions returns the simplified update versions and status information.
+func GetSimpleVersions() *SimpleVersions {
+	// Fill base info.
+	v := &SimpleVersions{
+		Build:     info.GetInfo(),
+		Resources: make(map[string]*SimplifiedResourceVersion),
+		Channel:   initialReleaseChannel,
+	}
+
+	// Iterate through all versions and add version info.
+	for id, resource := range registry.Export() {
+		func() {
+			resource.Lock()
+			defer resource.Unlock()
+
+			// Get current in-used or selected version.
+			var rv *updater.ResourceVersion
+			switch {
+			case resource.ActiveVersion != nil:
+				rv = resource.ActiveVersion
+			case resource.SelectedVersion != nil:
+				rv = resource.SelectedVersion
+			}
+
+			// Get information from resource.
+			if rv != nil {
+				v.Resources[id] = &SimplifiedResourceVersion{
+					Version: rv.VersionNumber,
+				}
+			}
+		}()
+	}
+
+	return v
+}
+
+// GetStateExport gets the update state from the registry and returns it in an
+// exportable struct.
+func GetStateExport() *UpdateStateExport {
+	export := registry.GetState()
+	return &UpdateStateExport{
+		UpdateState: &export.Updates,
+	}
+}
+
+// LoadStateExport loads the exported update state from the database.
+func LoadStateExport() (*UpdateStateExport, error) {
+	r, err := db.Get(updateStatusDBKey)
+	if err != nil {
+		return nil, err
+	}
+
+	// unwrap
+	if r.IsWrapped() {
+		// only allocate a new struct, if we need it
+		newRecord := &UpdateStateExport{}
+		err = record.Unwrap(r, newRecord)
+		if err != nil {
+			return nil, err
+		}
+		return newRecord, nil
+	}
+
+	// or adjust type
+	newRecord, ok := r.(*UpdateStateExport)
+	if !ok {
+		return nil, fmt.Errorf("record not of type *UpdateStateExport, but %T", r)
+	}
+	return newRecord, nil
+}
+
+func initVersionExport() (err error) {
+	if err := GetVersions().save(); err != nil {
+		log.Warningf("updates: failed to export version information: %s", err)
+	}
+	if err := GetSimpleVersions().save(); err != nil {
+		log.Warningf("updates: failed to export version information: %s", err)
+	}
+
+	module.EventVersionsUpdated.AddCallback("export version status", export)
+	return nil
+}
+
+func (v *Versions) save() error {
+	if !v.KeyIsSet() {
+		v.SetKey(versionsDBKey)
+	}
+	return db.Put(v)
+}
+
+func (v *SimpleVersions) save() error {
+	if !v.KeyIsSet() {
+		v.SetKey(simpleVersionsDBKey)
+	}
+	return db.Put(v)
+}
+
+func (s *UpdateStateExport) save() error {
+	if !s.KeyIsSet() {
+		s.SetKey(updateStatusDBKey)
+	}
+	return db.Put(s)
+}
+
+// export is an event hook.
+func export(_ *mgr.WorkerCtx, _ struct{}) (cancel bool, err error) {
+	// Export versions.
+	if err := GetVersions().save(); err != nil {
+		return false, err
+	}
+	if err := GetSimpleVersions().save(); err != nil {
+		return false, err
+	}
+	// Export udpate state.
+	if err := GetStateExport().save(); err != nil {
+		return false, err
+	}
+
+	return false, nil
+}
+
+// AddToDebugInfo adds the update system status to the given debug.Info.
+func AddToDebugInfo(di *debug.Info) {
+	// Get resources from registry.
+	resources := registry.Export()
+	platformPrefix := helper.PlatformIdentifier("")
+
+	// Collect data for debug info.
+	var active, selected []string
+	var activeCnt, totalCnt int
+	for id, r := range resources {
+		// Ignore resources for other platforms.
+		if !strings.HasPrefix(id, "all/") && !strings.HasPrefix(id, platformPrefix) {
+			continue
+		}
+
+		totalCnt++
+		if r.ActiveVersion != nil {
+			activeCnt++
+			active = append(active, fmt.Sprintf("%s: %s", id, r.ActiveVersion.VersionNumber))
+		}
+		if r.SelectedVersion != nil {
+			selected = append(selected, fmt.Sprintf("%s: %s", id, r.SelectedVersion.VersionNumber))
+		}
+	}
+	sort.Strings(active)
+	sort.Strings(selected)
+
+	// Compile to one list.
+	lines := make([]string, 0, len(active)+len(selected)+3)
+	lines = append(lines, "Active:")
+	lines = append(lines, active...)
+	lines = append(lines, "")
+	lines = append(lines, "Selected:")
+	lines = append(lines, selected...)
+
+	// Add section.
+	di.AddSection(
+		fmt.Sprintf("Updates: %s (%d/%d)", initialReleaseChannel, activeCnt, totalCnt),
+		debug.UseCodeSection|debug.AddContentLineBreaks,
+		lines...,
+	)
+}
diff --git a/service/updates/get.go b/service/updates/get.go
new file mode 100644
index 00000000..bac9ae14
--- /dev/null
+++ b/service/updates/get.go
@@ -0,0 +1,72 @@
+package updates
+
+import (
+	"path"
+
+	"github.com/safing/portmaster/base/updater"
+	"github.com/safing/portmaster/service/updates/helper"
+)
+
+// GetPlatformFile returns the latest platform specific file identified by the given identifier.
+func GetPlatformFile(identifier string) (*updater.File, error) {
+	identifier = helper.PlatformIdentifier(identifier)
+
+	file, err := registry.GetFile(identifier)
+	if err != nil {
+		return nil, err
+	}
+
+	module.EventVersionsUpdated.Submit(struct{}{})
+	return file, nil
+}
+
+// GetFile returns the latest generic file identified by the given identifier.
+func GetFile(identifier string) (*updater.File, error) {
+	identifier = path.Join("all", identifier)
+
+	file, err := registry.GetFile(identifier)
+	if err != nil {
+		return nil, err
+	}
+
+	module.EventVersionsUpdated.Submit(struct{}{})
+	return file, nil
+}
+
+// GetPlatformVersion returns the selected platform specific version of the
+// given identifier.
+// The returned resource version may not be modified.
+func GetPlatformVersion(identifier string) (*updater.ResourceVersion, error) {
+	identifier = helper.PlatformIdentifier(identifier)
+
+	rv, err := registry.GetVersion(identifier)
+	if err != nil {
+		return nil, err
+	}
+
+	return rv, nil
+}
+
+// GetVersion returns the selected generic version of the given identifier.
+// The returned resource version may not be modified.
+func GetVersion(identifier string) (*updater.ResourceVersion, error) {
+	identifier = path.Join("all", identifier)
+
+	rv, err := registry.GetVersion(identifier)
+	if err != nil {
+		return nil, err
+	}
+
+	return rv, nil
+}
+
+// GetVersionWithFullID returns the selected generic version of the given full identifier.
+// The returned resource version may not be modified.
+func GetVersionWithFullID(identifier string) (*updater.ResourceVersion, error) {
+	rv, err := registry.GetVersion(identifier)
+	if err != nil {
+		return nil, err
+	}
+
+	return rv, nil
+}
diff --git a/service/updates/helper/electron.go b/service/updates/helper/electron.go
new file mode 100644
index 00000000..4c8c4a07
--- /dev/null
+++ b/service/updates/helper/electron.go
@@ -0,0 +1,57 @@
+package helper
+
+import (
+	"errors"
+	"fmt"
+	"os"
+	"path/filepath"
+	"runtime"
+	"strings"
+
+	"github.com/safing/portmaster/base/log"
+	"github.com/safing/portmaster/base/updater"
+)
+
+var pmElectronUpdate *updater.File
+
+const suidBitWarning = `Failed to set SUID permissions for chrome-sandbox. This is required for Linux kernel versions that do not have unprivileged user namespaces (CONFIG_USER_NS_UNPRIVILEGED) enabled. If you're running and up-to-date distribution kernel you can likely ignore this warning. If you encounter issue starting the user interface please either update your kernel or set the SUID bit (mode 0%0o) on %s`
+
+// EnsureChromeSandboxPermissions makes sure the chrome-sandbox distributed
+// by our app-electron package has the SUID bit set on systems that do not
+// allow unprivileged CLONE_NEWUSER (clone(3)).
+// On non-linux systems or systems that have kernel.unprivileged_userns_clone
+// set to 1 EnsureChromeSandboPermissions is a NO-OP.
+func EnsureChromeSandboxPermissions(reg *updater.ResourceRegistry) error {
+	if runtime.GOOS != "linux" {
+		return nil
+	}
+
+	if pmElectronUpdate != nil && !pmElectronUpdate.UpgradeAvailable() {
+		return nil
+	}
+
+	identifier := PlatformIdentifier("app/portmaster-app.zip")
+
+	var err error
+	pmElectronUpdate, err = reg.GetFile(identifier)
+	if err != nil {
+		if errors.Is(err, updater.ErrNotAvailableLocally) {
+			return nil
+		}
+		return fmt.Errorf("failed to get file: %w", err)
+	}
+
+	unpackedPath := strings.TrimSuffix(
+		pmElectronUpdate.Path(),
+		filepath.Ext(pmElectronUpdate.Path()),
+	)
+	sandboxFile := filepath.Join(unpackedPath, "chrome-sandbox")
+	if err := os.Chmod(sandboxFile, 0o0755|os.ModeSetuid); err != nil {
+		log.Errorf(suidBitWarning, 0o0755|os.ModeSetuid, sandboxFile)
+
+		return fmt.Errorf("failed to chmod: %w", err)
+	}
+	log.Debugf("updates: fixed SUID permission for chrome-sandbox")
+
+	return nil
+}
diff --git a/service/updates/helper/indexes.go b/service/updates/helper/indexes.go
new file mode 100644
index 00000000..72457bc5
--- /dev/null
+++ b/service/updates/helper/indexes.go
@@ -0,0 +1,136 @@
+package helper
+
+import (
+	"errors"
+	"fmt"
+	"io/fs"
+	"os"
+	"path/filepath"
+
+	"github.com/safing/jess/filesig"
+	"github.com/safing/portmaster/base/updater"
+)
+
+// Release Channel Configuration Keys.
+const (
+	ReleaseChannelKey     = "core/releaseChannel"
+	ReleaseChannelJSONKey = "core.releaseChannel"
+)
+
+// Release Channels.
+const (
+	ReleaseChannelStable  = "stable"
+	ReleaseChannelBeta    = "beta"
+	ReleaseChannelStaging = "staging"
+	ReleaseChannelSupport = "support"
+)
+
+const jsonSuffix = ".json"
+
+// SetIndexes sets the update registry indexes and also configures the registry
+// to use pre-releases based on the channel.
+func SetIndexes(
+	registry *updater.ResourceRegistry,
+	releaseChannel string,
+	deleteUnusedIndexes bool,
+	autoDownload bool,
+	autoDownloadIntel bool,
+) (warning error) {
+	usePreReleases := false
+
+	// Be reminded that the order is important, as indexes added later will
+	// override the current release from earlier indexes.
+
+	// Reset indexes before adding them (again).
+	registry.ResetIndexes()
+
+	// Add the intel index first, in order to be able to override it with the
+	// other indexes when needed.
+	registry.AddIndex(updater.Index{
+		Path:         "all/intel/intel.json",
+		AutoDownload: autoDownloadIntel,
+	})
+
+	// Always add the stable index as a base.
+	registry.AddIndex(updater.Index{
+		Path:         ReleaseChannelStable + jsonSuffix,
+		AutoDownload: autoDownload,
+	})
+
+	// Add beta index if in beta or staging channel.
+	indexPath := ReleaseChannelBeta + jsonSuffix
+	if releaseChannel == ReleaseChannelBeta ||
+		releaseChannel == ReleaseChannelStaging ||
+		(releaseChannel == "" && indexExists(registry, indexPath)) {
+		registry.AddIndex(updater.Index{
+			Path:         indexPath,
+			PreRelease:   true,
+			AutoDownload: autoDownload,
+		})
+		usePreReleases = true
+	} else if deleteUnusedIndexes {
+		err := deleteIndex(registry, indexPath)
+		if err != nil {
+			warning = fmt.Errorf("failed to delete unused index %s: %w", indexPath, err)
+		}
+	}
+
+	// Add staging index if in staging channel.
+	indexPath = ReleaseChannelStaging + jsonSuffix
+	if releaseChannel == ReleaseChannelStaging ||
+		(releaseChannel == "" && indexExists(registry, indexPath)) {
+		registry.AddIndex(updater.Index{
+			Path:         indexPath,
+			PreRelease:   true,
+			AutoDownload: autoDownload,
+		})
+		usePreReleases = true
+	} else if deleteUnusedIndexes {
+		err := deleteIndex(registry, indexPath)
+		if err != nil {
+			warning = fmt.Errorf("failed to delete unused index %s: %w", indexPath, err)
+		}
+	}
+
+	// Add support index if in support channel.
+	indexPath = ReleaseChannelSupport + jsonSuffix
+	if releaseChannel == ReleaseChannelSupport ||
+		(releaseChannel == "" && indexExists(registry, indexPath)) {
+		registry.AddIndex(updater.Index{
+			Path:         indexPath,
+			AutoDownload: autoDownload,
+		})
+		usePreReleases = true
+	} else if deleteUnusedIndexes {
+		err := deleteIndex(registry, indexPath)
+		if err != nil {
+			warning = fmt.Errorf("failed to delete unused index %s: %w", indexPath, err)
+		}
+	}
+
+	// Set pre-release usage.
+	registry.SetUsePreReleases(usePreReleases)
+
+	return warning
+}
+
+func indexExists(registry *updater.ResourceRegistry, indexPath string) bool {
+	_, err := os.Stat(filepath.Join(registry.StorageDir().Path, indexPath))
+	return err == nil
+}
+
+func deleteIndex(registry *updater.ResourceRegistry, indexPath string) error {
+	// Remove index itself.
+	err := os.Remove(filepath.Join(registry.StorageDir().Path, indexPath))
+	if err != nil && !errors.Is(err, fs.ErrNotExist) {
+		return err
+	}
+
+	// Remove any accompanying signature.
+	err = os.Remove(filepath.Join(registry.StorageDir().Path, indexPath+filesig.Extension))
+	if err != nil && !errors.Is(err, fs.ErrNotExist) {
+		return err
+	}
+
+	return nil
+}
diff --git a/service/updates/helper/signing.go b/service/updates/helper/signing.go
new file mode 100644
index 00000000..136b1970
--- /dev/null
+++ b/service/updates/helper/signing.go
@@ -0,0 +1,42 @@
+package helper
+
+import (
+	"github.com/safing/jess"
+	"github.com/safing/portmaster/base/updater"
+)
+
+var (
+	// VerificationConfig holds the complete verification configuration for the registry.
+	VerificationConfig = map[string]*updater.VerificationOptions{
+		"": { // Default.
+			TrustStore:     BinarySigningTrustStore,
+			DownloadPolicy: updater.SignaturePolicyRequire,
+			DiskLoadPolicy: updater.SignaturePolicyWarn,
+		},
+		"all/intel/": nil, // Disable until IntelHub supports signing.
+	}
+
+	// BinarySigningKeys holds the signing keys in text format.
+	BinarySigningKeys = []string{
+		// Safing Code Signing Key #1
+		"recipient:public-ed25519-key:safing-code-signing-key-1:92bgBLneQUWrhYLPpBDjqHbpFPuNVCPAaivQ951A4aq72HcTiw7R1QmPJwFM1mdePAvEVDjkeb8S4fp2pmRCsRa8HrCvWQEjd88rfZ6TznJMfY4g7P8ioGFjfpyx2ZJ8WCZJG5Qt4Z9nkabhxo2Nbi3iywBTYDLSbP5CXqi7jryW7BufWWuaRVufFFzhwUC2ryWFWMdkUmsAZcvXwde4KLN9FrkWAy61fGaJ8GCwGnGCSitANnU2cQrsGBXZzxmzxwrYD",
+		// Safing Code Signing Key #2
+		"recipient:public-ed25519-key:safing-code-signing-key-2:92bgBLneQUWrhYLPpBDjqHbPC2d1o5JMyZFdavWBNVtdvbPfzDewLW95ScXfYPHd3QvWHSWCtB4xpthaYWxSkK1kYiGp68DPa2HaU8yQ5dZhaAUuV4Kzv42pJcWkCeVnBYqgGBXobuz52rFqhDJy3rz7soXEmYhJEJWwLwMeioK3VzN3QmGSYXXjosHMMNC76rjufSoLNtUQUWZDSnHmqbuxbKMCCsjFXUGGhtZVyb7bnu7QLTLk6SKHBJDMB6zdL9sw3",
+	}
+
+	// BinarySigningTrustStore is an in-memory trust store with the signing keys.
+	BinarySigningTrustStore = jess.NewMemTrustStore()
+)
+
+func init() {
+	for _, signingKey := range BinarySigningKeys {
+		rcpt, err := jess.RecipientFromTextFormat(signingKey)
+		if err != nil {
+			panic(err)
+		}
+		err = BinarySigningTrustStore.StoreSignet(rcpt)
+		if err != nil {
+			panic(err)
+		}
+	}
+}
diff --git a/service/updates/helper/updates.go b/service/updates/helper/updates.go
new file mode 100644
index 00000000..efae917d
--- /dev/null
+++ b/service/updates/helper/updates.go
@@ -0,0 +1,95 @@
+package helper
+
+import (
+	"fmt"
+	"runtime"
+
+	"github.com/tevino/abool"
+)
+
+const onWindows = runtime.GOOS == "windows"
+
+var intelOnly = abool.New()
+
+// IntelOnly specifies that only intel data is mandatory.
+func IntelOnly() {
+	intelOnly.Set()
+}
+
+// PlatformIdentifier converts identifier for the current platform.
+func PlatformIdentifier(identifier string) string {
+	// From https://golang.org/pkg/runtime/#GOARCH
+	// GOOS is the running program's operating system target: one of darwin, freebsd, linux, and so on.
+	// GOARCH is the running program's architecture target: one of 386, amd64, arm, s390x, and so on.
+	return fmt.Sprintf("%s_%s/%s", runtime.GOOS, runtime.GOARCH, identifier)
+}
+
+// MandatoryUpdates returns mandatory updates that should be loaded on install
+// or reset.
+func MandatoryUpdates() (identifiers []string) {
+	// Intel
+	identifiers = append(
+		identifiers,
+
+		// Filter lists data
+		"all/intel/lists/index.dsd",
+		"all/intel/lists/base.dsdl",
+		"all/intel/lists/intermediate.dsdl",
+		"all/intel/lists/urgent.dsdl",
+
+		// Geo IP data
+		"all/intel/geoip/geoipv4.mmdb.gz",
+		"all/intel/geoip/geoipv6.mmdb.gz",
+	)
+
+	// Stop here if we only want intel data.
+	if intelOnly.IsSet() {
+		return identifiers
+	}
+
+	// Binaries
+	if onWindows {
+		identifiers = append(
+			identifiers,
+			PlatformIdentifier("core/portmaster-core.exe"),
+			PlatformIdentifier("kext/portmaster-kext.sys"),
+			PlatformIdentifier("kext/portmaster-kext.pdb"),
+			PlatformIdentifier("start/portmaster-start.exe"),
+			PlatformIdentifier("notifier/portmaster-notifier.exe"),
+			PlatformIdentifier("notifier/portmaster-wintoast.dll"),
+			PlatformIdentifier("app2/portmaster-app.zip"),
+		)
+	} else {
+		identifiers = append(
+			identifiers,
+			PlatformIdentifier("core/portmaster-core"),
+			PlatformIdentifier("start/portmaster-start"),
+			PlatformIdentifier("notifier/portmaster-notifier"),
+			PlatformIdentifier("app2/portmaster-app"),
+		)
+	}
+
+	// Components, Assets and Data
+	identifiers = append(
+		identifiers,
+
+		// User interface components
+		PlatformIdentifier("app/portmaster-app.zip"),
+		"all/ui/modules/portmaster.zip",
+		"all/ui/modules/assets.zip",
+	)
+
+	return identifiers
+}
+
+// AutoUnpackUpdates returns assets that need unpacking.
+func AutoUnpackUpdates() []string {
+	if intelOnly.IsSet() {
+		return []string{}
+	}
+
+	return []string{
+		PlatformIdentifier("app/portmaster-app.zip"),
+		PlatformIdentifier("app2/portmaster-app.zip"),
+	}
+}
diff --git a/service/updates/index.go b/service/updates/index.go
deleted file mode 100644
index 8a30f643..00000000
--- a/service/updates/index.go
+++ /dev/null
@@ -1,381 +0,0 @@
-package updates
-
-import (
-	"crypto/sha256"
-	"crypto/subtle"
-	"encoding/hex"
-	"encoding/json"
-	"errors"
-	"fmt"
-	"io"
-	"io/fs"
-	"os"
-	"path/filepath"
-	"runtime"
-	"time"
-
-	semver "github.com/hashicorp/go-version"
-
-	"github.com/safing/jess"
-	"github.com/safing/jess/filesig"
-)
-
-// MaxUnpackSize defines the maximum size that is allowed to be unpacked.
-const MaxUnpackSize = 1 << 30 // 2^30 == 1GB
-
-const currentPlatform = runtime.GOOS + "_" + runtime.GOARCH
-
-var zeroVersion = semver.Must(semver.NewVersion("0.0.0"))
-
-// Artifact represents a single file with metadata.
-type Artifact struct {
-	Filename string   `json:"Filename"`
-	SHA256   string   `json:"SHA256"`
-	URLs     []string `json:"URLs"`
-	Platform string   `json:"Platform,omitempty"`
-	Unpack   string   `json:"Unpack,omitempty"`
-	Version  string   `json:"Version,omitempty"`
-
-	localFile  string
-	versionNum *semver.Version
-}
-
-// GetFileMode returns the required filesystem permission for the artifact.
-func (a *Artifact) GetFileMode() os.FileMode {
-	// Special case for portmaster ui. Should be able to be executed from the regular user
-	if a.Platform == currentPlatform && a.Filename == "portmaster" {
-		return executableUIFileMode
-	}
-
-	if a.Platform == currentPlatform {
-		return executableFileMode
-	}
-
-	return defaultFileMode
-}
-
-// Path returns the absolute path to the local file.
-func (a *Artifact) Path() string {
-	return a.localFile
-}
-
-// SemVer returns the version of the artifact.
-func (a *Artifact) SemVer() *semver.Version {
-	return a.versionNum
-}
-
-// IsNewerThan returns whether the artifact is newer than the given artifact.
-// Returns true if the given artifact is nil.
-// The second return value "ok" is false when version could not be compared.
-// In this case, it is up to the caller to decide how to proceed.
-func (a *Artifact) IsNewerThan(b *Artifact) (newer, ok bool) {
-	switch {
-	case a == nil:
-		return false, false
-	case b == nil:
-		return true, true
-	case a.versionNum == nil:
-		return false, false
-	case b.versionNum == nil:
-		return false, false
-	case a.versionNum.GreaterThan(b.versionNum):
-		return true, true
-	default:
-		return false, true
-	}
-}
-
-func (a *Artifact) export(dir string, indexVersion *semver.Version) *Artifact {
-	copied := &Artifact{
-		Filename:   a.Filename,
-		SHA256:     a.SHA256,
-		URLs:       a.URLs,
-		Platform:   a.Platform,
-		Unpack:     a.Unpack,
-		Version:    a.Version,
-		localFile:  filepath.Join(dir, a.Filename),
-		versionNum: a.versionNum,
-	}
-
-	// Make sure we have a version number.
-	switch {
-	case copied.versionNum != nil:
-		// Version already parsed.
-	case copied.Version != "":
-		// Need to parse version.
-		v, err := semver.NewVersion(copied.Version)
-		if err == nil {
-			copied.versionNum = v
-		}
-	default:
-		// No version defined, inherit index version.
-		copied.versionNum = indexVersion
-	}
-
-	return copied
-}
-
-// Index represents a collection of artifacts with metadata.
-type Index struct {
-	Name      string     `json:"Name"`
-	Version   string     `json:"Version"`
-	Published time.Time  `json:"Published"`
-	Artifacts []Artifact `json:"Artifacts"`
-
-	versionNum *semver.Version
-}
-
-// LoadIndex loads and parses an index from the given filename.
-func LoadIndex(filename string, trustStore jess.TrustStore) (*Index, error) {
-	// Read index file from disk.
-	content, err := os.ReadFile(filename)
-	if err != nil {
-		return nil, fmt.Errorf("read index file: %w", err)
-	}
-
-	// Parse and return.
-	return ParseIndex(content, trustStore)
-}
-
-// ParseIndex parses an index from a json string.
-func ParseIndex(jsonContent []byte, trustStore jess.TrustStore) (*Index, error) {
-	// Verify signature.
-	if trustStore != nil {
-		if err := filesig.VerifyJSONSignature(jsonContent, trustStore); err != nil {
-			return nil, fmt.Errorf("verify: %w", err)
-		}
-	}
-
-	// Parse json.
-	index := &Index{}
-	err := json.Unmarshal(jsonContent, index)
-	if err != nil {
-		return nil, fmt.Errorf("parse index: %w", err)
-	}
-
-	// Initialize data.
-	err = index.init()
-	if err != nil {
-		return nil, err
-	}
-
-	return index, nil
-}
-
-func (index *Index) init() error {
-	// Parse version number, if set.
-	if index.Version != "" {
-		versionNum, err := semver.NewVersion(index.Version)
-		if err != nil {
-			return fmt.Errorf("invalid index version %q: %w", index.Version, err)
-		}
-		index.versionNum = versionNum
-	}
-
-	// Filter artifacts by current platform.
-	filtered := make([]Artifact, 0)
-	for _, a := range index.Artifacts {
-		if a.Platform == "" || a.Platform == currentPlatform {
-			filtered = append(filtered, a)
-		}
-	}
-	index.Artifacts = filtered
-
-	// Parse artifact version numbers.
-	for _, a := range index.Artifacts {
-		if a.Version != "" {
-			v, err := semver.NewVersion(a.Version)
-			if err == nil {
-				a.versionNum = v
-			}
-		} else {
-			a.versionNum = index.versionNum
-		}
-	}
-
-	return nil
-}
-
-// CanDoUpgrades returns whether the index is able to follow a secure upgrade path.
-func (index *Index) CanDoUpgrades() error {
-	switch {
-	case index.versionNum == nil:
-		return errors.New("missing version number")
-
-	case index.Published.IsZero():
-		return errors.New("missing publish date")
-
-	case index.Published.After(time.Now().Add(15 * time.Minute)):
-		return fmt.Errorf("is from the future (%s)", time.Until(index.Published).Round(time.Minute))
-
-	default:
-		return nil
-	}
-}
-
-// ShouldUpgradeTo returns whether the given index is a successor and should be upgraded to.
-func (index *Index) ShouldUpgradeTo(newIndex *Index) error {
-	// Check if both indexes can do upgrades.
-	if err := index.CanDoUpgrades(); err != nil {
-		return fmt.Errorf("current index cannot do upgrades: %w", err)
-	}
-	if err := newIndex.CanDoUpgrades(); err != nil {
-		return fmt.Errorf("new index cannot do upgrade: %w", err)
-	}
-
-	switch {
-	case index.versionNum.Equal(zeroVersion):
-		// The zero version is used for bootstrapping.
-		// Upgrade in any case.
-		return nil
-
-	case index.Name != newIndex.Name:
-		return errors.New("new index name does not match")
-
-	case index.Published.After(newIndex.Published):
-		return errors.New("new index is older (time)")
-
-	case index.versionNum.Segments()[0] > newIndex.versionNum.Segments()[0]:
-		// Downgrades are allowed, if they are not breaking changes.
-		return errors.New("new index is a breaking change downgrade")
-
-	case index.Published.Equal(newIndex.Published):
-		// "Do nothing".
-		return ErrSameIndex
-
-	default:
-		// Upgrade!
-		return nil
-	}
-}
-
-// VerifyArtifacts checks if all artifacts are present in the given dir and have the correct hash.
-func (index *Index) VerifyArtifacts(dir string) error {
-	for _, artifact := range index.Artifacts {
-		err := checkSHA256SumFile(filepath.Join(dir, artifact.Filename), artifact.SHA256)
-		if err != nil {
-			return fmt.Errorf("verify %s: %w", artifact.Filename, err)
-		}
-	}
-
-	return nil
-}
-
-func (index *Index) Export(signingKey *jess.Signet, trustStore jess.TrustStore) ([]byte, error) {
-	// Serialize to json.
-	indexData, err := json.Marshal(index)
-	if err != nil {
-		return nil, fmt.Errorf("serialize: %w", err)
-	}
-
-	// Do not sign if signing key is not given.
-	if signingKey == nil {
-		return indexData, nil
-	}
-
-	// Make envelope.
-	envelope := jess.NewUnconfiguredEnvelope()
-	envelope.SuiteID = jess.SuiteSignV1
-	envelope.Senders = []*jess.Signet{signingKey}
-
-	// Sign json data.
-	signedIndex, err := filesig.AddJSONSignature(indexData, envelope, trustStore)
-	if err != nil {
-		return nil, fmt.Errorf("sign: %w", err)
-	}
-
-	return signedIndex, nil
-}
-
-func checkSHA256SumFile(filename string, sha256sum string) error {
-	// Check expected hash.
-	expectedDigest, err := hex.DecodeString(sha256sum)
-	if err != nil {
-		return fmt.Errorf("invalid hex encoding for expected hash %s: %w", sha256sum, err)
-	}
-	if len(expectedDigest) != sha256.Size {
-		return fmt.Errorf("invalid size for expected hash %s: %w", sha256sum, err)
-	}
-
-	// Open file for checking.
-	file, err := os.Open(filename)
-	if err != nil {
-		return fmt.Errorf("open file: %w", err)
-	}
-	defer func() { _ = file.Close() }()
-
-	// Calculate hash of the file.
-	fileHash := sha256.New()
-	if _, err := io.Copy(fileHash, file); err != nil {
-		return fmt.Errorf("read file: %w", err)
-	}
-	if subtle.ConstantTimeCompare(fileHash.Sum(nil), expectedDigest) != 1 {
-		return errors.New("sha256sum mismatch")
-	}
-
-	return nil
-}
-
-func checkSHA256Sum(fileData []byte, sha256sum string) error {
-	// Check expected hash.
-	expectedDigest, err := hex.DecodeString(sha256sum)
-	if err != nil {
-		return fmt.Errorf("invalid hex encoding for expected hash %s: %w", sha256sum, err)
-	}
-	if len(expectedDigest) != sha256.Size {
-		return fmt.Errorf("invalid size for expected hash %s: %w", sha256sum, err)
-	}
-
-	// Calculate and compare hash of the file.
-	hashSum := sha256.Sum256(fileData)
-	if subtle.ConstantTimeCompare(hashSum[:], expectedDigest) != 1 {
-		return errors.New("sha256sum mismatch")
-	}
-
-	return nil
-}
-
-// copyAndCheckSHA256Sum copies the file from src to dst and check the sha256 sum.
-// As a special case, if the sha256sum is not given, it is not checked.
-func copyAndCheckSHA256Sum(src, dst, sha256sum string, fileMode fs.FileMode) error {
-	// Check expected hash.
-	var expectedDigest []byte
-	if sha256sum != "" {
-		expectedDigest, err := hex.DecodeString(sha256sum)
-		if err != nil {
-			return fmt.Errorf("invalid hex encoding for expected hash %s: %w", sha256sum, err)
-		}
-		if len(expectedDigest) != sha256.Size {
-			return fmt.Errorf("invalid size for expected hash %s: %w", sha256sum, err)
-		}
-	}
-
-	// Read file from source.
-	fileData, err := os.ReadFile(src)
-	if err != nil {
-		return fmt.Errorf("read src file: %w", err)
-	}
-
-	// Calculate and compare hash of the file.
-	if len(expectedDigest) > 0 {
-		hashSum := sha256.Sum256(fileData)
-		if subtle.ConstantTimeCompare(hashSum[:], expectedDigest) != 1 {
-			return errors.New("sha256sum mismatch")
-		}
-	}
-
-	// Write to temporary file.
-	tmpDst := dst + ".copy"
-	err = os.WriteFile(tmpDst, fileData, fileMode)
-	if err != nil {
-		return fmt.Errorf("write temp dst file: %w", err)
-	}
-
-	// Rename/Move to actual location.
-	err = os.Rename(tmpDst, dst)
-	if err != nil {
-		return fmt.Errorf("rename dst file after write: %w", err)
-	}
-
-	return nil
-}
diff --git a/service/updates/index_scan.go b/service/updates/index_scan.go
deleted file mode 100644
index 6e6a6af2..00000000
--- a/service/updates/index_scan.go
+++ /dev/null
@@ -1,342 +0,0 @@
-package updates
-
-import (
-	"crypto/sha256"
-	"encoding/hex"
-	"errors"
-	"fmt"
-	"io/fs"
-	"os"
-	"path"
-	"path/filepath"
-	"regexp"
-	"slices"
-	"strings"
-	"time"
-
-	"github.com/gobwas/glob"
-	semver "github.com/hashicorp/go-version"
-)
-
-type IndexScanConfig struct {
-	Name            string
-	Version         string
-	PrimaryArtifact string
-	BaseURL         string
-
-	Templates   map[string]Artifact
-	IgnoreFiles []string
-	UnpackFiles map[string]string
-
-	cleanedBaseURL   string
-	ignoreFilesGlobs []glob.Glob
-	unpackFilesGlobs map[string]glob.Glob
-}
-
-func (bs *IndexScanConfig) init() error {
-	// Transform base URL into expected format.
-	bs.cleanedBaseURL = strings.TrimSuffix(bs.BaseURL, "/") + "/"
-
-	// Parse ignore files patterns.
-	bs.ignoreFilesGlobs = make([]glob.Glob, 0, len(bs.IgnoreFiles))
-	for _, pattern := range bs.IgnoreFiles {
-		g, err := glob.Compile(pattern, os.PathSeparator)
-		if err != nil {
-			return fmt.Errorf("invalid ingore files pattern %q: %w", pattern, err)
-		}
-		bs.ignoreFilesGlobs = append(bs.ignoreFilesGlobs, g)
-	}
-
-	// Parse unpack files patterns.
-	bs.unpackFilesGlobs = make(map[string]glob.Glob)
-	for setting, pattern := range bs.UnpackFiles {
-		g, err := glob.Compile(pattern, os.PathSeparator)
-		if err != nil {
-			return fmt.Errorf("invalid unpack files pattern %q: %w", pattern, err)
-		}
-		bs.unpackFilesGlobs[setting] = g
-	}
-
-	return nil
-}
-
-// IsIgnored returns whether a filename should be ignored.
-func (bs *IndexScanConfig) IsIgnored(filename string) bool {
-	for _, ignoreGlob := range bs.ignoreFilesGlobs {
-		if ignoreGlob.Match(filename) {
-			return true
-		}
-	}
-
-	return false
-}
-
-// UnpackSetting returns the unpack setings for the given filename.
-func (bs *IndexScanConfig) UnpackSetting(filename string) (string, error) {
-	var foundSetting string
-
-settings:
-	for unpackSetting, matchGlob := range bs.unpackFilesGlobs {
-		switch {
-		case !matchGlob.Match(filename):
-			// Check next if glob does not match.
-			continue settings
-		case foundSetting == "":
-			// First find, save setting.
-			foundSetting = unpackSetting
-		case foundSetting != unpackSetting:
-			// Additional find, and setting is not the same.
-			return "", errors.New("matches contradicting unpack settings")
-		}
-	}
-
-	return foundSetting, nil
-}
-
-// GenerateIndexFromDir generates a index from a given folder.
-func GenerateIndexFromDir(sourceDir string, cfg IndexScanConfig) (*Index, error) { //nolint:maintidx
-	artifacts := make(map[string]Artifact)
-
-	// Initialize.
-	err := cfg.init()
-	if err != nil {
-		return nil, fmt.Errorf("invalid index scan config: %w", err)
-	}
-	sourceDir, err = filepath.Abs(sourceDir)
-	if err != nil {
-		return nil, fmt.Errorf("invalid index dir: %w", err)
-	}
-	var indexVersion *semver.Version
-	if cfg.Version != "" {
-		indexVersion, err = semver.NewVersion(cfg.Version)
-		if err != nil {
-			return nil, fmt.Errorf("invalid index version: %w", err)
-		}
-	}
-
-	err = filepath.WalkDir(sourceDir, func(fullpath string, d fs.DirEntry, err error) error {
-		// Fail on access error.
-		if err != nil {
-			return err
-		}
-
-		// Step 1: Extract information and check ignores.
-
-		// Skip folders.
-		if d.IsDir() {
-			return nil
-		}
-
-		// Get relative path for processing.
-		relpath, err := filepath.Rel(sourceDir, fullpath)
-		if err != nil {
-			return fmt.Errorf("invalid relative path for %s: %w", fullpath, err)
-		}
-
-		// Check if file is in the ignore list.
-		if cfg.IsIgnored(relpath) {
-			return nil
-		}
-
-		// Extract version, if present.
-		identifier, version, ok := getIdentifierAndVersion(d.Name())
-		if !ok {
-			// Fallback to using filename as identifier, which is normal for the simplified system.
-			identifier = d.Name()
-			version = ""
-		}
-		var versionNum *semver.Version
-		if version != "" {
-			versionNum, err = semver.NewVersion(version)
-			if err != nil {
-				return fmt.Errorf("invalid version %s for %s: %w", relpath, version, err)
-			}
-		}
-
-		// Extract platform.
-		platform := "all"
-		before, _, found := strings.Cut(relpath, string(os.PathSeparator))
-		if found {
-			platform = before
-		}
-
-		// Step 2: Check and compare file version.
-
-		// Make the key platform specific since there can be same filename for multiple platforms.
-		key := platform + "/" + identifier
-		existing, ok := artifacts[key]
-		if ok {
-			// Check for duplicates and mixed versioned/non-versioned.
-			switch {
-			case existing.Version == version:
-				return fmt.Errorf("duplicate version for %s: %s and %s", key, existing.localFile, fullpath)
-			case (existing.Version == "") != (version == ""):
-				return fmt.Errorf("both a versioned and non-versioned file for: %s: %s and %s", key, existing.localFile, fullpath)
-			}
-
-			// Compare versions.
-			existingVersion, _ := semver.NewVersion(existing.Version)
-			switch {
-			case existingVersion.Equal(versionNum):
-				return fmt.Errorf("duplicate version for %s: %s and %s", key, existing.localFile, fullpath)
-			case existingVersion.GreaterThan(versionNum):
-				// New version is older, skip.
-				return nil
-			}
-		}
-
-		// Step 3: Create new Artifact.
-
-		artifact := Artifact{}
-
-		// Check if the caller provided a template for the artifact.
-		if t, ok := cfg.Templates[identifier]; ok {
-			artifact = t
-		}
-
-		// Set artifact properties.
-		if artifact.Filename == "" {
-			artifact.Filename = identifier
-		}
-		if len(artifact.URLs) == 0 && cfg.BaseURL != "" {
-			artifact.URLs = []string{cfg.cleanedBaseURL + relpath}
-		}
-		if artifact.Platform == "" {
-			artifact.Platform = platform
-		}
-		if artifact.Unpack == "" {
-			unpackSetting, err := cfg.UnpackSetting(relpath)
-			if err != nil {
-				return fmt.Errorf("invalid unpack setting for %s at %s: %w", key, relpath, err)
-			}
-			artifact.Unpack = unpackSetting
-		}
-		if artifact.Version == "" {
-			artifact.Version = version
-		}
-
-		// Remove unpack suffix.
-		if artifact.Unpack != "" {
-			artifact.Filename, _ = strings.CutSuffix(artifact.Filename, "."+artifact.Unpack)
-		}
-
-		// Set local file path.
-		artifact.localFile = fullpath
-
-		// Save new artifact to map.
-		artifacts[key] = artifact
-		return nil
-	})
-	if err != nil {
-		return nil, fmt.Errorf("scanning dir: %w", err)
-	}
-
-	// Create base index.
-	index := &Index{
-		Name:       cfg.Name,
-		Version:    cfg.Version,
-		Published:  time.Now(),
-		versionNum: indexVersion,
-	}
-	if index.Version == "" && cfg.PrimaryArtifact != "" {
-		pv, ok := artifacts[cfg.PrimaryArtifact]
-		if ok {
-			index.Version = pv.Version
-		}
-	}
-	if index.Name == "" {
-		index.Name = strings.Trim(filepath.Base(sourceDir), "./\\")
-	}
-
-	// Convert to slice and compute hashes.
-	export := make([]Artifact, 0, len(artifacts))
-	for _, artifact := range artifacts {
-		// Compute hash.
-		hash, err := getSHA256(artifact.localFile, artifact.Unpack)
-		if err != nil {
-			return nil, fmt.Errorf("calculate hash of file: %s %w", artifact.localFile, err)
-		}
-		artifact.SHA256 = hash
-
-		// Remove "all" platform IDs.
-		if artifact.Platform == "all" {
-			artifact.Platform = ""
-		}
-
-		// Remove default versions.
-		if artifact.Version == index.Version {
-			artifact.Version = ""
-		}
-
-		// Add to export slice.
-		export = append(export, artifact)
-	}
-
-	// Sort final artifacts.
-	slices.SortFunc(export, func(a, b Artifact) int {
-		switch {
-		case a.Filename != b.Filename:
-			return strings.Compare(a.Filename, b.Filename)
-		case a.Platform != b.Platform:
-			return strings.Compare(a.Platform, b.Platform)
-		case a.Version != b.Version:
-			return strings.Compare(a.Version, b.Version)
-		case a.SHA256 != b.SHA256:
-			return strings.Compare(a.SHA256, b.SHA256)
-		default:
-			return 0
-		}
-	})
-
-	// Assign and return.
-	index.Artifacts = export
-	return index, nil
-}
-
-func getSHA256(path string, unpackType string) (string, error) {
-	content, err := os.ReadFile(path)
-	if err != nil {
-		return "", err
-	}
-
-	// Decompress if compression was applied to the file.
-	if unpackType != "" {
-		content, err = decompress(unpackType, content)
-		if err != nil {
-			return "", err
-		}
-	}
-
-	// Calculate hash
-	hash := sha256.Sum256(content)
-	return hex.EncodeToString(hash[:]), nil
-}
-
-var fileVersionRegex = regexp.MustCompile(`_v[0-9]+-[0-9]+-[0-9]+(-[a-z]+)?`)
-
-func getIdentifierAndVersion(versionedPath string) (identifier, version string, ok bool) {
-	dirPath, filename := path.Split(versionedPath)
-
-	// Extract version from filename.
-	rawVersion := fileVersionRegex.FindString(filename)
-	if rawVersion == "" {
-		// No version present in file, making it invalid.
-		return "", "", false
-	}
-
-	// Trim the `_v` that gets caught by the regex and
-	// replace `-` with `.` to get the version string.
-	version = strings.Replace(strings.TrimLeft(rawVersion, "_v"), "-", ".", 2)
-
-	// Put the filename back together without version.
-	i := strings.Index(filename, rawVersion)
-	if i < 0 {
-		// extracted version not in string (impossible)
-		return "", "", false
-	}
-	filename = filename[:i] + filename[i+len(rawVersion):]
-
-	// Put the full path back together and return it.
-	// `dirPath + filename` is guaranteed by path.Split()
-	return dirPath + filename, version, true
-}
diff --git a/service/updates/main.go b/service/updates/main.go
new file mode 100644
index 00000000..bb942993
--- /dev/null
+++ b/service/updates/main.go
@@ -0,0 +1,318 @@
+package updates
+
+import (
+	"context"
+	"errors"
+	"flag"
+	"fmt"
+	"net/url"
+	"runtime"
+	"time"
+
+	"github.com/safing/portmaster/base/database"
+	"github.com/safing/portmaster/base/dataroot"
+	"github.com/safing/portmaster/base/log"
+	"github.com/safing/portmaster/base/updater"
+	"github.com/safing/portmaster/service/mgr"
+	"github.com/safing/portmaster/service/updates/helper"
+)
+
+const (
+	onWindows = runtime.GOOS == "windows"
+
+	enableSoftwareUpdatesKey = "core/automaticUpdates"
+	enableIntelUpdatesKey    = "core/automaticIntelUpdates"
+
+	// ModuleName is the name of the update module
+	// and can be used when declaring module dependencies.
+	ModuleName = "updates"
+
+	// VersionUpdateEvent is emitted every time a new
+	// version of a monitored resource is selected.
+	// During module initialization VersionUpdateEvent
+	// is also emitted.
+	VersionUpdateEvent = "active version update"
+
+	// ResourceUpdateEvent is emitted every time the
+	// updater successfully performed a resource update.
+	// ResourceUpdateEvent is emitted even if no new
+	// versions are available. Subscribers are expected
+	// to check if new versions of their resources are
+	// available by checking File.UpgradeAvailable().
+	ResourceUpdateEvent = "resource update"
+)
+
+var (
+	registry *updater.ResourceRegistry
+
+	userAgentFromFlag    string
+	updateServerFromFlag string
+
+	updateASAP          bool
+	disableTaskSchedule bool
+
+	db = database.NewInterface(&database.Options{
+		Local:    true,
+		Internal: true,
+	})
+
+	// UserAgent is an HTTP User-Agent that is used to add
+	// more context to requests made by the registry when
+	// fetching resources from the update server.
+	UserAgent = fmt.Sprintf("Portmaster (%s %s)", runtime.GOOS, runtime.GOARCH)
+
+	// DefaultUpdateURLs defines the default base URLs of the update server.
+	DefaultUpdateURLs = []string{
+		"https://updates.safing.io",
+	}
+
+	// DisableSoftwareAutoUpdate specifies whether software updates should be disabled.
+	// This is used on Android, as it will never require binary updates.
+	DisableSoftwareAutoUpdate = false
+)
+
+const (
+	updatesDirName = "updates"
+
+	updateTaskRepeatDuration = 1 * time.Hour
+)
+
+func init() {
+	flag.StringVar(&updateServerFromFlag, "update-server", "", "set an alternative update server (full URL)")
+	flag.StringVar(&userAgentFromFlag, "update-agent", "", "set an alternative user agent for requests to the update server")
+}
+
+func prep() error {
+	// Check if update server URL supplied via flag is a valid URL.
+	if updateServerFromFlag != "" {
+		u, err := url.Parse(updateServerFromFlag)
+		if err != nil {
+			return fmt.Errorf("supplied update server URL is invalid: %w", err)
+		}
+		if u.Scheme != "https" {
+			return errors.New("supplied update server URL must use HTTPS")
+		}
+	}
+
+	if err := registerConfig(); err != nil {
+		return err
+	}
+
+	return registerAPIEndpoints()
+}
+
+func start() error {
+	initConfig()
+
+	module.restartWorkerMgr.Repeat(10 * time.Minute)
+	module.instance.Config().EventConfigChange.AddCallback("update registry config", updateRegistryConfig)
+
+	// create registry
+	registry = &updater.ResourceRegistry{
+		Name:             ModuleName,
+		UpdateURLs:       DefaultUpdateURLs,
+		UserAgent:        UserAgent,
+		MandatoryUpdates: helper.MandatoryUpdates(),
+		AutoUnpack:       helper.AutoUnpackUpdates(),
+		Verification:     helper.VerificationConfig,
+		DevMode:          devMode(),
+		Online:           true,
+	}
+	// Override values from flags.
+	if userAgentFromFlag != "" {
+		registry.UserAgent = userAgentFromFlag
+	}
+	if updateServerFromFlag != "" {
+		registry.UpdateURLs = []string{updateServerFromFlag}
+	}
+
+	// pre-init state
+	updateStateExport, err := LoadStateExport()
+	if err != nil {
+		log.Debugf("updates: failed to load exported update state: %s", err)
+	} else if updateStateExport.UpdateState != nil {
+		err := registry.PreInitUpdateState(*updateStateExport.UpdateState)
+		if err != nil {
+			return err
+		}
+	}
+
+	// initialize
+	err = registry.Initialize(dataroot.Root().ChildDir(updatesDirName, 0o0755))
+	if err != nil {
+		return err
+	}
+
+	// register state provider
+	err = registerRegistryStateProvider()
+	if err != nil {
+		return err
+	}
+	registry.StateNotifyFunc = pushRegistryState
+
+	// Set indexes based on the release channel.
+	warning := helper.SetIndexes(
+		registry,
+		initialReleaseChannel,
+		true,
+		enableSoftwareUpdates() && !DisableSoftwareAutoUpdate,
+		enableIntelUpdates(),
+	)
+	if warning != nil {
+		log.Warningf("updates: %s", warning)
+	}
+
+	err = registry.LoadIndexes(module.m.Ctx())
+	if err != nil {
+		log.Warningf("updates: failed to load indexes: %s", err)
+	}
+
+	err = registry.ScanStorage("")
+	if err != nil {
+		log.Warningf("updates: error during storage scan: %s", err)
+	}
+
+	registry.SelectVersions()
+	module.EventVersionsUpdated.Submit(struct{}{})
+
+	// Initialize the version export - this requires the registry to be set up.
+	err = initVersionExport()
+	if err != nil {
+		return err
+	}
+
+	// start updater task
+	if !disableTaskSchedule {
+		_ = module.updateWorkerMgr.Repeat(30 * time.Minute)
+	}
+
+	if updateASAP {
+		module.updateWorkerMgr.Go()
+	}
+
+	// react to upgrades
+	if err := initUpgrader(); err != nil {
+		return err
+	}
+
+	warnOnIncorrectParentPath()
+
+	return nil
+}
+
+// TriggerUpdate queues the update task to execute ASAP.
+func TriggerUpdate(forceIndexCheck, downloadAll bool) error {
+	switch {
+	case !forceIndexCheck && !enableSoftwareUpdates() && !enableIntelUpdates():
+		return errors.New("automatic updating is disabled")
+
+	default:
+		if forceIndexCheck {
+			forceCheck.Set()
+		}
+		if downloadAll {
+			forceDownload.Set()
+		}
+
+		// If index check if forced, start quicker.
+		module.updateWorkerMgr.Go()
+	}
+
+	log.Debugf("updates: triggering update to run as soon as possible")
+	return nil
+}
+
+// DisableUpdateSchedule disables the update schedule.
+// If called, updates are only checked when TriggerUpdate()
+// is called.
+func DisableUpdateSchedule() error {
+	// TODO: Updater state should be always on
+	// switch module.Status() {
+	// case modules.StatusStarting, modules.StatusOnline, modules.StatusStopping:
+	// 	return errors.New("module already online")
+	// }
+
+	disableTaskSchedule = true
+
+	return nil
+}
+
+func checkForUpdates(ctx *mgr.WorkerCtx) (err error) {
+	// Set correct error if context was canceled.
+	defer func() {
+		select {
+		case <-ctx.Done():
+			err = context.Canceled
+		default:
+		}
+	}()
+
+	// Get flags.
+	forceIndexCheck := forceCheck.SetToIf(true, false)
+	downloadAll := forceDownload.SetToIf(true, false)
+
+	// Check again if downloading updates is enabled, or forced.
+	if !forceIndexCheck && !enableSoftwareUpdates() && !enableIntelUpdates() {
+		log.Warningf("updates: automatic updates are disabled")
+		return nil
+	}
+
+	defer func() {
+		// Resolve any error and send success notification.
+		if err == nil {
+			log.Infof("updates: successfully checked for updates")
+			notifyUpdateSuccess(forceIndexCheck)
+			return
+		}
+
+		// Log and notify error.
+		log.Errorf("updates: check failed: %s", err)
+		notifyUpdateCheckFailed(forceIndexCheck, err)
+	}()
+
+	if err = registry.UpdateIndexes(ctx.Ctx()); err != nil {
+		err = fmt.Errorf("failed to update indexes: %w", err)
+		return //nolint:nakedret // TODO: Would "return err" work with the defer?
+	}
+
+	err = registry.DownloadUpdates(ctx.Ctx(), downloadAll)
+	if err != nil {
+		err = fmt.Errorf("failed to download updates: %w", err)
+		return //nolint:nakedret // TODO: Would "return err" work with the defer?
+	}
+
+	registry.SelectVersions()
+
+	// Unpack selected resources.
+	err = registry.UnpackResources()
+	if err != nil {
+		err = fmt.Errorf("failed to unpack updates: %w", err)
+		return //nolint:nakedret // TODO: Would "return err" work with the defer?
+	}
+
+	// Purge old resources
+	registry.Purge(2)
+
+	module.EventResourcesUpdated.Submit(struct{}{})
+	return nil
+}
+
+func stop() error {
+	if registry != nil {
+		err := registry.Cleanup()
+		if err != nil {
+			log.Warningf("updates: failed to clean up registry: %s", err)
+		}
+	}
+
+	return nil
+}
+
+// RootPath returns the root path used for storing updates.
+func RootPath() string {
+	// if !module.Online() {
+	// 	return ""
+	// }
+
+	return registry.StorageDir().Path
+}
diff --git a/service/updates/module.go b/service/updates/module.go
index e5b32129..c7cae1a9 100644
--- a/service/updates/module.go
+++ b/service/updates/module.go
@@ -2,517 +2,80 @@ package updates
 
 import (
 	"errors"
-	"fmt"
-	"os"
-	"path/filepath"
-	"runtime"
-	"strings"
-	"sync"
-	"time"
+	"sync/atomic"
 
-	"github.com/tevino/abool"
-
-	"github.com/safing/jess"
-	"github.com/safing/portmaster/base/log"
+	"github.com/safing/portmaster/base/api"
+	"github.com/safing/portmaster/base/config"
 	"github.com/safing/portmaster/base/notifications"
 	"github.com/safing/portmaster/service/mgr"
 )
 
-const (
-	updateTaskRepeatDuration          = 1 * time.Hour
-	noNewUpdateNotificationID         = "updates:no-new-update"
-	updateAvailableNotificationID     = "updates:update-available"
-	restartRequiredNotificationID     = "updates:restart-required"
-	updateFailedNotificationID        = "updates:update-failed"
-	corruptInstallationNotificationID = "updates:corrupt-installation"
-
-	// ResourceUpdateEvent is emitted every time the
-	// updater successfully performed a resource update.
-	ResourceUpdateEvent = "resource update"
-)
-
-// UserAgent is an HTTP User-Agent that is used to add
-// more context to requests made by the registry when
-// fetching resources from the update server.
-var UserAgent = fmt.Sprintf("Portmaster (%s %s)", runtime.GOOS, runtime.GOARCH)
-
-// Errors.
-var (
-	ErrNotFound  = errors.New("file not found")
-	ErrSameIndex = errors.New("same index")
-
-	ErrAutoCheckDisabled = errors.New("automatic update checks are disabled")
-	ErrNoUpdateAvailable = errors.New("no update available")
-	ErrActionRequired    = errors.New("action required")
-)
-
-// Config holds the configuration for the updates module.
-type Config struct {
-	// Name of the updater.
-	Name string
-	// Directory is the main directory where the currently to-be-used artifacts live.
-	Directory string
-	// DownloadDirectory is the directory where new artifacts are downloaded to and prepared for upgrading.
-	// After the upgrade, this directory is cleared.
-	DownloadDirectory string
-	// PurgeDirectory is the directory where old artifacts are moved to during the upgrade process.
-	// After the upgrade, this directory is cleared.
-	PurgeDirectory string
-	// Ignore defines file and directory names within the main directory that should be ignored during the upgrade.
-	Ignore []string
-
-	// IndexURLs defines file
-	IndexURLs []string
-	// IndexFile is the name of the index file used in the directories.
-	IndexFile string
-	// Verify enables and specifies the trust the index signatures will be checked against.
-	Verify jess.TrustStore
-
-	// AutoCheck defines that new indexes may be downloaded automatically without outside trigger.
-	AutoCheck bool
-	// AutoDownload defines that updates may be downloaded automatically without outside trigger.
-	AutoDownload bool
-	// AutoApply defines that updates may be automatically applied without outside trigger.
-	// Requires AutoDownload the be enabled.
-	AutoApply bool
-	// NeedsRestart defines that a restart is required after an upgrade has been completed.
-	// Restart is triggered automatically, if Notify is disabled.
-	NeedsRestart bool
-	// Notify defines whether the user shall be informed about events via notifications.
-	// If enabled, disables automatic restart after upgrade.
-	Notify bool
-}
-
-// Check looks for obvious configuration errors.
-func (cfg *Config) Check() error {
-	// Check if required fields are set.
-	switch {
-	case cfg.Name == "":
-		return errors.New("name must be set")
-	case cfg.Directory == "":
-		return errors.New("directory must be set")
-	case cfg.DownloadDirectory == "":
-		return errors.New("download directory must be set")
-	case cfg.PurgeDirectory == "":
-		return errors.New("purge directory must be set")
-	case cfg.IndexFile == "":
-		return errors.New("index file must be set")
-	case cfg.AutoApply && !cfg.AutoDownload:
-		return errors.New("auto apply is set, but auto download is not")
-	}
-
-	// Check if Ignore contains paths.
-	for i, s := range cfg.Ignore {
-		if strings.ContainsRune(s, filepath.Separator) {
-			return fmt.Errorf("ignore entry #%d invalid: must be file or directory name, not path", i+1)
-		}
-	}
-
-	// Check if IndexURLs are HTTPS.
-	for i, url := range cfg.IndexURLs {
-		if !strings.HasPrefix(url, "https://") {
-			return fmt.Errorf("index URL #%d invalid: is not a HTTPS url", i+1)
-		}
-	}
-
-	return nil
-}
-
-// Updater provides access to released artifacts.
-type Updater struct {
+// Updates provides access to released artifacts.
+type Updates struct {
 	m      *mgr.Manager
 	states *mgr.StateMgr
-	cfg    Config
 
-	index     *Index
-	indexLock sync.Mutex
-
-	updateCheckWorkerMgr *mgr.WorkerMgr
-	upgradeWorkerMgr     *mgr.WorkerMgr
+	updateWorkerMgr  *mgr.WorkerMgr
+	restartWorkerMgr *mgr.WorkerMgr
 
 	EventResourcesUpdated *mgr.EventMgr[struct{}]
-
-	corruptedInstallation bool
-
-	isUpdateRunning *abool.AtomicBool
+	EventVersionsUpdated  *mgr.EventMgr[struct{}]
 
 	instance instance
 }
 
-// New returns a new Updates module.
-func New(instance instance, name string, cfg Config) (*Updater, error) {
-	m := mgr.New(name)
-	module := &Updater{
+var (
+	module     *Updates
+	shimLoaded atomic.Bool
+)
+
+// New returns a new UI module.
+func New(instance instance) (*Updates, error) {
+	if !shimLoaded.CompareAndSwap(false, true) {
+		return nil, errors.New("only one instance allowed")
+	}
+
+	m := mgr.New("Updates")
+	module = &Updates{
 		m:      m,
 		states: m.NewStateMgr(),
-		cfg:    cfg,
 
+		updateWorkerMgr:       m.NewWorkerMgr("updater", checkForUpdates, nil),
+		restartWorkerMgr:      m.NewWorkerMgr("automatic restart", automaticRestart, nil),
 		EventResourcesUpdated: mgr.NewEventMgr[struct{}](ResourceUpdateEvent, m),
-
-		isUpdateRunning: abool.NewBool(false),
-
-		instance: instance,
+		EventVersionsUpdated:  mgr.NewEventMgr[struct{}](VersionUpdateEvent, m),
+		instance:              instance,
+	}
+	if err := prep(); err != nil {
+		return nil, err
 	}
 
-	// Check config.
-	if err := module.cfg.Check(); err != nil {
-		return nil, fmt.Errorf("config is invalid: %w", err)
-	}
-
-	// Create Workers.
-	module.updateCheckWorkerMgr = m.NewWorkerMgr("update checker", module.updateCheckWorker, nil)
-	module.upgradeWorkerMgr = m.NewWorkerMgr("upgrader", module.upgradeWorker, nil)
-
-	// Load index.
-	index, err := LoadIndex(filepath.Join(cfg.Directory, cfg.IndexFile), cfg.Verify)
-	if err == nil {
-		module.index = index
-		return module, nil
-	}
-
-	// Fall back to scanning the directory.
-	if !errors.Is(err, os.ErrNotExist) {
-		log.Errorf("updates/%s: invalid index file, falling back to dir scan: %s", cfg.Name, err)
-	}
-	index, err = GenerateIndexFromDir(cfg.Directory, IndexScanConfig{Version: "0.0.0"})
-	if err == nil && index.init() == nil {
-		module.index = index
-		return module, nil
-	}
-
-	// Fall back to empty index.
 	return module, nil
 }
 
-func (u *Updater) updateAndUpgrade(w *mgr.WorkerCtx, indexURLs []string, ignoreVersion, forceApply bool) (err error) { //nolint:maintidx
-	// Make sure only one update process is running.
-	if !u.isUpdateRunning.SetToIf(false, true) {
-		return fmt.Errorf("an updater task is already running, please try again later")
-	}
-	defer u.isUpdateRunning.UnSet()
-
-	// Create a new downloader.
-	downloader := NewDownloader(u, indexURLs)
-
-	// Update or load the index file.
-	if len(indexURLs) > 0 {
-		// Download fresh copy, if indexURLs are given.
-		err = downloader.updateIndex(w.Ctx())
-		if err != nil {
-			return fmt.Errorf("update index file: %w", err)
-		}
-	} else {
-		// Otherwise, load index from download dir.
-		downloader.index, err = LoadIndex(filepath.Join(u.cfg.DownloadDirectory, u.cfg.IndexFile), u.cfg.Verify)
-		if err != nil {
-			return fmt.Errorf("load previously downloaded index file: %w", err)
-		}
-	}
-
-	// Get index to check version.
-	u.indexLock.Lock()
-	index := u.index
-	u.indexLock.Unlock()
-
-	// Check if there is a new version.
-	if !ignoreVersion && index != nil {
-		// Check with local pointer to index.
-		if err := index.ShouldUpgradeTo(downloader.index); err != nil {
-			if errors.Is(err, ErrSameIndex) {
-				log.Infof("updates/%s: no new update", u.cfg.Name)
-				if u.cfg.Notify && u.instance.Notifications() != nil {
-					u.instance.Notifications().Notify(&notifications.Notification{
-						EventID: noNewUpdateNotificationID,
-						Type:    notifications.Info,
-						Title:   "Portmaster Is Up-To-Date",
-						Message: "Portmaster v" + index.Version + " is the newest version.",
-						Expires: time.Now().Add(1 * time.Minute).Unix(),
-						AvailableActions: []*notifications.Action{
-							{
-								ID:   "ack",
-								Text: "OK",
-							},
-						},
-					})
-				}
-			} else {
-				log.Warningf("updates/%s: cannot update: %s", u.cfg.Name, err)
-				if u.cfg.Notify && u.instance.Notifications() != nil {
-					u.instance.Notifications().Notify(&notifications.Notification{
-						EventID: noNewUpdateNotificationID,
-						Type:    notifications.Info,
-						Title:   "Portmaster Is Up-To-Date*",
-						Message: "While Portmaster v" + index.Version + " is the newest version, there is an internal issue with checking for updates: " + err.Error(),
-						Expires: time.Now().Add(1 * time.Minute).Unix(),
-						AvailableActions: []*notifications.Action{
-							{
-								ID:   "ack",
-								Text: "OK",
-							},
-						},
-					})
-				}
-			}
-			return fmt.Errorf("%w: %w", ErrNoUpdateAvailable, err)
-		}
-	}
-
-	// Check if automatic downloads are enabled.
-	if !u.cfg.AutoDownload && !forceApply {
-		if u.cfg.Notify && u.instance.Notifications() != nil {
-			u.instance.Notifications().Notify(&notifications.Notification{
-				EventID: updateAvailableNotificationID,
-				Type:    notifications.Info,
-				Title:   "New Update Available",
-				Message: "Portmaster v" + downloader.index.Version + " is available. Click Upgrade to download and upgrade now.",
-				AvailableActions: []*notifications.Action{
-					{
-						ID:   "ack",
-						Text: "OK",
-					},
-					{
-						ID:   "upgrade",
-						Text: "Upgrade Now",
-						Type: notifications.ActionTypeWebhook,
-						Payload: notifications.ActionTypeWebhookPayload{
-							Method: "POST",
-							URL:    "updates/apply",
-						},
-					},
-				},
-			})
-		}
-		return fmt.Errorf("%w: apply updates to download and upgrade", ErrActionRequired)
-	}
-
-	// Check for existing resources before starting to download.
-	_ = downloader.gatherExistingFiles(u.cfg.Directory)         // Artifacts are re-used between versions.
-	_ = downloader.gatherExistingFiles(u.cfg.DownloadDirectory) // Previous download may have been interrupted.
-	_ = downloader.gatherExistingFiles(u.cfg.PurgeDirectory)    // Revover faster from a failed upgrade.
-
-	// Download any remaining needed files.
-	// If everything is already found in the download directory, then this is a no-op.
-	log.Infof("updates/%s: downloading new version: %s %s", u.cfg.Name, downloader.index.Name, downloader.index.Version)
-	err = downloader.downloadArtifacts(w.Ctx())
-	if err != nil {
-		log.Errorf("updates/%s: failed to download update: %s", u.cfg.Name, err)
-		if err := u.deleteUnfinishedFiles(u.cfg.DownloadDirectory); err != nil {
-			log.Debugf("updates/%s: failed to delete unfinished files in download directory %s", u.cfg.Name, u.cfg.DownloadDirectory)
-		}
-		return fmt.Errorf("downloading failed: %w", err)
-	}
-
-	// Notify the user that an upgrade is available.
-	if !u.cfg.AutoApply && !forceApply {
-		if u.cfg.Notify && u.instance.Notifications() != nil {
-			u.instance.Notifications().Notify(&notifications.Notification{
-				EventID: updateAvailableNotificationID,
-				Type:    notifications.Info,
-				Title:   "New Update Ready",
-				Message: "Portmaster v" + downloader.index.Version + " is available. Click Upgrade to upgrade now.",
-				AvailableActions: []*notifications.Action{
-					{
-						ID:   "ack",
-						Text: "OK",
-					},
-					{
-						ID:   "upgrade",
-						Text: "Upgrade Now",
-						Type: notifications.ActionTypeWebhook,
-						Payload: notifications.ActionTypeWebhookPayload{
-							Method: "POST",
-							URL:    "updates/apply",
-						},
-					},
-				},
-			})
-		}
-		return fmt.Errorf("%w: apply updates to download and upgrade", ErrActionRequired)
-	}
-
-	// Run upgrade procedure.
-	err = u.upgrade(downloader, ignoreVersion)
-	if err != nil {
-		if err := u.deleteUnfinishedFiles(u.cfg.PurgeDirectory); err != nil {
-			log.Debugf("updates/%s: failed to delete unfinished files in purge directory %s", u.cfg.Name, u.cfg.PurgeDirectory)
-		}
-		return err
-	}
-
-	// Install is complete!
-
-	// Clean up and notify modules of changed files.
-	err = u.cleanupAfterUpgrade()
-	if err != nil {
-		log.Debugf("updates/%s: failed to clean up after upgrade: %s", u.cfg.Name, err)
-	}
-	u.EventResourcesUpdated.Submit(struct{}{})
-
-	// If no restart is needed, we are done.
-	if !u.cfg.NeedsRestart {
-		return nil
-	}
-
-	// Notify user that a restart is required.
-	if u.cfg.Notify {
-		if u.instance.Notifications() != nil {
-			u.instance.Notifications().Notify(&notifications.Notification{
-				EventID: restartRequiredNotificationID,
-				Type:    notifications.Info,
-				Title:   "Restart Required",
-				Message: "Portmaster v" + downloader.index.Version + " is installed. Restart to use new version.",
-				AvailableActions: []*notifications.Action{
-					{
-						ID:   "ack",
-						Text: "Later",
-					},
-					{
-						ID:   "restart",
-						Text: "Restart Now",
-						Type: notifications.ActionTypeWebhook,
-						Payload: notifications.ActionTypeWebhookPayload{
-							Method: "POST",
-							URL:    "updates/apply",
-						},
-					},
-				},
-			})
-		}
-
-		return fmt.Errorf("%w: restart required", ErrActionRequired)
-	}
-
-	// Otherwise, trigger restart immediately.
-	u.instance.Restart()
-	return nil
-}
-
-func (u *Updater) updateCheckWorker(w *mgr.WorkerCtx) error {
-	err := u.updateAndUpgrade(w, u.cfg.IndexURLs, false, false)
-	switch {
-	case err == nil:
-		return nil // Success!
-	case errors.Is(err, ErrSameIndex):
-		return nil // Nothing to do.
-	case errors.Is(err, ErrNoUpdateAvailable):
-		return nil // Already logged.
-	case errors.Is(err, ErrActionRequired) && !u.cfg.Notify:
-		return fmt.Errorf("user action required, but notifying user is disabled: %w", err)
-	default:
-		return fmt.Errorf("udpating failed: %w", err)
-	}
-}
-
-func (u *Updater) upgradeWorker(w *mgr.WorkerCtx) error {
-	err := u.updateAndUpgrade(w, u.cfg.IndexURLs, false, true)
-	switch {
-	case err == nil:
-		return nil // Success!
-	case errors.Is(err, ErrSameIndex):
-		return nil // Nothing to do.
-	case errors.Is(err, ErrNoUpdateAvailable):
-		return nil // Already logged.
-	case errors.Is(err, ErrActionRequired) && !u.cfg.Notify:
-		return fmt.Errorf("user action required, but notifying user is disabled: %w", err)
-	default:
-		return fmt.Errorf("udpating failed: %w", err)
-	}
-}
-
-// ForceUpdate executes a forced update and upgrade directly and synchronously
-// and is intended to be used only within a tool, not a service.
-func (u *Updater) ForceUpdate() error {
-	return u.m.Do("update and upgrade", func(w *mgr.WorkerCtx) error {
-		return u.updateAndUpgrade(w, u.cfg.IndexURLs, true, true)
-	})
-}
-
-// UpdateFromURL installs an update from the provided url.
-func (u *Updater) UpdateFromURL(url string) error {
-	u.m.Go("custom update from url", func(w *mgr.WorkerCtx) error {
-		_ = u.updateAndUpgrade(w, []string{url}, true, true)
-		return nil
-	})
-
-	return nil
-}
-
-// TriggerUpdateCheck triggers an update check.
-func (u *Updater) TriggerUpdateCheck() {
-	u.updateCheckWorkerMgr.Go()
-}
-
-// TriggerApplyUpdates triggers upgrade.
-func (u *Updater) TriggerApplyUpdates() {
-	u.upgradeWorkerMgr.Go()
-}
-
 // States returns the state manager.
-func (u *Updater) States() *mgr.StateMgr {
+func (u *Updates) States() *mgr.StateMgr {
 	return u.states
 }
 
 // Manager returns the module manager.
-func (u *Updater) Manager() *mgr.Manager {
+func (u *Updates) Manager() *mgr.Manager {
 	return u.m
 }
 
 // Start starts the module.
-func (u *Updater) Start() error {
-	if u.corruptedInstallation && u.cfg.Notify && u.instance.Notifications() != nil {
-		// FIXME: this might make sense as a module state
-		u.instance.Notifications().NotifyError(
-			corruptInstallationNotificationID,
-			"Install Corruption",
-			"Portmaster has detected that one or more of its own files have been corrupted. Please re-install the software.",
-		)
-	}
-
-	// Check for updates automatically, if enabled.
-	if u.cfg.AutoCheck {
-		u.updateCheckWorkerMgr.
-			Repeat(updateTaskRepeatDuration).
-			Delay(15 * time.Second)
-	}
-	return nil
-}
-
-func (u *Updater) GetMainDir() string {
-	return u.cfg.Directory
-}
-
-// GetFile returns the path of a file given the name. Returns ErrNotFound if file is not found.
-func (u *Updater) GetFile(name string) (*Artifact, error) {
-	u.indexLock.Lock()
-	defer u.indexLock.Unlock()
-
-	// Check if any index is active.
-	if u.index == nil {
-		return nil, ErrNotFound
-	}
-
-	for _, artifact := range u.index.Artifacts {
-		switch {
-		case artifact.Filename != name:
-			// Name does not match.
-		case artifact.Platform != "" && artifact.Platform != currentPlatform:
-			// Platform is defined and does not match.
-			// Platforms are usually pre-filtered, but just to be sure.
-		default:
-			// Artifact matches!
-			return artifact.export(u.cfg.Directory, u.index.versionNum), nil
-		}
-	}
-
-	return nil, ErrNotFound
+func (u *Updates) Start() error {
+	return start()
 }
 
 // Stop stops the module.
-func (u *Updater) Stop() error {
-	return nil
+func (u *Updates) Stop() error {
+	return stop()
 }
 
 type instance interface {
+	API() *api.API
+	Config() *config.Config
 	Restart()
 	Shutdown()
 	Notifications() *notifications.Notifications
diff --git a/service/updates/notify.go b/service/updates/notify.go
new file mode 100644
index 00000000..076eea34
--- /dev/null
+++ b/service/updates/notify.go
@@ -0,0 +1,180 @@
+package updates
+
+import (
+	"fmt"
+	"strings"
+	"sync/atomic"
+	"time"
+
+	"github.com/safing/portmaster/base/notifications"
+)
+
+const (
+	updateFailed            = "updates:failed"
+	updateSuccess           = "updates:success"
+	updateSuccessPending    = "updates:success-pending"
+	updateSuccessDownloaded = "updates:success-downloaded"
+
+	failedUpdateNotifyDurationThreshold = 24 * time.Hour
+	failedUpdateNotifyCountThreshold    = 3
+)
+
+var updateFailedCnt = new(atomic.Int32)
+
+func (u *Updates) notificationsEnabled() bool {
+	return u.instance.Notifications() != nil
+}
+
+func notifyUpdateSuccess(force bool) {
+	if !module.notificationsEnabled() {
+		return
+	}
+
+	updateFailedCnt.Store(0)
+	module.states.Clear()
+	updateState := registry.GetState().Updates
+
+	flavor := updateSuccess
+	switch {
+	case len(updateState.PendingDownload) > 0:
+		// Show notification if there are pending downloads.
+		flavor = updateSuccessPending
+	case updateState.LastDownloadAt != nil &&
+		time.Since(*updateState.LastDownloadAt) < 5*time.Second:
+		// Show notification if we downloaded something within the last minute.
+		flavor = updateSuccessDownloaded
+	case force:
+		// Always show notification if update was manually triggered.
+	default:
+		// Otherwise, the update was uneventful. Do not show notification.
+		return
+	}
+
+	switch flavor {
+	case updateSuccess:
+		notifications.Notify(&notifications.Notification{
+			EventID: updateSuccess,
+			Type:    notifications.Info,
+			Title:   "Portmaster Is Up-To-Date",
+			Message: "Portmaster successfully checked for updates. Everything is up to date.\n\n" + getUpdatingInfoMsg(),
+			Expires: time.Now().Add(1 * time.Minute).Unix(),
+			AvailableActions: []*notifications.Action{
+				{
+					ID:   "ack",
+					Text: "OK",
+				},
+			},
+		})
+
+	case updateSuccessPending:
+		msg := fmt.Sprintf(
+			`%d updates are available for download:
+
+- %s
+
+Press "Download Now" to download and automatically apply all pending updates. You will be notified of important updates that need restarting.`,
+			len(updateState.PendingDownload),
+			strings.Join(updateState.PendingDownload, "\n- "),
+		)
+
+		notifications.Notify(&notifications.Notification{
+			EventID: updateSuccess,
+			Type:    notifications.Info,
+			Title:   fmt.Sprintf("%d Updates Available", len(updateState.PendingDownload)),
+			Message: msg,
+			AvailableActions: []*notifications.Action{
+				{
+					ID:   "ack",
+					Text: "OK",
+				},
+				{
+					ID:   "download",
+					Text: "Download Now",
+					Type: notifications.ActionTypeWebhook,
+					Payload: &notifications.ActionTypeWebhookPayload{
+						URL:          apiPathCheckForUpdates + "?download",
+						ResultAction: "display",
+					},
+				},
+			},
+		})
+
+	case updateSuccessDownloaded:
+		msg := fmt.Sprintf(
+			`%d updates were downloaded and applied:
+
+- %s
+
+%s
+`,
+			len(updateState.LastDownload),
+			strings.Join(updateState.LastDownload, "\n- "),
+			getUpdatingInfoMsg(),
+		)
+
+		notifications.Notify(&notifications.Notification{
+			EventID: updateSuccess,
+			Type:    notifications.Info,
+			Title:   fmt.Sprintf("%d Updates Applied", len(updateState.LastDownload)),
+			Message: msg,
+			Expires: time.Now().Add(1 * time.Minute).Unix(),
+			AvailableActions: []*notifications.Action{
+				{
+					ID:   "ack",
+					Text: "OK",
+				},
+			},
+		})
+
+	}
+}
+
+func getUpdatingInfoMsg() string {
+	switch {
+	case enableSoftwareUpdates() && enableIntelUpdates():
+		return "You will be notified of important updates that need restarting."
+	case enableIntelUpdates():
+		return "Automatic software updates are disabled, but you will be notified when a new software update is ready to be downloaded and applied."
+	default:
+		return "Automatic software updates are disabled. Please check for updates regularly yourself."
+	}
+}
+
+func notifyUpdateCheckFailed(force bool, err error) {
+	if !module.notificationsEnabled() {
+		return
+	}
+
+	failedCnt := updateFailedCnt.Add(1)
+	lastSuccess := registry.GetState().Updates.LastSuccessAt
+
+	switch {
+	case force:
+		// Always show notification if update was manually triggered.
+	case failedCnt < failedUpdateNotifyCountThreshold:
+		// Not failed often enough for notification.
+		return
+	case lastSuccess == nil:
+		// No recorded successful update.
+	case time.Now().Add(-failedUpdateNotifyDurationThreshold).Before(*lastSuccess):
+		// Failed too recently for notification.
+		return
+	}
+
+	notifications.NotifyWarn(
+		updateFailed,
+		"Update Check Failed",
+		fmt.Sprintf(
+			"Portmaster failed to check for updates. This might be a temporary issue of your device, your network or the update servers. The Portmaster will automatically try again later. The error was: %s",
+			err,
+		),
+		notifications.Action{
+			Text: "Try Again Now",
+			Type: notifications.ActionTypeWebhook,
+			Payload: &notifications.ActionTypeWebhookPayload{
+				URL:          apiPathCheckForUpdates,
+				ResultAction: "display",
+			},
+		},
+	).SyncWithState(module.states)
+}
diff --git a/service/updates/os_integration_default.go b/service/updates/os_integration_default.go
new file mode 100644
index 00000000..b817c351
--- /dev/null
+++ b/service/updates/os_integration_default.go
@@ -0,0 +1,8 @@
+//go:build !linux
+// +build !linux
+
+package updates
+
+func upgradeSystemIntegration() error {
+	return nil
+}
diff --git a/service/updates/os_integration_linux.go b/service/updates/os_integration_linux.go
new file mode 100644
index 00000000..cef0b9ef
--- /dev/null
+++ b/service/updates/os_integration_linux.go
@@ -0,0 +1,204 @@
+package updates
+
+import (
+	"bytes"
+	"crypto/sha256"
+	_ "embed"
+	"encoding/hex"
+	"errors"
+	"fmt"
+	"io"
+	"io/fs"
+	"os"
+	"path/filepath"
+
+	"github.com/tevino/abool"
+	"golang.org/x/exp/slices"
+
+	"github.com/safing/portmaster/base/dataroot"
+	"github.com/safing/portmaster/base/log"
+	"github.com/safing/portmaster/base/utils/renameio"
+)
+
+var (
+	portmasterCoreServiceFilePath     = "portmaster.service"
+	portmasterNotifierServiceFilePath = "portmaster_notifier.desktop"
+	backupExtension                   = ".backup"
+
+	//go:embed assets/portmaster.service
+	currentPortmasterCoreServiceFile []byte
+
+	checkedSystemIntegration = abool.New()
+
+	// ErrRequiresManualUpgrade is returned when a system integration file requires a manual upgrade.
+	ErrRequiresManualUpgrade = errors.New("requires a manual upgrade")
+)
+
+func upgradeSystemIntegration() {
+	// Check if we already checked the system integration.
+	if !checkedSystemIntegration.SetToIf(false, true) {
+		return
+	}
+
+	// Upgrade portmaster core systemd service.
+	err := upgradeSystemIntegrationFile(
+		"portmaster core systemd service",
+		filepath.Join(dataroot.Root().Path, portmasterCoreServiceFilePath),
+		0o0600,
+		currentPortmasterCoreServiceFile,
+		[]string{
+			"bc26dd37e6953af018ad3676ee77570070e075f2b9f5df6fa59d65651a481468", // Commit 19c76c7 on 2022-01-25
+			"cc0cb49324dfe11577e8c066dd95cc03d745b50b2153f32f74ca35234c3e8cb5", // Commit ef479e5 on 2022-01-24
+			"d08a3b5f3aee351f8e120e6e2e0a089964b94c9e9d0a9e5fa822e60880e315fd", // Commit b64735e on 2021-12-07
+		},
+	)
+	if err != nil {
+		log.Warningf("updates: %s", err)
+		return
+	}
+
+	// Upgrade portmaster notifier systemd user service.
+	// Permissions only!
+	err = upgradeSystemIntegrationFile(
+		"portmaster notifier systemd user service",
+		filepath.Join(dataroot.Root().Path, portmasterNotifierServiceFilePath),
+		0o0644,
+		nil, // Do not update contents.
+		nil, // Do not update contents.
+	)
+	if err != nil {
+		log.Warningf("updates: %s", err)
+		return
+	}
+}
+
+// upgradeSystemIntegrationFile upgrades the file contents and permissions.
+// System integration files are not necessarily present and may also be
+// edited by third parties, such as the OS itself or other installers.
+// The supplied hashes must be sha256 hex-encoded.
+func upgradeSystemIntegrationFile(
+	name string,
+	filePath string,
+	fileMode fs.FileMode,
+	fileData []byte,
+	permittedUpgradeHashes []string,
+) error {
+	// Upgrade file contents.
+	if len(fileData) > 0 {
+		if err := upgradeSystemIntegrationFileContents(name, filePath, fileData, permittedUpgradeHashes); err != nil {
+			return err
+		}
+	}
+
+	// Upgrade file permissions.
+	if fileMode != 0 {
+		if err := upgradeSystemIntegrationFilePermissions(name, filePath, fileMode); err != nil {
+			return err
+		}
+	}
+
+	return nil
+}
+
+// upgradeSystemIntegrationFileContents upgrades the file contents.
+// System integration files are not necessarily present and may also be
+// edited by third parties, such as the OS itself or other installers.
+// The supplied hashes must be sha256 hex-encoded.
+func upgradeSystemIntegrationFileContents(
+	name string,
+	filePath string,
+	fileData []byte,
+	permittedUpgradeHashes []string,
+) error {
+	// Read existing file.
+	existingFileData, err := os.ReadFile(filePath)
+	if err != nil {
+		if errors.Is(err, os.ErrNotExist) {
+			return nil
+		}
+		return fmt.Errorf("failed to read %s at %s: %w", name, filePath, err)
+	}
+
+	// Check if file is already the current version.
+	existingSum := sha256.Sum256(existingFileData)
+	existingHexSum := hex.EncodeToString(existingSum[:])
+	currentSum := sha256.Sum256(fileData)
+	currentHexSum := hex.EncodeToString(currentSum[:])
+	if existingHexSum == currentHexSum {
+		log.Debugf("updates: %s at %s is up to date", name, filePath)
+		return nil
+	}
+
+	// Check if we are allowed to upgrade from the existing file.
+	if !slices.Contains[[]string, string](permittedUpgradeHashes, existingHexSum) {
+		return fmt.Errorf("%s at %s (sha256:%s) %w, as it is not a previously published version and cannot be automatically upgraded - try installing again", name, filePath, existingHexSum, ErrRequiresManualUpgrade)
+	}
+
+	// Start with upgrade!
+
+	// Make backup of existing file.
+	err = CopyFile(filePath, filePath+backupExtension)
+	if err != nil {
+		return fmt.Errorf(
+			"failed to create backup of %s from %s to %s: %w",
+			name,
+			filePath,
+			filePath+backupExtension,
+			err,
+		)
+	}
+
+	// Open destination file for writing.
+	atomicDstFile, err := renameio.TempFile(registry.TmpDir().Path, filePath)
+	if err != nil {
+		return fmt.Errorf("failed to create tmp file to update %s at %s: %w", name, filePath, err)
+	}
+	defer atomicDstFile.Cleanup() //nolint:errcheck // ignore error for now, tmp dir will be cleaned later again anyway
+
+	// Write file.
+	_, err = io.Copy(atomicDstFile, bytes.NewReader(fileData))
+	if err != nil {
+		return err
+	}
+
+	// Finalize file.
+	err = atomicDstFile.CloseAtomicallyReplace()
+	if err != nil {
+		return fmt.Errorf("failed to finalize update of %s at %s: %w", name, filePath, err)
+	}
+
+	log.Warningf("updates: %s at %s was upgraded to %s - a reboot may be required", name, filePath, currentHexSum)
+	return nil
+}
+
+// upgradeSystemIntegrationFilePermissions upgrades the file permissions.
+// System integration files are not necessarily present and may also be
+// edited by third parties, such as the OS itself or other installers.
+func upgradeSystemIntegrationFilePermissions(
+	name string,
+	filePath string,
+	fileMode fs.FileMode,
+) error {
+	// Get current file permissions.
+	stat, err := os.Stat(filePath)
+	if err != nil {
+		if errors.Is(err, os.ErrNotExist) {
+			return nil
+		}
+		return fmt.Errorf("failed to read %s file metadata at %s: %w", name, filePath, err)
+	}
+
+	// If permissions are as expected, do nothing.
+	if stat.Mode().Perm() == fileMode {
+		return nil
+	}
+
+	// Otherwise, set correct permissions.
+	err = os.Chmod(filePath, fileMode)
+	if err != nil {
+		return fmt.Errorf("failed to update %s file permissions at %s: %w", name, filePath, err)
+	}
+
+	log.Warningf("updates: %s file permissions at %s updated to %v", name, filePath, fileMode)
+	return nil
+}
diff --git a/service/updates/restart.go b/service/updates/restart.go
new file mode 100644
index 00000000..729853ff
--- /dev/null
+++ b/service/updates/restart.go
@@ -0,0 +1,135 @@
+package updates
+
+import (
+	"os/exec"
+	"runtime"
+	"sync"
+	"time"
+
+	"github.com/tevino/abool"
+
+	"github.com/safing/portmaster/base/log"
+	"github.com/safing/portmaster/service/mgr"
+)
+
+var (
+	// RebootOnRestart defines whether the whole system, not just the service,
+	// should be restarted automatically when triggering a restart internally.
+	RebootOnRestart bool
+
+	restartPending   = abool.New()
+	restartTriggered = abool.New()
+
+	restartTime     time.Time
+	restartTimeLock sync.Mutex
+)
+
+// IsRestarting returns whether a restart has been triggered.
+func IsRestarting() bool {
+	return restartTriggered.IsSet()
+}
+
+// RestartIsPending returns whether a restart is pending.
+func RestartIsPending() (pending bool, restartAt time.Time) {
+	if restartPending.IsNotSet() {
+		return false, time.Time{}
+	}
+
+	restartTimeLock.Lock()
+	defer restartTimeLock.Unlock()
+
+	return true, restartTime
+}
+
+// DelayedRestart triggers a restart of the application by shutting down the
+// module system gracefully and returning with RestartExitCode. The restart
+// may be further delayed by up to 10 minutes by the internal task scheduling
+// system. This only works if the process is managed by portmaster-start.
+func DelayedRestart(delay time.Duration) {
+	// Check if restart is already pending.
+	if !restartPending.SetToIf(false, true) {
+		return
+	}
+
+	// Schedule the restart task.
+	log.Warningf("updates: restart triggered, will execute in %s", delay)
+	restartAt := time.Now().Add(delay)
+	module.restartWorkerMgr.Delay(delay)
+
+	// Set restartTime.
+	restartTimeLock.Lock()
+	defer restartTimeLock.Unlock()
+	restartTime = restartAt
+}
+
+// AbortRestart aborts a (delayed) restart.
+func AbortRestart() {
+	if restartPending.SetToIf(true, false) {
+		log.Warningf("updates: restart aborted")
+
+		// Cancel schedule.
+		module.restartWorkerMgr.Delay(0)
+	}
+}
+
+// TriggerRestartIfPending triggers an automatic restart, if one is pending.
+// This can be used to prepone a scheduled restart if the conditions are preferable.
+func TriggerRestartIfPending() {
+	if restartPending.IsSet() {
+		module.restartWorkerMgr.Go()
+	}
+}
+
+// RestartNow immediately executes a restart.
+// This only works if the process is managed by portmaster-start.
+func RestartNow() {
+	restartPending.Set()
+	module.restartWorkerMgr.Go()
+}
+
+func automaticRestart(w *mgr.WorkerCtx) error {
+	// Check if the restart is still scheduled.
+	if restartPending.IsNotSet() {
+		return nil
+	}
+
+	// Trigger restart.
+	if restartTriggered.SetToIf(false, true) {
+		log.Warning("updates: initiating (automatic) restart")
+
+		// Check if we should reboot instead.
+		var rebooting bool
+		if RebootOnRestart {
+			// Trigger system reboot and record success.
+			rebooting = triggerSystemReboot()
+			if !rebooting {
+				log.Warningf("updates: rebooting failed, only restarting service instead")
+			}
+		}
+
+		// Set restart exit code.
+		if !rebooting {
+			module.instance.Restart()
+		} else {
+			module.instance.Shutdown()
+		}
+	}
+
+	return nil
+}
+
+func triggerSystemReboot() (success bool) {
+	switch runtime.GOOS {
+	case "linux":
+		err := exec.Command("systemctl", "reboot").Run()
+		if err != nil {
+			log.Errorf("updates: triggering reboot with systemctl failed: %s", err)
+			return false
+		}
+	default:
+		log.Warningf("updates: rebooting is not support on %s", runtime.GOOS)
+		return false
+	}
+
+	return true
+}
diff --git a/service/updates/state.go b/service/updates/state.go
new file mode 100644
index 00000000..3a1144b1
--- /dev/null
+++ b/service/updates/state.go
@@ -0,0 +1,49 @@
+package updates
+
+import (
+	"github.com/safing/portmaster/base/database/record"
+	"github.com/safing/portmaster/base/runtime"
+	"github.com/safing/portmaster/base/updater"
+)
+
+var pushRegistryStatusUpdate runtime.PushFunc
+
+// RegistryStateExport is a wrapper to export the registry state.
+type RegistryStateExport struct {
+	record.Base
+	*updater.RegistryState
+}
+
+func exportRegistryState(s *updater.RegistryState) *RegistryStateExport {
+	if s == nil {
+		state := registry.GetState()
+		s = &state
+	}
+
+	export := &RegistryStateExport{
+		RegistryState: s,
+	}
+
+	export.CreateMeta()
+	export.SetKey("runtime:core/updates/state")
+
+	return export
+}
+
+func pushRegistryState(s *updater.RegistryState) {
+	export := exportRegistryState(s)
+	pushRegistryStatusUpdate(export)
+}
+
+func registerRegistryStateProvider() (err error) {
+	registryStateProvider := runtime.SimpleValueGetterFunc(func(_ string) ([]record.Record, error) {
+		return []record.Record{exportRegistryState(nil)}, nil
+	})
+
+	pushRegistryStatusUpdate, err = runtime.Register("core/updates/state", registryStateProvider)
+	if err != nil {
+		return err
+	}
+
+	return nil
+}
diff --git a/service/updates/updates_test.go b/service/updates/updates_test.go
deleted file mode 100644
index d79706c7..00000000
--- a/service/updates/updates_test.go
+++ /dev/null
@@ -1,155 +0,0 @@
-package updates
-
-import (
-	"encoding/json"
-	"fmt"
-	"os"
-	"path/filepath"
-	"testing"
-	"time"
-
-	"github.com/safing/portmaster/base/notifications"
-	"github.com/safing/portmaster/service/mgr"
-)
-
-type testInstance struct{}
-
-func (i *testInstance) Restart()  {}
-func (i *testInstance) Shutdown() {}
-
-func (i *testInstance) Notifications() *notifications.Notifications {
-	return nil
-}
-
-func (i *testInstance) Ready() bool {
-	return true
-}
-
-func (i *testInstance) SetCmdLineOperation(f func() error) {}
-
-func TestPerformUpdate(t *testing.T) {
-	t.Parallel()
-
-	// Initialize mock instance
-	stub := &testInstance{}
-
-	// Make tmp dirs
-	installedDir, err := os.MkdirTemp("", "updates_current_")
-	if err != nil {
-		t.Fatal(err)
-	}
-	defer func() { _ = os.RemoveAll(installedDir) }()
-	updateDir, err := os.MkdirTemp("", "updates_new_")
-	if err != nil {
-		t.Fatal(err)
-	}
-	defer func() { _ = os.RemoveAll(updateDir) }()
-	purgeDir, err := os.MkdirTemp("", "updates_purge_")
-	if err != nil {
-		t.Fatal(err)
-	}
-	defer func() { _ = os.RemoveAll(purgeDir) }()
-
-	// Generate mock files
-	now := time.Now()
-	if err := GenerateMockFolder(installedDir, "Test", "1.0.0", now); err != nil {
-		t.Fatal(err)
-	}
-	if err := GenerateMockFolder(updateDir, "Test", "1.0.1", now.Add(1*time.Minute)); err != nil {
-		t.Fatal(err)
-	}
-
-	// Create updater (loads index).
-	updater, err := New(stub, "Test", Config{
-		Name:              "Test",
-		Directory:         installedDir,
-		DownloadDirectory: updateDir,
-		PurgeDirectory:    purgeDir,
-		IndexFile:         "index.json",
-		AutoDownload:      true,
-		AutoApply:         true,
-	})
-	if err != nil {
-		t.Fatal(err)
-	}
-
-	// Try to apply the updates
-	m := mgr.New("updates test")
-	_ = m.Do("test update and upgrade", func(w *mgr.WorkerCtx) error {
-		if err := updater.updateAndUpgrade(w, nil, false, false); err != nil {
-			if data, err := os.ReadFile(filepath.Join(installedDir, "index.json")); err == nil {
-				fmt.Println(string(data))
-				fmt.Println(updater.index.Version)
-				fmt.Println(updater.index.versionNum)
-			}
-			if data, err := os.ReadFile(filepath.Join(updateDir, "index.json")); err == nil {
-				fmt.Println(string(data))
-				idx, err := ParseIndex(data, nil)
-				if err == nil {
-					fmt.Println(idx.Version)
-					fmt.Println(idx.versionNum)
-				}
-			}
-
-			t.Fatal(err)
-		}
-		return nil
-	})
-
-	// Check if the current version is now the new.
-	newIndex, err := LoadIndex(filepath.Join(installedDir, "index.json"), nil)
-	if err != nil {
-		t.Fatal(err)
-	}
-	if newIndex.Version != "1.0.1" {
-		t.Fatalf("expected version 1.0.1 found %s", newIndex.Version)
-	}
-}
-
-// GenerateMockFolder generates mock index folder for testing.
-func GenerateMockFolder(dir, name, version string, published time.Time) error {
-	// Make sure dir exists
-	_ = os.MkdirAll(dir, defaultDirMode)
-
-	// Create empty files
-	file, err := os.Create(filepath.Join(dir, "portmaster"))
-	if err != nil {
-		return err
-	}
-	_ = file.Close()
-	file, err = os.Create(filepath.Join(dir, "portmaster-core"))
-	if err != nil {
-		return err
-	}
-	_ = file.Close()
-	file, err = os.Create(filepath.Join(dir, "portmaster.zip"))
-	if err != nil {
-		return err
-	}
-	_ = file.Close()
-	file, err = os.Create(filepath.Join(dir, "assets.zip"))
-	if err != nil {
-		return err
-	}
-	_ = file.Close()
-
-	index, err := GenerateIndexFromDir(dir, IndexScanConfig{
-		Name:    name,
-		Version: version,
-	})
-	if err != nil {
-		return err
-	}
-	index.Published = published
-
-	indexJSON, err := json.MarshalIndent(index, "", "  ")
-	if err != nil {
-		fmt.Fprintf(os.Stderr, "failed to marshal index: %s\n", err)
-	}
-
-	err = os.WriteFile(filepath.Join(dir, "index.json"), indexJSON, defaultFileMode)
-	if err != nil {
-		return err
-	}
-	return nil
-}
diff --git a/service/updates/upgrade.go b/service/updates/upgrade.go
deleted file mode 100644
index 86077ee1..00000000
--- a/service/updates/upgrade.go
+++ /dev/null
@@ -1,209 +0,0 @@
-package updates
-
-import (
-	"errors"
-	"fmt"
-	"io/fs"
-	"os"
-	"path/filepath"
-	"slices"
-	"strings"
-
-	"github.com/safing/portmaster/base/log"
-)
-
-// FIXME: previous update system did in-place service file upgrades. Check if this is still necessary and if changes are in current installers.
-
-const (
-	defaultFileMode      = os.FileMode(0o0644)
-	executableFileMode   = os.FileMode(0o0744)
-	executableUIFileMode = os.FileMode(0o0755)
-	defaultDirMode       = os.FileMode(0o0755)
-)
-
-func (u *Updater) upgrade(downloader *Downloader, ignoreVersion bool) error {
-	// Lock index for the upgrade.
-	u.indexLock.Lock()
-	defer u.indexLock.Unlock()
-
-	// Check if we should upgrade at all.
-	if !ignoreVersion && u.index != nil {
-		if err := u.index.ShouldUpgradeTo(downloader.index); err != nil {
-			return fmt.Errorf("cannot upgrade: %w", ErrNoUpdateAvailable)
-		}
-	}
-
-	// Execute the upgrade.
-	upgradeError := u.upgradeMoveFiles(downloader)
-	if upgradeError == nil {
-		return nil
-	}
-
-	// Attempt to recover from failed upgrade.
-	recoveryErr := u.recoverFromFailedUpgrade()
-	if recoveryErr == nil {
-		return fmt.Errorf("upgrade failed, but recovery was successful: %w", upgradeError)
-	}
-
-	// Recovery failed too.
-	return fmt.Errorf("upgrade (including recovery) failed: %w", upgradeError)
-}
-
-func (u *Updater) upgradeMoveFiles(downloader *Downloader) error {
-	// Important:
-	// We assume that the downloader has done its job and all artifacts are verified.
-	// Files will just be moved here.
-	// In case the files are copied, they are verified in the process.
-
-	// Reset purge directory, so that we can do a clean rollback later.
-	_ = os.RemoveAll(u.cfg.PurgeDirectory)
-	err := os.MkdirAll(u.cfg.PurgeDirectory, defaultDirMode)
-	if err != nil {
-		return fmt.Errorf("failed to create purge directory: %w", err)
-	}
-
-	// Move current version files into purge folder.
-	if u.index != nil {
-		log.Debugf("updates/%s: removing the old version (v%s from %s)", u.cfg.Name, u.index.Version, u.index.Published)
-	}
-	files, err := os.ReadDir(u.cfg.Directory)
-	if err != nil {
-		if !errors.Is(err, os.ErrNotExist) {
-			return fmt.Errorf("read current directory: %w", err)
-		}
-		err = os.MkdirAll(u.cfg.Directory, defaultDirMode)
-		if err != nil {
-			return fmt.Errorf("create current directory: %w", err)
-		}
-	} else {
-		// Move files.
-		for _, file := range files {
-			// Check if file is ignored.
-			if slices.Contains(u.cfg.Ignore, file.Name()) {
-				continue
-			}
-
-			// Otherwise, move file to purge dir.
-			src := filepath.Join(u.cfg.Directory, file.Name())
-			dst := filepath.Join(u.cfg.PurgeDirectory, file.Name())
-			err := u.moveFile(src, dst, "", file.Type().Perm())
-			if err != nil {
-				return fmt.Errorf("failed to move current file %s to purge dir: %w", file.Name(), err)
-			}
-		}
-	}
-
-	// Move the new index file into main directory.
-	log.Debugf("updates/%s: installing the new version (v%s from %s)", u.cfg.Name, downloader.index.Version, downloader.index.Published)
-	src := filepath.Join(u.cfg.DownloadDirectory, u.cfg.IndexFile)
-	dst := filepath.Join(u.cfg.Directory, u.cfg.IndexFile)
-	err = u.moveFile(src, dst, "", defaultFileMode)
-	if err != nil {
-		return fmt.Errorf("failed to move index file to %s: %w", dst, err)
-	}
-
-	// Move downloaded files to the current version folder.
-	for _, artifact := range downloader.index.Artifacts {
-		src = filepath.Join(u.cfg.DownloadDirectory, artifact.Filename)
-		dst = filepath.Join(u.cfg.Directory, artifact.Filename)
-		err = u.moveFile(src, dst, artifact.SHA256, artifact.GetFileMode())
-		if err != nil {
-			return fmt.Errorf("failed to move file %s: %w", artifact.Filename, err)
-		} else {
-			log.Debugf("updates/%s: %s moved", u.cfg.Name, artifact.Filename)
-		}
-	}
-
-	// Set new index on module.
-	u.index = downloader.index
-	log.Infof("updates/%s: update complete (v%s from %s)", u.cfg.Name, u.index.Version, u.index.Published)
-
-	return nil
-}
-
-// moveFile moves a file and falls back to copying if it fails.
-func (u *Updater) moveFile(currentPath, newPath string, sha256sum string, fileMode fs.FileMode) error {
-	// Try to simply move file.
-	err := os.Rename(currentPath, newPath)
-	if err == nil {
-		// Moving was successful, return.
-		return nil
-	}
-	log.Tracef("updates/%s: failed to move to %q, falling back to copy+delete: %s", u.cfg.Name, newPath, err)
-
-	// Copy and check the checksum while we are at it.
-	err = copyAndCheckSHA256Sum(currentPath, newPath, sha256sum, fileMode)
-	if err != nil {
-		return fmt.Errorf("move failed, copy+delete fallback failed: %w", err)
-	}
-
-	return nil
-}
-
-// recoverFromFailedUpgrade attempts to roll back any moved files by the upgrade process.
-func (u *Updater) recoverFromFailedUpgrade() error {
-	// Get list of files from purge dir.
-	files, err := os.ReadDir(u.cfg.PurgeDirectory)
-	if err != nil {
-		return err
-	}
-
-	// Move all files back to main dir.
-	for _, file := range files {
-		purgedFile := filepath.Join(u.cfg.PurgeDirectory, file.Name())
-		activeFile := filepath.Join(u.cfg.Directory, file.Name())
-		err := u.moveFile(purgedFile, activeFile, "", file.Type().Perm())
-		if err != nil {
-			// Only warn and continue to recover as many files as possible.
-			log.Warningf("updates/%s: failed to roll back file %s: %s", u.cfg.Name, file.Name(), err)
-		}
-	}
-
-	return nil
-}
-
-func (u *Updater) cleanupAfterUpgrade() error {
-	err := os.RemoveAll(u.cfg.PurgeDirectory)
-	if err != nil {
-		return fmt.Errorf("delete purge dir: %w", err)
-	}
-
-	err = os.RemoveAll(u.cfg.DownloadDirectory)
-	if err != nil {
-		return fmt.Errorf("delete download dir: %w", err)
-	}
-
-	return nil
-}
-
-func (u *Updater) deleteUnfinishedFiles(dir string) error {
-	entries, err := os.ReadDir(dir)
-	if err != nil {
-		return err
-	}
-
-	for _, e := range entries {
-		switch {
-		case e.IsDir():
-			// Continue.
-
-		case strings.HasSuffix(e.Name(), ".download"):
-			path := filepath.Join(dir, e.Name())
-			log.Warningf("updates/%s: deleting unfinished download file: %s", u.cfg.Name, path)
-			err := os.Remove(path)
-			if err != nil {
-				log.Errorf("updates/%s: failed to delete unfinished download file %s: %s", u.cfg.Name, path, err)
-			}
-
-		case strings.HasSuffix(e.Name(), ".copy"):
-			path := filepath.Join(dir, e.Name())
-			log.Warningf("updates/%s: deleting unfinished copied file: %s", u.cfg.Name, path)
-			err := os.Remove(path)
-			if err != nil {
-				log.Errorf("updates/%s: failed to delete unfinished copied file %s: %s", u.cfg.Name, path, err)
-			}
-		}
-	}
-
-	return nil
-}
diff --git a/service/updates/upgrader.go b/service/updates/upgrader.go
new file mode 100644
index 00000000..622b3909
--- /dev/null
+++ b/service/updates/upgrader.go
@@ -0,0 +1,406 @@
+package updates
+
+import (
+	"context"
+	"fmt"
+	"io"
+	"os"
+	"os/exec"
+	"path/filepath"
+	"regexp"
+	"strings"
+	"time"
+
+	processInfo "github.com/shirou/gopsutil/process"
+	"github.com/tevino/abool"
+
+	"github.com/safing/portmaster/base/dataroot"
+	"github.com/safing/portmaster/base/info"
+	"github.com/safing/portmaster/base/log"
+	"github.com/safing/portmaster/base/notifications"
+	"github.com/safing/portmaster/base/rng"
+	"github.com/safing/portmaster/base/updater"
+	"github.com/safing/portmaster/base/utils/renameio"
+	"github.com/safing/portmaster/service/mgr"
+	"github.com/safing/portmaster/service/updates/helper"
+)
+
+const (
+	upgradedSuffix = "-upgraded"
+	exeExt         = ".exe"
+)
+
+var (
+	upgraderActive = abool.NewBool(false)
+
+	pmCtrlUpdate *updater.File
+	pmCoreUpdate *updater.File
+
+	spnHubUpdate *updater.File
+
+	rawVersionRegex = regexp.MustCompile(`^[0-9]+\.[0-9]+\.[0-9]+b?\*?$`)
+)
+
+func initUpgrader() error {
+	module.EventResourcesUpdated.AddCallback("run upgrades", upgrader)
+	return nil
+}
+
+func upgrader(m *mgr.WorkerCtx, _ struct{}) (cancel bool, err error) {
+	// Lock runs, but discard additional runs.
+	if !upgraderActive.SetToIf(false, true) {
+		return false, nil
+	}
+	defer upgraderActive.SetTo(false)
+
+	// Upgrade portmaster-start.
+	err = upgradePortmasterStart()
+	if err != nil {
+		log.Warningf("updates: failed to upgrade portmaster-start: %s", err)
+	}
+
+	// Upgrade based on binary.
+	binBaseName := strings.Split(filepath.Base(os.Args[0]), "_")[0]
+	switch binBaseName {
+	case "portmaster-core":
+		// Notify about upgrade.
+		if err := upgradeCoreNotify(); err != nil {
+			log.Warningf("updates: failed to notify about core upgrade: %s", err)
+		}
+
+		// Fix chrome sandbox permissions.
+		if err := helper.EnsureChromeSandboxPermissions(registry); err != nil {
+			log.Warningf("updates: failed to handle electron upgrade: %s", err)
+		}
+
+		// Upgrade system integration.
+		upgradeSystemIntegration()
+
+	case "spn-hub":
+		// Trigger upgrade procedure.
+		if err := upgradeHub(); err != nil {
+			log.Warningf("updates: failed to initiate hub upgrade: %s", err)
+		}
+	}
+
+	return false, nil
+}
+
+func upgradeCoreNotify() error {
+	if pmCoreUpdate != nil && !pmCoreUpdate.UpgradeAvailable() {
+		return nil
+	}
+
+	// make identifier
+	identifier := "core/portmaster-core" // identifier, use forward slash!
+	if onWindows {
+		identifier += exeExt
+	}
+
+	// get newest portmaster-core
+	newFile, err := GetPlatformFile(identifier)
+	if err != nil {
+		return err
+	}
+	pmCoreUpdate = newFile
+
+	// check for new version
+	if info.VersionNumber() != pmCoreUpdate.Version() {
+		n := notifications.Notify(&notifications.Notification{
+			EventID: "updates:core-update-available",
+			Type:    notifications.Info,
+			Title: fmt.Sprintf(
+				"Portmaster Update v%s Is Ready!",
+				pmCoreUpdate.Version(),
+			),
+			Category: "Core",
+			Message: fmt.Sprintf(
+				`A new Portmaster version is ready to go! Restart the Portmaster to upgrade to %s.`,
+				pmCoreUpdate.Version(),
+			),
+			ShowOnSystem: true,
+			AvailableActions: []*notifications.Action{
+				// TODO: Use special UI action in order to reload UI on restart.
+				{
+					ID:   "restart",
+					Text: "Restart",
+				},
+				{
+					ID:   "later",
+					Text: "Not now",
+				},
+			},
+		})
+		n.SetActionFunction(upgradeCoreNotifyActionHandler)
+
+		log.Debugf("updates: new portmaster version available, sending notification to user")
+	}
+
+	return nil
+}
+
+func upgradeCoreNotifyActionHandler(_ context.Context, n *notifications.Notification) error {
+	switch n.SelectedActionID {
+	case "restart":
+		log.Infof("updates: user triggered restart via core update notification")
+		RestartNow()
+	case "later":
+		n.Delete()
+	}
+
+	return nil
+}
+
+func upgradeHub() error {
+	if spnHubUpdate != nil && !spnHubUpdate.UpgradeAvailable() {
+		return nil
+	}
+
+	// Make identifier for getting file from updater.
+	identifier := "hub/spn-hub" // identifier, use forward slash!
+	if onWindows {
+		identifier += exeExt
+	}
+
+	// Get newest spn-hub file.
+	newFile, err := GetPlatformFile(identifier)
+	if err != nil {
+		return err
+	}
+	spnHubUpdate = newFile
+
+	// Check if the new version is different.
+	if info.GetInfo().Version != spnHubUpdate.Version() {
+		// Get random delay with up to three hours.
+		delayMinutes, err := rng.Number(3 * 60)
+		if err != nil {
+			return err
+		}
+
+		// Delay restart for at least one hour for preparations.
+		DelayedRestart(time.Duration(delayMinutes+60) * time.Minute)
+
+		// Increase update checks in order to detect aborts better.
+		if !disableTaskSchedule {
+			module.updateWorkerMgr.Repeat(10 * time.Minute)
+		}
+	} else {
+		AbortRestart()
+
+		// Set update task schedule back to normal.
+		if !disableTaskSchedule {
+			module.updateWorkerMgr.Repeat(updateTaskRepeatDuration)
+		}
+	}
+
+	return nil
+}
+
+func upgradePortmasterStart() error {
+	filename := "portmaster-start"
+	if onWindows {
+		filename += exeExt
+	}
+
+	// check if we can upgrade
+	if pmCtrlUpdate == nil || pmCtrlUpdate.UpgradeAvailable() {
+		// get newest portmaster-start
+		newFile, err := GetPlatformFile("start/" + filename) // identifier, use forward slash!
+		if err != nil {
+			return err
+		}
+		pmCtrlUpdate = newFile
+	} else {
+		return nil
+	}
+
+	// update portmaster-start in data root
+	rootPmStartPath := filepath.Join(dataroot.Root().Path, filename)
+	err := upgradeBinary(rootPmStartPath, pmCtrlUpdate)
+	if err != nil {
+		return err
+	}
+
+	return nil
+}
+
+func warnOnIncorrectParentPath() {
+	expectedFileName := "portmaster-start"
+	if onWindows {
+		expectedFileName += exeExt
+	}
+
+	// upgrade parent process, if it's portmaster-start
+	parent, err := processInfo.NewProcess(int32(os.Getppid()))
+	if err != nil {
+		log.Tracef("could not get parent process: %s", err)
+		return
+	}
+	parentName, err := parent.Name()
+	if err != nil {
+		log.Tracef("could not get parent process name: %s", err)
+		return
+	}
+	if parentName != expectedFileName {
+		// Only warn about this if not in dev mode.
+		if !devMode() {
+			log.Warningf("updates: parent process does not seem to be portmaster-start, name is %s", parentName)
+		}
+
+		// TODO(ppacher): once we released a new installer and folks had time
+		//                to update we should send a module warning/hint to the
+		//                UI notifying the user that he's still using portmaster-control.
+		return
+	}
+
+	parentPath, err := parent.Exe()
+	if err != nil {
+		log.Tracef("could not get parent process path: %s", err)
+		return
+	}
+
+	absPath, err := filepath.Abs(parentPath)
+	if err != nil {
+		log.Tracef("could not get absolut parent process path: %s", err)
+		return
+	}
+
+	root := filepath.Dir(registry.StorageDir().Path)
+	if !strings.HasPrefix(absPath, root) {
+		log.Warningf("detected unexpected path %s for portmaster-start", absPath)
+		notifications.NotifyWarn(
+			"updates:unsupported-parent",
+			"Unsupported Launcher",
+			fmt.Sprintf(
+				"The Portmaster has been launched by an unexpected %s binary at %s. Please configure your system to use the binary at %s as this version will be kept up to date automatically.",
+				expectedFileName,
+				absPath,
+				filepath.Join(root, expectedFileName),
+			),
+		)
+	}
+}
+
+func upgradeBinary(fileToUpgrade string, file *updater.File) error {
+	fileExists := false
+	_, err := os.Stat(fileToUpgrade)
+	if err == nil {
+		// file exists and is accessible
+		fileExists = true
+	}
+
+	if fileExists {
+		// get current version
+		var currentVersion string
+		cmd := exec.Command(fileToUpgrade, "version", "--short")
+		out, err := cmd.Output()
+		if err == nil {
+			// abort if version matches
+			currentVersion = strings.Trim(strings.TrimSpace(string(out)), "*")
+			if currentVersion == file.Version() {
+				log.Debugf("updates: %s is already v%s", fileToUpgrade, file.Version())
+				// already up to date!
+				return nil
+			}
+		} else {
+			log.Warningf("updates: failed to run %s to get version for upgrade check: %s", fileToUpgrade, err)
+			currentVersion = "0.0.0"
+		}
+
+		// test currentVersion for sanity
+		if !rawVersionRegex.MatchString(currentVersion) {
+			log.Debugf("updates: version string returned by %s is invalid: %s", fileToUpgrade, currentVersion)
+		}
+
+		// try removing old version
+		err = os.Remove(fileToUpgrade)
+		if err != nil {
+			// ensure tmp dir is here
+			err = registry.TmpDir().Ensure()
+			if err != nil {
+				return fmt.Errorf("could not prepare tmp directory for moving file that needs upgrade: %w", err)
+			}
+
+			// maybe we're on windows and it's in use, try moving
+			err = os.Rename(fileToUpgrade, filepath.Join(
+				registry.TmpDir().Path,
+				fmt.Sprintf(
+					"%s-%d%s",
+					filepath.Base(fileToUpgrade),
+					time.Now().UTC().Unix(),
+					upgradedSuffix,
+				),
+			))
+			if err != nil {
+				return fmt.Errorf("unable to move file that needs upgrade: %w", err)
+			}
+		}
+	}
+
+	// copy upgrade
+	err = CopyFile(file.Path(), fileToUpgrade)
+	if err != nil {
+		// try again
+		time.Sleep(1 * time.Second)
+		err = CopyFile(file.Path(), fileToUpgrade)
+		if err != nil {
+			return err
+		}
+	}
+
+	// check permissions
+	if !onWindows {
+		info, err := os.Stat(fileToUpgrade)
+		if err != nil {
+			return fmt.Errorf("failed to get file info on %s: %w", fileToUpgrade, err)
+		}
+		if info.Mode() != 0o0755 {
+			err := os.Chmod(fileToUpgrade, 0o0755) //nolint:gosec // Set execute permissions.
+			if err != nil {
+				return fmt.Errorf("failed to set permissions on %s: %w", fileToUpgrade, err)
+			}
+		}
+	}
+
+	log.Infof("updates: upgraded %s to v%s", fileToUpgrade, file.Version())
+	return nil
+}
+
+// CopyFile atomically copies a file using the update registry's tmp dir.
+func CopyFile(srcPath, dstPath string) error {
+	// check tmp dir
+	err := registry.TmpDir().Ensure()
+	if err != nil {
+		return fmt.Errorf("could not prepare tmp directory for copying file: %w", err)
+	}
+
+	// open file for writing
+	atomicDstFile, err := renameio.TempFile(registry.TmpDir().Path, dstPath)
+	if err != nil {
+		return fmt.Errorf("could not create temp file for atomic copy: %w", err)
+	}
+	defer atomicDstFile.Cleanup() //nolint:errcheck // ignore error for now, tmp dir will be cleaned later again anyway
+
+	// open source
+	srcFile, err := os.Open(srcPath)
+	if err != nil {
+		return err
+	}
+	defer func() {
+		_ = srcFile.Close()
+	}()
+
+	// copy data
+	_, err = io.Copy(atomicDstFile, srcFile)
+	if err != nil {
+		return err
+	}
+
+	// finalize file
+	err = atomicDstFile.CloseAtomicallyReplace()
+	if err != nil {
+		return fmt.Errorf("updates: failed to finalize copy to file %s: %w", dstPath, err)
+	}
+
+	return nil
+}
diff --git a/spn/access/module.go b/spn/access/module.go
index a805240a..d49524f9 100644
--- a/spn/access/module.go
+++ b/spn/access/module.go
@@ -80,7 +80,7 @@ func start() error {
 		// Add config listener to enable/disable SPN.
 		module.instance.Config().EventConfigChange.AddCallback("spn enable check", func(wc *mgr.WorkerCtx, s struct{}) (bool, error) {
 			// Do not do anything when we are shutting down.
-			if module.instance.IsShuttingDown() {
+			if module.instance.Stopping() {
 				return true, nil
 			}
 
@@ -255,5 +255,5 @@ func New(instance instance) (*Access, error) {
 type instance interface {
 	Config() *config.Config
 	SPNGroup() *mgr.ExtendedGroup
-	IsShuttingDown() bool
+	Stopping() bool
 }
diff --git a/spn/captain/hooks.go b/spn/captain/hooks.go
index 6e1b1010..d21bd67b 100644
--- a/spn/captain/hooks.go
+++ b/spn/captain/hooks.go
@@ -1,6 +1,7 @@
 package captain
 
 import (
+	"github.com/safing/portmaster/service/updates"
 	"github.com/safing/portmaster/spn/conf"
 	"github.com/safing/portmaster/spn/docks"
 )
@@ -40,7 +41,5 @@ func updateConnectionStatus() {
 			return
 		}
 	}
-
-	// TODO(vladimir): what was this needed for?
-	// updates.TriggerRestartIfPending()
+	updates.TriggerRestartIfPending()
 }
diff --git a/spn/captain/intel.go b/spn/captain/intel.go
index 085bf35c..c71ce663 100644
--- a/spn/captain/intel.go
+++ b/spn/captain/intel.go
@@ -6,6 +6,7 @@ import (
 	"os"
 	"sync"
 
+	"github.com/safing/portmaster/base/updater"
 	"github.com/safing/portmaster/service/mgr"
 	"github.com/safing/portmaster/service/updates"
 	"github.com/safing/portmaster/spn/conf"
@@ -15,14 +16,14 @@ import (
 )
 
 var (
-	intelResource           *updates.Artifact
-	intelResourceName       = "main-intel.yaml"
+	intelResource           *updater.File
+	intelResourcePath       = "intel/spn/main-intel.yaml"
 	intelResourceMapName    = "main"
 	intelResourceUpdateLock sync.Mutex
 )
 
 func registerIntelUpdateHook() error {
-	module.instance.IntelUpdates().EventResourcesUpdated.AddCallback("update SPN intel", func(wc *mgr.WorkerCtx, s struct{}) (cancel bool, err error) {
+	module.instance.Updates().EventResourcesUpdated.AddCallback("update SPN intel", func(wc *mgr.WorkerCtx, s struct{}) (cancel bool, err error) {
 		return false, updateSPNIntel(wc.Ctx(), nil)
 	})
 
@@ -42,21 +43,17 @@ func updateSPNIntel(_ context.Context, _ interface{}) (err error) {
 		return fmt.Errorf("intel resource not for map %q", conf.MainMapName)
 	}
 
-	// Get possibly updated file.
-	file, err := module.instance.IntelUpdates().GetFile(intelResourceName)
-	if err != nil {
-		return fmt.Errorf("failed to get SPN intel update: %w", err)
-	}
-
-	// Check if file is newer.
-	// Continue on check failure.
-	newer, ok := file.IsNewerThan(intelResource)
-	if ok && !newer {
+	// Check if there is something to do.
+	if intelResource != nil && !intelResource.UpgradeAvailable() {
 		return nil
 	}
 
-	// Load intel file from disk.
-	intelData, err := os.ReadFile(file.Path())
+	// Get intel file and load it from disk.
+	intelResource, err = updates.GetFile(intelResourcePath)
+	if err != nil {
+		return fmt.Errorf("failed to get SPN intel update: %w", err)
+	}
+	intelData, err := os.ReadFile(intelResource.Path())
 	if err != nil {
 		return fmt.Errorf("failed to load SPN intel update: %w", err)
 	}
@@ -67,15 +64,8 @@ func updateSPNIntel(_ context.Context, _ interface{}) (err error) {
 		return fmt.Errorf("failed to parse SPN intel update: %w", err)
 	}
 
-	// Apply new intel.
 	setVirtualNetworkConfig(intel.VirtualNetworks)
-	err = navigator.Main.UpdateIntel(intel, cfgOptionTrustNodeNodes())
-	if err != nil {
-		return fmt.Errorf("failed to update intel on map: %w", err)
-	}
-
-	intelResource = file
-	return nil
+	return navigator.Main.UpdateIntel(intel, cfgOptionTrustNodeNodes())
 }
 
 func resetSPNIntel() {
diff --git a/spn/captain/module.go b/spn/captain/module.go
index c5279c4d..1f82d763 100644
--- a/spn/captain/module.go
+++ b/spn/captain/module.go
@@ -249,6 +249,6 @@ type instance interface {
 	NetEnv() *netenv.NetEnv
 	Patrol() *patrol.Patrol
 	Config() *config.Config
-	IntelUpdates() *updates.Updater
+	Updates() *updates.Updates
 	SPNGroup() *mgr.ExtendedGroup
 }
diff --git a/spn/hub/hub_test.go b/spn/hub/hub_test.go
index 09425e72..391a61e7 100644
--- a/spn/hub/hub_test.go
+++ b/spn/hub/hub_test.go
@@ -20,11 +20,11 @@ type testInstance struct {
 	db      *dbmodule.DBModule
 	api     *api.API
 	config  *config.Config
-	updates *updates.Updater
+	updates *updates.Updates
 	base    *base.Base
 }
 
-func (stub *testInstance) IntelUpdates() *updates.Updater {
+func (stub *testInstance) Updates() *updates.Updates {
 	return stub.updates
 }
 
@@ -62,16 +62,6 @@ func runTest(m *testing.M) error {
 	}
 	defer func() { _ = os.RemoveAll(ds) }()
 
-	installDir, err := os.MkdirTemp("", "hub_installdir")
-	if err != nil {
-		return fmt.Errorf("failed to create tmp install dir: %w", err)
-	}
-	defer func() { _ = os.RemoveAll(installDir) }()
-	err = updates.GenerateMockFolder(installDir, "Test Intel", "1.0.0")
-	if err != nil {
-		return fmt.Errorf("failed to generate mock installation: %w", err)
-	}
-
 	stub := &testInstance{}
 	// Init
 	stub.db, err = dbmodule.New(stub)
@@ -86,10 +76,7 @@ func runTest(m *testing.M) error {
 	if err != nil {
 		return fmt.Errorf("failed to create config: %w", err)
 	}
-	stub.updates, err = updates.New(stub, "Test Intel", updates.Config{
-		Directory: installDir,
-		IndexFile: "index.json",
-	})
+	stub.updates, err = updates.New(stub)
 	if err != nil {
 		return fmt.Errorf("failed to create updates: %w", err)
 	}
diff --git a/spn/instance.go b/spn/instance.go
index dcfe01aa..69114d38 100644
--- a/spn/instance.go
+++ b/spn/instance.go
@@ -9,7 +9,6 @@ import (
 	"github.com/safing/portmaster/base/api"
 	"github.com/safing/portmaster/base/config"
 	"github.com/safing/portmaster/base/database/dbmodule"
-	"github.com/safing/portmaster/base/log"
 	"github.com/safing/portmaster/base/metrics"
 	"github.com/safing/portmaster/base/notifications"
 	"github.com/safing/portmaster/base/rng"
@@ -35,17 +34,10 @@ import (
 
 // Instance is an instance of a Portmaster service.
 type Instance struct {
-	ctx       context.Context
-	cancelCtx context.CancelFunc
-
-	shutdownCtx       context.Context
-	cancelShutdownCtx context.CancelFunc
-
+	ctx          context.Context
+	cancelCtx    context.CancelFunc
 	serviceGroup *mgr.Group
 
-	binDir  string
-	dataDir string
-
 	exitCode atomic.Int32
 
 	base     *base.Base
@@ -56,12 +48,11 @@ type Instance struct {
 	runtime  *runtime.Runtime
 	rng      *rng.Rng
 
-	core          *core.Core
-	binaryUpdates *updates.Updater
-	intelUpdates  *updates.Updater
-	geoip         *geoip.GeoIP
-	netenv        *netenv.NetEnv
-	filterLists   *filterlists.FilterLists
+	core        *core.Core
+	updates     *updates.Updates
+	geoip       *geoip.GeoIP
+	netenv      *netenv.NetEnv
+	filterLists *filterlists.FilterLists
 
 	access    *access.Access
 	cabin     *cabin.Cabin
@@ -75,7 +66,6 @@ type Instance struct {
 	terminal  *terminal.TerminalModule
 
 	CommandLineOperation func() error
-	ShouldRestart        bool
 }
 
 // New returns a new Portmaster service instance.
@@ -83,20 +73,6 @@ func New() (*Instance, error) {
 	// Create instance to pass it to modules.
 	instance := &Instance{}
 	instance.ctx, instance.cancelCtx = context.WithCancel(context.Background())
-	instance.shutdownCtx, instance.cancelShutdownCtx = context.WithCancel(context.Background())
-
-	binaryUpdateIndex := updates.Config{
-		// FIXME: fill
-	}
-
-	intelUpdateIndex := updates.Config{
-		// FIXME: fill
-	}
-
-	// Initialize log
-	log.GlobalWriter = log.NewStdoutWriter()
-
-	// FIXME: initialize log file.
 
 	var err error
 
@@ -135,11 +111,7 @@ func New() (*Instance, error) {
 	if err != nil {
 		return instance, fmt.Errorf("create core module: %w", err)
 	}
-	instance.binaryUpdates, err = updates.New(instance, "Binary Updater", binaryUpdateIndex)
-	if err != nil {
-		return instance, fmt.Errorf("create updates module: %w", err)
-	}
-	instance.intelUpdates, err = updates.New(instance, "Intel Updater", intelUpdateIndex)
+	instance.updates, err = updates.New(instance)
 	if err != nil {
 		return instance, fmt.Errorf("create updates module: %w", err)
 	}
@@ -209,8 +181,7 @@ func New() (*Instance, error) {
 		instance.rng,
 
 		instance.core,
-		instance.binaryUpdates,
-		instance.intelUpdates,
+		instance.updates,
 		instance.geoip,
 		instance.netenv,
 
@@ -249,18 +220,6 @@ func (i *Instance) SetSleep(enabled bool) {
 	}
 }
 
-// BinDir returns the directory for binaries.
-// This directory may be read-only.
-func (i *Instance) BinDir() string {
-	return i.binDir
-}
-
-// DataDir returns the directory for variable data.
-// This directory is expected to be read/writeable.
-func (i *Instance) DataDir() string {
-	return i.dataDir
-}
-
 // Database returns the database module.
 func (i *Instance) Database() *dbmodule.DBModule {
 	return i.database
@@ -296,14 +255,9 @@ func (i *Instance) Base() *base.Base {
 	return i.base
 }
 
-// BinaryUpdates returns the updates module.
-func (i *Instance) BinaryUpdates() *updates.Updater {
-	return i.binaryUpdates
-}
-
-// IntelUpdates returns the updates module.
-func (i *Instance) IntelUpdates() *updates.Updater {
-	return i.intelUpdates
+// Updates returns the updates module.
+func (i *Instance) Updates() *updates.Updates {
+	return i.updates
 }
 
 // GeoIP returns the geoip module.
@@ -406,6 +360,12 @@ func (i *Instance) Ready() bool {
 	return i.serviceGroup.Ready()
 }
 
+// Ctx returns the instance context.
+// It is only canceled on shutdown.
+func (i *Instance) Ctx() context.Context {
+	return i.ctx
+}
+
 // Start starts the instance.
 func (i *Instance) Start() error {
 	return i.serviceGroup.Start()
@@ -413,6 +373,7 @@ func (i *Instance) Start() error {
 
 // Stop stops the instance and cancels the instance context when done.
 func (i *Instance) Stop() error {
+	defer i.cancelCtx()
 	return i.serviceGroup.Stop()
 }
 
@@ -426,8 +387,6 @@ func (i *Instance) Restart() {
 	i.core.EventRestart.Submit(struct{}{})
 	time.Sleep(10 * time.Millisecond)
 
-	// Set the restart flag and shutdown.
-	i.ShouldRestart = true
 	i.shutdown(RestartExitCode)
 }
 
@@ -441,63 +400,32 @@ func (i *Instance) Shutdown() {
 }
 
 func (i *Instance) shutdown(exitCode int) {
-	// Only shutdown once.
-	if i.IsShuttingDown() {
-		return
-	}
-
-	// Cancel main  context.
-	i.cancelCtx()
-
 	// Set given exit code.
 	i.exitCode.Store(int32(exitCode))
 
-	// Start shutdown asynchronously in a separate manager.
 	m := mgr.New("instance")
 	m.Go("shutdown", func(w *mgr.WorkerCtx) error {
-		// Stop all modules.
-		if err := i.Stop(); err != nil {
-			w.Error("failed to shutdown", "err", err)
+		for {
+			if err := i.Stop(); err != nil {
+				w.Error("failed to shutdown", "err", err, "retry", "1s")
+				time.Sleep(1 * time.Second)
+			} else {
+				return nil
+			}
 		}
-
-		// Cancel shutdown process context.
-		i.cancelShutdownCtx()
-		return nil
 	})
 }
 
-// Ctx returns the instance context.
-// It is canceled when shutdown is started.
-func (i *Instance) Ctx() context.Context {
-	return i.ctx
+// Stopping returns whether the instance is shutting down.
+func (i *Instance) Stopping() bool {
+	return i.ctx.Err() == nil
 }
 
-// IsShuttingDown returns whether the instance is shutting down.
-func (i *Instance) IsShuttingDown() bool {
-	return i.ctx.Err() != nil
-}
-
-// ShuttingDown returns a channel that is triggered when the instance starts shutting down.
-func (i *Instance) ShuttingDown() <-chan struct{} {
+// Stopped returns a channel that is triggered when the instance has shut down.
+func (i *Instance) Stopped() <-chan struct{} {
 	return i.ctx.Done()
 }
 
-// ShutdownCtx returns the instance shutdown context.
-// It is canceled when shutdown is complete.
-func (i *Instance) ShutdownCtx() context.Context {
-	return i.shutdownCtx
-}
-
-// IsShutDown returns whether the instance has stopped.
-func (i *Instance) IsShutDown() bool {
-	return i.shutdownCtx.Err() != nil
-}
-
-// ShutDownComplete returns a channel that is triggered when the instance has shut down.
-func (i *Instance) ShutdownComplete() <-chan struct{} {
-	return i.shutdownCtx.Done()
-}
-
 // ExitCode returns the set exit code of the instance.
 func (i *Instance) ExitCode() int {
 	return int(i.exitCode.Load())
diff --git a/spn/navigator/module_test.go b/spn/navigator/module_test.go
index a35cf9f6..c0de9194 100644
--- a/spn/navigator/module_test.go
+++ b/spn/navigator/module_test.go
@@ -19,12 +19,12 @@ type testInstance struct {
 	db      *dbmodule.DBModule
 	api     *api.API
 	config  *config.Config
-	updates *updates.Updater
+	updates *updates.Updates
 	base    *base.Base
 	geoip   *geoip.GeoIP
 }
 
-func (stub *testInstance) IntelUpdates() *updates.Updater {
+func (stub *testInstance) Updates() *updates.Updates {
 	return stub.updates
 }
 
@@ -62,16 +62,6 @@ func runTest(m *testing.M) error {
 	}
 	defer func() { _ = os.RemoveAll(ds) }()
 
-	installDir, err := os.MkdirTemp("", "geoip_installdir")
-	if err != nil {
-		return fmt.Errorf("failed to create tmp install dir: %w", err)
-	}
-	defer func() { _ = os.RemoveAll(installDir) }()
-	err = updates.GenerateMockFolder(installDir, "Test Intel", "1.0.0")
-	if err != nil {
-		return fmt.Errorf("failed to generate mock installation: %w", err)
-	}
-
 	stub := &testInstance{}
 	log.SetLogLevel(log.DebugLevel)
 
@@ -88,10 +78,7 @@ func runTest(m *testing.M) error {
 	if err != nil {
 		return fmt.Errorf("failed to create config: %w", err)
 	}
-	stub.updates, err = updates.New(stub, "Test Intel", updates.Config{
-		Directory: installDir,
-		IndexFile: "index.json",
-	})
+	stub.updates, err = updates.New(stub)
 	if err != nil {
 		return fmt.Errorf("failed to create updates: %w", err)
 	}