summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAAGaming <aagaming@riseup.net>2025-07-28 20:58:59 -0400
committerGitHub <noreply@github.com>2025-07-28 20:58:59 -0400
commit8f41eb93ef80bfbf3851ce8a82ea0f88c87e6c68 (patch)
tree6cbfdf1843dc0aba959e9b76201df149a71964fc
parent670ae7d8a7d8fc8efc28ce403865177ebf0b715e (diff)
downloaddecky-loader-8f41eb93ef80bfbf3851ce8a82ea0f88c87e6c68.tar.gz
decky-loader-8f41eb93ef80bfbf3851ce8a82ea0f88c87e6c68.zip
Merge commit from forkv3.1.10
* fix incorrect permissions on plugin directories * chown plugin dirs too * fix the stupid * cleanup useless comments
-rw-r--r--backend/decky_loader/browser.py35
-rw-r--r--backend/decky_loader/enums.py5
-rw-r--r--backend/decky_loader/helpers.py3
-rw-r--r--backend/decky_loader/localplatform/localplatformlinux.py17
-rw-r--r--backend/decky_loader/localplatform/localplatformwin.py5
-rw-r--r--backend/decky_loader/main.py2
-rw-r--r--backend/decky_loader/plugin/plugin.py23
-rw-r--r--backend/decky_loader/plugin/sandboxed_plugin.py8
-rw-r--r--backend/decky_loader/settings.py6
-rw-r--r--backend/decky_loader/wsrouter.py4
-rw-r--r--backend/poetry.lock48
-rw-r--r--backend/pyproject.toml2
12 files changed, 94 insertions, 64 deletions
diff --git a/backend/decky_loader/browser.py b/backend/decky_loader/browser.py
index 4a63f6ec..975a917a 100644
--- a/backend/decky_loader/browser.py
+++ b/backend/decky_loader/browser.py
@@ -18,9 +18,10 @@ from enum import IntEnum
from typing import Dict, List, TypedDict
# Local modules
-from .localplatform.localplatform import chown, chmod
+from .localplatform.localplatform import chown, chmod, get_chown_plugin_path
from .loader import Loader, Plugins
from .helpers import get_ssl_context, download_remote_binary_to_path
+from .enums import UserType
from .settings import SettingsManager
logger = getLogger("Browser")
@@ -60,13 +61,6 @@ class PluginBrowser:
return False
zip_file = ZipFile(zip)
zip_file.extractall(self.plugin_path)
- plugin_folder = self.find_plugin_folder(name)
- assert plugin_folder is not None
- plugin_dir = path.join(self.plugin_path, plugin_folder)
-
- if not chown(plugin_dir) or not chmod(plugin_dir, 555):
- logger.error(f"chown/chmod exited with a non-zero exit code")
- return False
return True
async def _download_remote_binaries_for_plugin_with_name(self, pluginBasePath: str):
@@ -101,8 +95,6 @@ class PluginBrowser:
rv = False
raise Exception(f"Error Downloading Remote Binary {binName}@{binURL} with hash {binHash} to {path.join(pluginBinPath, binName)}")
- chown(self.plugin_path)
- chmod(pluginBasePath, 555)
else:
rv = True
logger.info(f"No Remote Binaries to Download")
@@ -124,6 +116,25 @@ class PluginBrowser:
return folder
except:
logger.debug(f"skipping {folder}")
+
+ def set_plugin_dir_permissions(self, plugin_dir: str) -> bool:
+ plugin_json_path = path.join(plugin_dir, 'plugin.json')
+ logger.debug(f"Checking plugin.json at {plugin_json_path}")
+
+ root_plugin = False
+
+ if access(plugin_json_path, R_OK):
+ with open(plugin_json_path, "r", encoding="utf-8") as f:
+ plugin_json = json.load(f)
+ if "flags" in plugin_json and "root" in plugin_json["flags"]:
+ root_plugin = True
+
+ logger.debug("root_plugin %d, dir %s", root_plugin, plugin_dir)
+ if get_chown_plugin_path():
+ return chown(plugin_dir, UserType.EFFECTIVE_USER if root_plugin else UserType.HOST_USER, True) and chown(plugin_dir, UserType.EFFECTIVE_USER, False) and chmod(plugin_dir, 755) and chown(plugin_json_path, UserType.EFFECTIVE_USER, False) and chmod(plugin_json_path, 755)
+ else:
+ logger.debug("chown disabled by environment")
+ return True
async def uninstall_plugin(self, name: str):
if self.loader.watcher:
@@ -266,6 +277,7 @@ class PluginBrowser:
plugin_dir = path.join(self.plugin_path, plugin_folder)
await self.loader.ws.emit("loader/plugin_download_info", 95, "Store.download_progress_info.download_remote")
ret = await self._download_remote_binaries_for_plugin_with_name(plugin_dir)
+ chown_ret = self.set_plugin_dir_permissions(plugin_dir)
if ret:
logger.info(f"Installed {name} (Version: {version})")
if name in self.loader.plugins:
@@ -278,6 +290,9 @@ class PluginBrowser:
self.settings.setSetting("pluginOrder", current_plugin_order)
logger.debug("Plugin %s was added to the pluginOrder setting", name)
await self.loader.import_plugin(path.join(plugin_dir, "main.py"), plugin_folder)
+ elif not chown_ret:
+ logger.error("Could not chown plugin")
+ return
else:
logger.error("Could not download remote binaries")
return
diff --git a/backend/decky_loader/enums.py b/backend/decky_loader/enums.py
index e7fb4905..4a6296ad 100644
--- a/backend/decky_loader/enums.py
+++ b/backend/decky_loader/enums.py
@@ -1,9 +1,8 @@
from enum import IntEnum
class UserType(IntEnum):
- HOST_USER = 1
- EFFECTIVE_USER = 2
- ROOT = 3
+ HOST_USER = 1 # usually deck
+ EFFECTIVE_USER = 2 # usually root
class PluginLoadType(IntEnum):
LEGACY_EVAL_IIFE = 0 # legacy, uses legacy serverAPI
diff --git a/backend/decky_loader/helpers.py b/backend/decky_loader/helpers.py
index 857503ab..1621652f 100644
--- a/backend/decky_loader/helpers.py
+++ b/backend/decky_loader/helpers.py
@@ -181,7 +181,8 @@ def get_user_group_id() -> int:
# Get the default home path unless a user is specified
def get_home_path(username: str | None = None) -> str:
- return localplatform.get_home_path(UserType.ROOT if username == "root" else UserType.HOST_USER)
+ # TODO hardcoded root is kinda a hack
+ return localplatform.get_home_path(UserType.EFFECTIVE_USER if username == "root" else UserType.HOST_USER)
async def is_systemd_unit_active(unit_name: str) -> bool:
return await localplatform.service_active(unit_name)
diff --git a/backend/decky_loader/localplatform/localplatformlinux.py b/backend/decky_loader/localplatform/localplatformlinux.py
index beaa2540..1c8f2ace 100644
--- a/backend/decky_loader/localplatform/localplatformlinux.py
+++ b/backend/decky_loader/localplatform/localplatformlinux.py
@@ -59,8 +59,6 @@ def chown(path : str, user : UserType = UserType.HOST_USER, recursive : bool =
user_str = _get_user()+":"+_get_user_group()
elif user == UserType.EFFECTIVE_USER:
user_str = _get_effective_user()+":"+_get_effective_user_group()
- elif user == UserType.ROOT:
- user_str = "root:root"
else:
raise Exception("Unknown User Type")
@@ -87,7 +85,7 @@ def chmod(path : str, permissions : int, recursive : bool = True) -> bool:
return True
-def folder_owner(path : str) -> UserType|None:
+def file_owner(path : str) -> UserType|None:
user_owner = _get_user_owner(path)
if (user_owner == _get_user()):
@@ -106,13 +104,14 @@ def get_home_path(user : UserType = UserType.HOST_USER) -> str:
user_name = _get_user()
elif user == UserType.EFFECTIVE_USER:
user_name = _get_effective_user()
- elif user == UserType.ROOT:
- pass
else:
raise Exception("Unknown User Type")
return pwd.getpwnam(user_name).pw_dir
+def get_effective_username() -> str:
+ return _get_effective_user()
+
def get_username() -> str:
return _get_user()
@@ -121,8 +120,8 @@ def setgid(user : UserType = UserType.HOST_USER):
if user == UserType.HOST_USER:
user_id = _get_user_group_id()
- elif user == UserType.ROOT:
- pass
+ elif user == UserType.EFFECTIVE_USER:
+ pass # we already are
else:
raise Exception("Unknown user type")
@@ -133,8 +132,8 @@ def setuid(user : UserType = UserType.HOST_USER):
if user == UserType.HOST_USER:
user_id = _get_user_id()
- elif user == UserType.ROOT:
- pass
+ elif user == UserType.EFFECTIVE_USER:
+ pass # we already are
else:
raise Exception("Unknown user type")
diff --git a/backend/decky_loader/localplatform/localplatformwin.py b/backend/decky_loader/localplatform/localplatformwin.py
index 7e3bd31c..8755c943 100644
--- a/backend/decky_loader/localplatform/localplatformwin.py
+++ b/backend/decky_loader/localplatform/localplatformwin.py
@@ -7,7 +7,7 @@ def chown(path : str, user : UserType = UserType.HOST_USER, recursive : bool =
def chmod(path : str, permissions : int, recursive : bool = True) -> bool:
return True # Stubbed
-def folder_owner(path : str) -> UserType|None:
+def file_owner(path : str) -> UserType|None:
return UserType.HOST_USER # Stubbed
def get_home_path(user : UserType = UserType.HOST_USER) -> str:
@@ -34,6 +34,9 @@ async def service_restart(service_name : str, block : bool = True) -> bool:
return True # Stubbed
+def get_effective_username() -> str:
+ return os.getlogin()
+
def get_username() -> str:
return os.getlogin()
diff --git a/backend/decky_loader/main.py b/backend/decky_loader/main.py
index 315b7d29..16caca2e 100644
--- a/backend/decky_loader/main.py
+++ b/backend/decky_loader/main.py
@@ -50,7 +50,7 @@ def chown_plugin_dir():
if not path.exists(plugin_path): # For safety, create the folder before attempting to do anything with it
mkdir_as_user(plugin_path)
- if not chown(plugin_path, UserType.HOST_USER) or not chmod(plugin_path, 555):
+ if not chown(plugin_path, UserType.EFFECTIVE_USER, False) or not chmod(plugin_path, 755, False):
logger.error(f"chown/chmod exited with a non-zero exit code")
if get_chown_plugin_path() == True:
diff --git a/backend/decky_loader/plugin/plugin.py b/backend/decky_loader/plugin/plugin.py
index ba23fff9..61de4b1f 100644
--- a/backend/decky_loader/plugin/plugin.py
+++ b/backend/decky_loader/plugin/plugin.py
@@ -8,7 +8,8 @@ from traceback import format_exc
from .sandboxed_plugin import SandboxedPlugin
from .messages import MethodCallRequest, SocketMessageType
-from ..enums import PluginLoadType
+from ..enums import PluginLoadType, UserType
+from ..localplatform.localplatform import file_owner, chown, chmod, get_chown_plugin_path
from ..localplatform.localsocket import LocalSocket
from ..helpers import get_homebrew_path, mkdir_as_user
@@ -26,9 +27,12 @@ class PluginWrapper:
self.load_type = PluginLoadType.LEGACY_EVAL_IIFE.value
- json = load(open(path.join(plugin_path, plugin_directory, "plugin.json"), "r", encoding="utf-8"))
- if path.isfile(path.join(plugin_path, plugin_directory, "package.json")):
- package_json = load(open(path.join(plugin_path, plugin_directory, "package.json"), "r", encoding="utf-8"))
+ plugin_dir_path = path.join(plugin_path, plugin_directory)
+ plugin_json_path = path.join(plugin_dir_path, "plugin.json")
+
+ json = load(open(plugin_json_path, "r", encoding="utf-8"))
+ if path.isfile(path.join(plugin_dir_path, "package.json")):
+ package_json = load(open(path.join(plugin_dir_path, "package.json"), "r", encoding="utf-8"))
self.version = package_json["version"]
if ("type" in package_json and package_json["type"] == "module"):
self.load_type = PluginLoadType.ESMODULE_V1.value
@@ -42,6 +46,17 @@ class PluginWrapper:
self.log = getLogger("plugin")
+ if get_chown_plugin_path():
+ # ensure plugin folder ownership
+ if file_owner(plugin_dir_path) != UserType.EFFECTIVE_USER:
+ chown(plugin_dir_path, UserType.EFFECTIVE_USER if "root" in self.flags else UserType.HOST_USER, True)
+ chown(plugin_dir_path, UserType.EFFECTIVE_USER, False)
+ chmod(plugin_dir_path, 755, True)
+ # fix plugin.json permissions
+ if file_owner(plugin_json_path) != UserType.EFFECTIVE_USER:
+ chown(plugin_json_path, UserType.EFFECTIVE_USER, False)
+ chmod(plugin_json_path, 755, False)
+
self.sandboxed_plugin = SandboxedPlugin(self.name, self.passive, self.flags, self.file, self.plugin_directory, self.plugin_path, self.version, self.author, self.api_version)
self.proc: Process | None = None
self._socket = LocalSocket()
diff --git a/backend/decky_loader/plugin/sandboxed_plugin.py b/backend/decky_loader/plugin/sandboxed_plugin.py
index 7d88719f..71e1d17b 100644
--- a/backend/decky_loader/plugin/sandboxed_plugin.py
+++ b/backend/decky_loader/plugin/sandboxed_plugin.py
@@ -13,7 +13,7 @@ from .messages import SocketResponseDict, SocketMessageType
from ..localplatform.localsocket import LocalSocket
from ..localplatform.localplatform import setgid, setuid, get_username, get_home_path, ON_LINUX
from ..enums import UserType
-from .. import helpers, settings, injector # pyright: ignore [reportUnusedImport]
+from .. import helpers
from typing import List, TypeVar, Any
@@ -61,10 +61,10 @@ class SandboxedPlugin:
if self.passive:
return
- setgid(UserType.ROOT if "root" in self.flags else UserType.HOST_USER)
- setuid(UserType.ROOT if "root" in self.flags else UserType.HOST_USER)
+ setgid(UserType.EFFECTIVE_USER if "root" in self.flags else UserType.HOST_USER)
+ setuid(UserType.EFFECTIVE_USER if "root" in self.flags else UserType.HOST_USER)
# export a bunch of environment variables to help plugin developers
- environ["HOME"] = get_home_path(UserType.ROOT if "root" in self.flags else UserType.HOST_USER)
+ environ["HOME"] = get_home_path(UserType.EFFECTIVE_USER if "root" in self.flags else UserType.HOST_USER)
environ["USER"] = "root" if "root" in self.flags else get_username()
environ["DECKY_VERSION"] = helpers.get_loader_version()
environ["DECKY_USER"] = get_username()
diff --git a/backend/decky_loader/settings.py b/backend/decky_loader/settings.py
index b5f034aa..73dbe38a 100644
--- a/backend/decky_loader/settings.py
+++ b/backend/decky_loader/settings.py
@@ -1,7 +1,7 @@
from json import dump, load
from os import mkdir, path, listdir, rename
from typing import Any, Dict
-from .localplatform.localplatform import chown, folder_owner, get_chown_plugin_path
+from .localplatform.localplatform import chown, file_owner, get_chown_plugin_path
from .enums import UserType
from .helpers import get_homebrew_path
@@ -28,8 +28,8 @@ class SettingsManager:
#If the owner of the settings directory is not the user, then set it as the user:
- expected_user = UserType.HOST_USER if get_chown_plugin_path() else UserType.ROOT
- if folder_owner(settings_directory) != expected_user:
+ expected_user = UserType.HOST_USER if get_chown_plugin_path() else UserType.EFFECTIVE_USER
+ if file_owner(settings_directory) != expected_user:
chown(settings_directory, expected_user, False)
self.settings: Dict[str, Any] = {}
diff --git a/backend/decky_loader/wsrouter.py b/backend/decky_loader/wsrouter.py
index 8a019b44..691abd05 100644
--- a/backend/decky_loader/wsrouter.py
+++ b/backend/decky_loader/wsrouter.py
@@ -7,7 +7,7 @@ from aiohttp.web import Application, WebSocketResponse, Request, Response, get
from enum import IntEnum
-from typing import Callable, Coroutine, Dict, Any, cast, TypeVar
+from typing import Callable, Coroutine, Dict, Any, cast
from traceback import format_exc
@@ -29,8 +29,6 @@ class WSMessageExtra(WSMessage):
# see wsrouter.ts for typings
-DataType = TypeVar("DataType")
-
Route = Callable[..., Coroutine[Any, Any, Any]]
class WSRouter:
diff --git a/backend/poetry.lock b/backend/poetry.lock
index bd67b3bb..03300f16 100644
--- a/backend/poetry.lock
+++ b/backend/poetry.lock
@@ -1,4 +1,4 @@
-# This file is automatically @generated by Poetry 2.1.1 and should not be changed by hand.
+# This file is automatically @generated by Poetry 2.1.3 and should not be changed by hand.
[[package]]
name = "aiohappyeyeballs"
@@ -190,7 +190,7 @@ description = "Timeout context manager for asyncio programs"
optional = false
python-versions = ">=3.7"
groups = ["main"]
-markers = "python_version < \"3.11\""
+markers = "python_version == \"3.10\""
files = [
{file = "async-timeout-4.0.3.tar.gz", hash = "sha256:4640d96be84d82d02ed59ea2b7105a0f7b33abe8703703cd0ab0bf87c427522f"},
{file = "async_timeout-4.0.3-py3-none-any.whl", hash = "sha256:7405140ff1230c310e51dc27b3145b9092d659ce68ff733fb0cefe3ee42be028"},
@@ -678,32 +678,32 @@ files = [
[[package]]
name = "pyinstaller"
-version = "6.8.0"
+version = "6.14.2"
description = "PyInstaller bundles a Python application and all its dependencies into a single package."
optional = false
-python-versions = "<3.13,>=3.8"
+python-versions = "<3.14,>=3.8"
groups = ["dev"]
files = [
- {file = "pyinstaller-6.8.0-py3-none-macosx_10_13_universal2.whl", hash = "sha256:5ff6bc2784c1026f8e2f04aa3760cbed41408e108a9d4cf1dd52ee8351a3f6e1"},
- {file = "pyinstaller-6.8.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:39ac424d2ee2457d2ab11a5091436e75a0cccae207d460d180aa1fcbbafdd528"},
- {file = "pyinstaller-6.8.0-py3-none-manylinux2014_i686.whl", hash = "sha256:355832a3acc7de90a255ecacd4b9f9e166a547a79c8905d49f14e3a75c1acdb9"},
- {file = "pyinstaller-6.8.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:6303c7a009f47e6a96ef65aed49f41e36ece8d079b9193ca92fe807403e5fe80"},
- {file = "pyinstaller-6.8.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:2b71509468c811968c0b5decb5bbe85b6292ea52d7b1f26313d2aabb673fa9a5"},
- {file = "pyinstaller-6.8.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:ff31c5b99e05a4384bbe2071df67ec8b2b347640a375eae9b40218be2f1754c6"},
- {file = "pyinstaller-6.8.0-py3-none-musllinux_1_1_aarch64.whl", hash = "sha256:000c36b13fe4cd8d0d8c2bc855b1ddcf39867b5adf389e6b5ca45b25fa3e619d"},
- {file = "pyinstaller-6.8.0-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:fe0af018d7d5077180e3144ada89a4da5df8d07716eb7e9482834a56dc57a4e8"},
- {file = "pyinstaller-6.8.0-py3-none-win32.whl", hash = "sha256:d257f6645c7334cbd66f38a4fac62c3ad614cc46302b2b5d9f8cc48c563bce0e"},
- {file = "pyinstaller-6.8.0-py3-none-win_amd64.whl", hash = "sha256:81cccfa9b16699b457f4788c5cc119b50f3cd4d0db924955f15c33f2ad27a50d"},
- {file = "pyinstaller-6.8.0-py3-none-win_arm64.whl", hash = "sha256:1c3060a263758cf7f0144ab4c016097b20451b2469d468763414665db1bb743d"},
- {file = "pyinstaller-6.8.0.tar.gz", hash = "sha256:3f4b6520f4423fe19bcc2fd63ab7238851ae2bdcbc98f25bc5d2f97cc62012e9"},
+ {file = "pyinstaller-6.14.2-py3-none-macosx_10_13_universal2.whl", hash = "sha256:d77d18bf5343a1afef2772393d7a489d4ec2282dee5bca549803fc0d74b78330"},
+ {file = "pyinstaller-6.14.2-py3-none-manylinux2014_aarch64.whl", hash = "sha256:3fa0c391e1300a9fd7752eb1ffe2950112b88fba9d2743eee2ef218a15f4705f"},
+ {file = "pyinstaller-6.14.2-py3-none-manylinux2014_i686.whl", hash = "sha256:077efb2d01d16d9c8fdda3ad52788f0fead2791c5cec9ed6ce058af7e26eb74b"},
+ {file = "pyinstaller-6.14.2-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:fdd2bd020a18736806a6bd5d3c4352f1209b427a96ad6c459d88aec1d90c4f21"},
+ {file = "pyinstaller-6.14.2-py3-none-manylinux2014_s390x.whl", hash = "sha256:03862c6b3cf7b16843d24b529f89cd4077cbe467883cd54ce7a81940d6da09d3"},
+ {file = "pyinstaller-6.14.2-py3-none-manylinux2014_x86_64.whl", hash = "sha256:78827a21ada2a848e98671852d20d74b2955b6e2aaf2359ed13a462e1a603d84"},
+ {file = "pyinstaller-6.14.2-py3-none-musllinux_1_1_aarch64.whl", hash = "sha256:185710ab1503dfdfa14c43237d394d96ac183422d588294be42531480dfa6c38"},
+ {file = "pyinstaller-6.14.2-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:6c673a7e761bd4a2560cfd5dbe1ccdcfe2dff304b774e6e5242fc5afed953661"},
+ {file = "pyinstaller-6.14.2-py3-none-win32.whl", hash = "sha256:1697601aa788e3a52f0b5e620b4741a34b82e6f222ec6e1318b3a1349f566bb2"},
+ {file = "pyinstaller-6.14.2-py3-none-win_amd64.whl", hash = "sha256:e10e0e67288d6dcb5898a917dd1d4272aa0ff33f197ad49a0e39618009d63ed9"},
+ {file = "pyinstaller-6.14.2-py3-none-win_arm64.whl", hash = "sha256:69fd11ca57e572387826afaa4a1b3d4cb74927d76f231f0308c0bd7872ca5ac1"},
+ {file = "pyinstaller-6.14.2.tar.gz", hash = "sha256:142cce0719e79315f0cc26400c2e5c45d9b6b17e7e0491fee444a9f8f16f4917"},
]
[package.dependencies]
altgraph = "*"
macholib = {version = ">=1.8", markers = "sys_platform == \"darwin\""}
packaging = ">=22.0"
-pefile = {version = ">=2022.5.30", markers = "sys_platform == \"win32\""}
-pyinstaller-hooks-contrib = ">=2024.6"
+pefile = {version = ">=2022.5.30,<2024.8.26 || >2024.8.26", markers = "sys_platform == \"win32\""}
+pyinstaller-hooks-contrib = ">=2025.5"
pywin32-ctypes = {version = ">=0.2.1", markers = "sys_platform == \"win32\""}
setuptools = ">=42.0.0"
@@ -713,14 +713,14 @@ hook-testing = ["execnet (>=1.5.0)", "psutil", "pytest (>=2.7.3)"]
[[package]]
name = "pyinstaller-hooks-contrib"
-version = "2024.7"
+version = "2025.8"
description = "Community maintained hooks for PyInstaller"
optional = false
-python-versions = ">=3.7"
+python-versions = ">=3.8"
groups = ["dev"]
files = [
- {file = "pyinstaller_hooks_contrib-2024.7-py2.py3-none-any.whl", hash = "sha256:8bf0775771fbaf96bcd2f4dfd6f7ae6c1dd1b1efe254c7e50477b3c08e7841d8"},
- {file = "pyinstaller_hooks_contrib-2024.7.tar.gz", hash = "sha256:fd5f37dcf99bece184e40642af88be16a9b89613ecb958a8bd1136634fc9fac5"},
+ {file = "pyinstaller_hooks_contrib-2025.8-py3-none-any.whl", hash = "sha256:8d0b8cfa0cb689a619294ae200497374234bd4e3994b3ace2a4442274c899064"},
+ {file = "pyinstaller_hooks_contrib-2025.8.tar.gz", hash = "sha256:3402ad41dfe9b5110af134422e37fc5d421ba342c6cb980bd67cb30b7415641c"},
]
[package.dependencies]
@@ -1041,5 +1041,5 @@ propcache = ">=0.2.0"
[metadata]
lock-version = "2.1"
-python-versions = ">=3.10,<3.13"
-content-hash = "3c9488709e61f3aa21ab47ceb9c677927ce770d8e1e327602a1a6afb09164475"
+python-versions = ">=3.10,<3.14"
+content-hash = "9a331b42c52134230384c1a7348c2856903d82d6007e06cd75eed13842aa21ea"
diff --git a/backend/pyproject.toml b/backend/pyproject.toml
index 38fc304d..27934398 100644
--- a/backend/pyproject.toml
+++ b/backend/pyproject.toml
@@ -14,7 +14,7 @@ include = [
]
[tool.poetry.dependencies]
-python = ">=3.10,<3.13"
+python = ">=3.10,<3.14"
aiohttp = "^3.10.11"
aiohttp-jinja2 = "^1.5.1"