wip: 修改了大量漏洞

This commit is contained in:
朝夕 2026-04-17 22:07:49 +08:00
parent 3a8b1e4054
commit 3cf2411c4a
14 changed files with 98 additions and 67 deletions

View File

@ -1,9 +1,31 @@
待解决问题 待解决问题
--- ---
## 问题栏
#### 🔴 核心缺陷与修复 (Bug Fixes & Stability)
- [ ] /pretor/core/individual每个template进行优化
- [ ] /pretor/worker_individual待完善复合子个体和基础子个体
#### 🛡️ 安全与合规 (Security & Auth)
#### ⚡ 性能与资源优化 (Performance & Scalability)
- [ ] 增加对应全workflow的情况追踪使得在任务运行中人机交互更加自然方便
#### 🏗️ 架构演进 (Architecture & Refactoring)
- [ ] 使用fastapi-users完善用户系统
- [x] /pretor/api的接口函数进行重构
- [ ] /dockerfile待完善
---
## 日志
#### 2026/4/12 #### 2026/4/12
- [ ] /pretor/tool_plugin/approval/approval.py的approval函数event改为依赖注入 - [x] /pretor/api的接口函数进行重构
- [ ] /pretor/core/individual每个template进行优化 - [ ] /pretor/core/individual每个template进行优化
- [ ] /pretor/worker_individual待完善复合子个体和基础子个体 - [ ] /pretor/worker_individual待完善复合子个体和基础子个体
- [ ] /pretor/api待完善 - [ ] /pretor/api待完善
- [ ] /dockerfile待完善 - [ ] /dockerfile待完善
#### 2026/4/16
- [ ] 发布v0.1.0正式版
- [ ] 增加对应全workflow的情况追踪使得在任务运行中人机交互更加自然方便
- [ ] 使用fastapi-users完善用户系统

View File

@ -12,7 +12,7 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
from typing import Union from typing import Union
from pretor.utils.ray_hook import ray_actor_hook
from fastapi import APIRouter, Request, Depends from fastapi import APIRouter, Request, Depends
from pydantic import BaseModel from pydantic import BaseModel
from pretor.utils.access import Accessor, TokenData from pretor.utils.access import Accessor, TokenData
@ -30,22 +30,21 @@ class AgentLocalRegister(BaseModel):
@agent_router.post("") @agent_router.post("")
async def load_agent(agent_register: Union[AgentRegister, AgentLocalRegister], async def load_agent(agent_register: Union[AgentRegister, AgentLocalRegister],
request: Request,
_: TokenData = Depends(Accessor.get_current_user)): _: TokenData = Depends(Accessor.get_current_user)):
global_state_machine = request.app.state.global_state_machine global_state_machine = ray_actor_hook("global_state_machine")
if isinstance(agent_register, AgentLocalRegister): if isinstance(agent_register, AgentLocalRegister):
pass pass
elif isinstance(agent_register, AgentRegister): elif isinstance(agent_register, AgentRegister):
match agent_register.individual_title: match agent_register.individual_title:
case "supervisory_node": case "supervisory_node":
node = request.app.state.supervisory_node node = ray_actor_hook("supervisory_node")
node.create_agent.remote(global_state_machine,agent_register.provider_title,agent_register.model_id) node.create_agent.remote(global_state_machine,agent_register.provider_title,agent_register.model_id)
case "consciousness_node": case "consciousness_node":
node = request.app.state.consciousness_node node = ray_actor_hook("consciousness_node")
node.create_agent.remote(global_state_machine,agent_register.provider_title,agent_register.model_id) node.create_agent.remote(global_state_machine,agent_register.provider_title,agent_register.model_id)
case "control_node": case "control_node":
node = request.app.state.control_node node = ray_actor_hook("control_node")
node.create_agent.remote(global_state_machine,agent_register.provider_title,agent_register.model_id) node.create_agent.remote(global_state_machine,agent_register.provider_title,agent_register.model_id)
case _: case _:
pass pass

View File

@ -12,10 +12,11 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
from fastapi import APIRouter, Request from fastapi import APIRouter
from pydantic import BaseModel from pydantic import BaseModel
from pretor.utils.access import Accessor from pretor.utils.access import Accessor
from fastapi.concurrency import run_in_threadpool from fastapi.concurrency import run_in_threadpool
from pretor.utils.ray_hook import ray_actor_hook
auth_router = APIRouter(prefix="/api/v1/auth", tags=["auth"]) auth_router = APIRouter(prefix="/api/v1/auth", tags=["auth"])
@ -24,10 +25,10 @@ class UserRegister(BaseModel):
password: str password: str
@auth_router.post("/register") @auth_router.post("/register")
async def create_user(user_register: UserRegister, request: Request): async def create_user(user_register: UserRegister):
postgres_database = request.app.state.postgres_database postgres_database = ray_actor_hook("postgres_database")
hashed_password = await run_in_threadpool(Accessor.hash_password, user_register.password) hashed_password = await run_in_threadpool(Accessor.hash_password, user_register.password)
user = await postgres_database.auth_database.add_user.remote(user_register.user_name, hashed_password) user = await postgres_database.add_user.remote(user_register.user_name, hashed_password)
return {"message": "success", "user_id": user.user_id} return {"message": "success", "user_id": user.user_id}
class UserLogin(BaseModel): class UserLogin(BaseModel):
@ -35,9 +36,9 @@ class UserLogin(BaseModel):
password: str password: str
@auth_router.post("/login") @auth_router.post("/login")
async def login_user(user_login: UserLogin, request: Request): async def login_user(user_login: UserLogin):
postgres_database = request.app.state.postgres_database postgres_database = ray_actor_hook("postgres_database")
user = await postgres_database.auth_database.login_user.remote(user_login.user_name) user = await postgres_database.login_user.remote(user_login.user_name)
if user.user_name != user_login.user_name: if user.user_name != user_login.user_name:
pass pass
token = await run_in_threadpool(Accessor.login_hashed_password, user, user_login.password) token = await run_in_threadpool(Accessor.login_hashed_password, user, user_login.password)

View File

@ -14,10 +14,10 @@
from fastapi import APIRouter, Request, Depends, HTTPException, status, WebSocket, WebSocketDisconnect from fastapi import APIRouter, Request, Depends, HTTPException, status, WebSocket, WebSocketDisconnect
from pydantic import BaseModel from pydantic import BaseModel
from pretor.utils.access import Accessor, TokenData from pretor.utils.access import Accessor, TokenData
from pretor.api.platform.event import PretorEvent from pretor.api.platform.event import PretorEvent
from loguru import logger from loguru import logger
from pretor.utils.ray_hook import ray_actor_hook
client_router = APIRouter(prefix="/api/v1/adapter/client", tags=["client"]) client_router = APIRouter(prefix="/api/v1/adapter/client", tags=["client"])
@ -26,7 +26,6 @@ class Message(BaseModel):
@client_router.post("") @client_router.post("")
async def create_message(message: Message, async def create_message(message: Message,
request: Request,
token_data: TokenData = Depends(Accessor.get_current_user)): token_data: TokenData = Depends(Accessor.get_current_user)):
logger.info("收到消息,来源:客户端") logger.info("收到消息,来源:客户端")
logger.debug(f"消息内容:{message.message}") logger.debug(f"消息内容:{message.message}")
@ -34,11 +33,11 @@ async def create_message(message: Message,
user_id=str(token_data.user_id), user_id=str(token_data.user_id),
user_name=token_data.user_name, user_name=token_data.user_name,
message=message.message) message=message.message)
supervisory_node = request.app.state.supervisory_node supervisory_node = ray_actor_hook("supervisor_node")
message = await supervisory_node.working.remote(event) message = await supervisory_node.working.remote(event)
if message == "任务已创建": if message == "任务已创建":
global_state_machine = request.app.state.global_state_machine global_state_machine = ray_actor_hook("global_state_machine")
global_state_machine.add.remote(event) global_state_machine.add_event.remote(event)
return {"message": event.event_id} return {"message": event.event_id}
elif message == "未知相应类型": elif message == "未知相应类型":
raise HTTPException( raise HTTPException(

View File

@ -18,7 +18,7 @@ from typing import Literal
from pretor.utils.access import TokenData, Accessor from pretor.utils.access import TokenData, Accessor
from typing import Dict from typing import Dict
from pretor.core.global_state_machine.model_provider.base_provider import Provider from pretor.core.global_state_machine.model_provider.base_provider import Provider
from pretor.utils.ray_hook import ray_actor_hook
provider_router = APIRouter(prefix="/api/v1/provider", tags=["provider"]) provider_router = APIRouter(prefix="/api/v1/provider", tags=["provider"])
@ -30,9 +30,8 @@ class ProviderRegister(BaseModel):
@provider_router.post("") @provider_router.post("")
async def create_provider(provider_register: ProviderRegister, async def create_provider(provider_register: ProviderRegister,
request: Request,
token_data: TokenData = Depends(Accessor.get_current_user)) -> None: token_data: TokenData = Depends(Accessor.get_current_user)) -> None:
global_state_machine = request.app.state.global_state_machine global_state_machine = ray_actor_hook("global_state_machine")
await global_state_machine.add_provider.remote(provider_type=provider_register.provider_type, await global_state_machine.add_provider.remote(provider_type=provider_register.provider_type,
provider_title=provider_register.provider_title, provider_title=provider_register.provider_title,
provider_url=provider_register.provider_url, provider_url=provider_register.provider_url,
@ -41,8 +40,7 @@ async def create_provider(provider_register: ProviderRegister,
@provider_router.get("/list") @provider_router.get("/list")
async def get_provider_list(request: Request, async def get_provider_list(_: TokenData = Depends(Accessor.get_current_user)) -> Dict[str, Provider]:
_: TokenData = Depends(Accessor.get_current_user)) -> Dict[str, Provider]: global_state_machine = ray_actor_hook("global_state_machine")
global_state_machine = request.app.state.global_state_machine
provider_list: Dict[str, Provider] = await global_state_machine.get_provider_list.remote() provider_list: Dict[str, Provider] = await global_state_machine.get_provider_list.remote()
return {"provider_list": provider_list} return {"provider_list": provider_list}

View File

@ -28,6 +28,8 @@ async def create_workflow_template(workflow_template: WorkflowTemplate,
await global_state_machine.workflow_template_generate.remote(workflow_template) await global_state_machine.workflow_template_generate.remote(workflow_template)
return {"message": "创建成功"} return {"message": "创建成功"}
class Skill(BaseModel): class Skill(BaseModel):
repo_url: str repo_url: str
path: str | None path: str | None
@ -36,6 +38,7 @@ class Skill(BaseModel):
async def install_skill(skill: Skill, async def install_skill(skill: Skill,
_: TokenData = Depends(Accessor.get_current_user)): _: TokenData = Depends(Accessor.get_current_user)):
global_state_machine = ray_actor_hook("global_state_machine") global_state_machine = ray_actor_hook("global_state_machine")
# noinspection PyUnresolvedReferences
await viceroy.install_skill_async(url = skill.repo_url, await viceroy.install_skill_async(url = skill.repo_url,
path = skill.path, path = skill.path,
output = "./pretor/plugin/tool_plugin") output = "./pretor/plugin/tool_plugin")

View File

@ -38,12 +38,6 @@ class PretorGateway:
self.app = FastAPI() self.app = FastAPI()
self.gateway = {} self.gateway = {}
self.app.state.postgres_database = postgres_database
self.app.state.global_state_machine = global_state_machine
self.app.state.supervisory_node = supervisory_node
self.app.state.consciousness_node = consciousness_node
self.app.state.control_node = control_node
self.app.include_router(client_router) self.app.include_router(client_router)
self.app.include_router(auth_router) self.app.include_router(auth_router)
self.app.include_router(provider_router) self.app.include_router(provider_router)

View File

@ -24,7 +24,7 @@ class AuthDatabase:
@database_exception @database_exception
async def add_user(self, user_name: str, hashed_password: str) -> User: async def add_user(self, user_name: str, hashed_password: str) -> User:
user = User(user_name=user_name, hashed_password=hashed_password) user = User(user_name=user_name, hashed_password=hashed_password)
async with self.async_session_maker as session: async with self.async_session_maker() as session:
session.add(user) session.add(user)
await session.commit() await session.commit()
await session.refresh(user) await session.refresh(user)

View File

@ -37,12 +37,26 @@ class PostgresDatabase:
self.auth_database = AuthDatabase(self.async_session_maker) self.auth_database = AuthDatabase(self.async_session_maker)
self.provider_database = ProviderDatabase(self.async_session_maker) self.provider_database = ProviderDatabase(self.async_session_maker)
async def init_db(self) -> None:
async with self.async_engine.begin() as conn:
await conn.run_sync(SQLModel.metadata.create_all)
# provider_database操作
async def get_providers(self): async def get_providers(self):
return await self.provider_database.get_provider() return await self.provider_database.get_provider()
async def add_provider(self, **kwargs): async def add_provider(self, **kwargs):
return await self.provider_database.add_provider(**kwargs) return await self.provider_database.add_provider(**kwargs)
async def init_db(self) -> None: # auth_database操作
async with self.async_engine.begin() as conn: async def add_user(self, **kwargs):
await conn.run_sync(SQLModel.metadata.create_all) return await self.auth_database.add_user(**kwargs)
async def change_password(self, **kwargs):
return await self.auth_database.change_password(**kwargs)
async def delete_user(self, **kwargs):
return await self.auth_database.delete_user(**kwargs)
async def login_user(self, **kwargs):
return await self.auth_database.login_user(**kwargs)

View File

@ -158,7 +158,7 @@ class GlobalStateMachine:
###以下为workflow_template_manager方法 ###以下为workflow_template_manager方法
def workflow_template_generator(self, workflow_template: WorkflowTemplate) -> None: def workflow_template_generate(self, workflow_template: WorkflowTemplate) -> None:
self.global_workflow_template_manager.generate_workflow_template(workflow_template) self.global_workflow_template_manager.generate_workflow_template(workflow_template)
###以下为skill_manager方法 ###以下为skill_manager方法

View File

@ -35,7 +35,6 @@ class WorkflowEngine:
"""工作流当前WorkflowEngine待执行的workflow""" """工作流当前WorkflowEngine待执行的workflow"""
self._steps_by_id: Dict[int, WorkStep] = {step.step: step for step in self.workflow.work_link} self._steps_by_id: Dict[int, WorkStep] = {step.step: step for step in self.workflow.work_link}
"""步骤表将当前workflow的步骤序号和步骤内容存放""" """步骤表将当前workflow的步骤序号和步骤内容存放"""
self.consciousness_node = consciousness_node self.consciousness_node = consciousness_node
"""意识节点""" """意识节点"""
self.control_node = control_node self.control_node = control_node
@ -238,6 +237,9 @@ class WorkflowRunningEngine:
} }
self.workflow_queue = asyncio.Queue() self.workflow_queue = asyncio.Queue()
async def put_workflow(self, workflow: PretorWorkflow) -> None:
await self.workflow_queue.put(workflow)
async def runner(self, i: int) -> None: async def runner(self, i: int) -> None:
""" """
runner方法从self.workflow_queue中不断取出任务并执行 runner方法从self.workflow_queue中不断取出任务并执行

View File

@ -19,18 +19,20 @@ from typing import Optional
from fastapi import HTTPException, status, Request from fastapi import HTTPException, status, Request
from pydantic import BaseModel, ValidationError from pydantic import BaseModel, ValidationError
from pretor.core.database.table.user import User from pretor.core.database.table.user import User
from passlib.context import CryptContext from pwdlib import PasswordHash
from pwdlib.hashers.bcrypt import BcryptHasher
class TokenData(BaseModel): class TokenData(BaseModel):
user_id: str user_id: str
username: Optional[str] = None username: Optional[str] = None
exp: Optional[int] = None exp: Optional[int] = None
SECRET_KEY = os.getenv("SECRET_KEY") SECRET_KEY = os.getenv("SECRET_KEY")
ALGORITHM = "HS256" ALGORITHM = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES = 60 * 24 # 默认有效期 1 天 ACCESS_TOKEN_EXPIRE_MINUTES = 60 * 24
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
password_hasher = PasswordHash.recommended()
class Accessor: class Accessor:
@ -54,19 +56,16 @@ class Accessor:
detail="无效的认证凭证", detail="无效的认证凭证",
) )
@staticmethod @staticmethod
def _create_access_token(data: dict) -> str: def _create_access_token(data: dict) -> str:
to_encode = data.copy() to_encode = data.copy()
expire = datetime.now(timezone.utc) + timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES) expire = datetime.now(timezone.utc) + timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
to_encode.update({"exp": int(expire.timestamp())}) to_encode.update({"exp": int(expire.timestamp())})
encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM) return jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
return encoded_jwt
@staticmethod @staticmethod
def _verify_password(plain_password: str, hashed_password: str) -> bool: def _verify_password(plain_password: str, hashed_password: str) -> bool:
return pwd_context.verify(plain_password, hashed_password) return password_hasher.verify(plain_password, hashed_password)
@staticmethod @staticmethod
def get_current_user(request: Request) -> TokenData: def get_current_user(request: Request) -> TokenData:
@ -92,8 +91,8 @@ class Accessor:
detail="用户名或密码错误", detail="用户名或密码错误",
) )
token_payload = { token_payload = {
"user_id": str(user.id), # 确保是字符串格式 "user_id": str(user.user_id),
"username": user.username "username": user.user_name
} }
return Accessor._create_access_token(data=token_payload) return Accessor._create_access_token(data=token_payload)
@ -103,4 +102,4 @@ class Accessor:
raise ValueError("密码不能为空") raise ValueError("密码不能为空")
if len(password) < 6: if len(password) < 6:
raise ValueError("密码长度不能小于 6 位") raise ValueError("密码长度不能小于 6 位")
return pwd_context.hash(password) return password_hasher.hash(password)

View File

@ -11,8 +11,8 @@ dependencies = [
"httpx>=0.28.1", "httpx>=0.28.1",
"jinja2>=3.1.6", "jinja2>=3.1.6",
"loguru>=0.7.3", "loguru>=0.7.3",
"passlib[bcrypt]>=1.7.4",
"pretor-viceroy>=0.2.0", "pretor-viceroy>=0.2.0",
"pwdlib[bcrypt]>=0.3.0",
"pydantic-ai>=1.73.0", "pydantic-ai>=1.73.0",
"pyfiglet>=1.0.4", "pyfiglet>=1.0.4",
"python-ulid>=3.1.0", "python-ulid>=3.1.0",

32
uv.lock
View File

@ -2915,20 +2915,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/42/32/658973117bf0fd82a24abbfb94fe73a5e86216e49342985e10acce54775a/partial_json_parser-0.2.1.1.post7-py3-none-any.whl", hash = "sha256:145119e5eabcf80cbb13844a6b50a85c68bf99d376f8ed771e2a3c3b03e653ae", size = 10877, upload-time = "2025-11-17T07:27:40.457Z" }, { url = "https://files.pythonhosted.org/packages/42/32/658973117bf0fd82a24abbfb94fe73a5e86216e49342985e10acce54775a/partial_json_parser-0.2.1.1.post7-py3-none-any.whl", hash = "sha256:145119e5eabcf80cbb13844a6b50a85c68bf99d376f8ed771e2a3c3b03e653ae", size = 10877, upload-time = "2025-11-17T07:27:40.457Z" },
] ]
[[package]]
name = "passlib"
version = "1.7.4"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/b6/06/9da9ee59a67fae7761aab3ccc84fa4f3f33f125b370f1ccdb915bf967c11/passlib-1.7.4.tar.gz", hash = "sha256:defd50f72b65c5402ab2c573830a6978e5f202ad0d984793c8dde2c4152ebe04", size = 689844, upload-time = "2020-10-08T19:00:52.121Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/3b/a4/ab6b7589382ca3df236e03faa71deac88cae040af60c071a78d254a62172/passlib-1.7.4-py2.py3-none-any.whl", hash = "sha256:aa6bca462b8d8bda89c70b382f0c298a20b5560af6cbfa2dce410c0a2fb669f1", size = 525554, upload-time = "2020-10-08T19:00:49.856Z" },
]
[package.optional-dependencies]
bcrypt = [
{ name = "bcrypt" },
]
[[package]] [[package]]
name = "pathable" name = "pathable"
version = "0.5.0" version = "0.5.0"
@ -3025,8 +3011,8 @@ dependencies = [
{ name = "httpx" }, { name = "httpx" },
{ name = "jinja2" }, { name = "jinja2" },
{ name = "loguru" }, { name = "loguru" },
{ name = "passlib", extra = ["bcrypt"] },
{ name = "pretor-viceroy" }, { name = "pretor-viceroy" },
{ name = "pwdlib", extra = ["bcrypt"] },
{ name = "pydantic-ai", version = "1.75.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.14'" }, { name = "pydantic-ai", version = "1.75.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.14'" },
{ name = "pydantic-ai", version = "1.84.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.14'" }, { name = "pydantic-ai", version = "1.84.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.14'" },
{ name = "pyfiglet" }, { name = "pyfiglet" },
@ -3055,8 +3041,8 @@ requires-dist = [
{ name = "httpx", specifier = ">=0.28.1" }, { name = "httpx", specifier = ">=0.28.1" },
{ name = "jinja2", specifier = ">=3.1.6" }, { name = "jinja2", specifier = ">=3.1.6" },
{ name = "loguru", specifier = ">=0.7.3" }, { name = "loguru", specifier = ">=0.7.3" },
{ name = "passlib", extras = ["bcrypt"], specifier = ">=1.7.4" },
{ name = "pretor-viceroy", specifier = ">=0.2.0" }, { name = "pretor-viceroy", specifier = ">=0.2.0" },
{ name = "pwdlib", extras = ["bcrypt"], specifier = ">=0.3.0" },
{ name = "pydantic-ai", specifier = ">=1.73.0" }, { name = "pydantic-ai", specifier = ">=1.73.0" },
{ name = "pyfiglet", specifier = ">=1.0.4" }, { name = "pyfiglet", specifier = ">=1.0.4" },
{ name = "python-ulid", specifier = ">=3.1.0" }, { name = "python-ulid", specifier = ">=3.1.0" },
@ -3245,6 +3231,20 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/8c/c7/7bb2e321574b10df20cbde462a94e2b71d05f9bbda251ef27d104668306a/psutil-7.2.2-cp37-abi3-win_arm64.whl", hash = "sha256:8c233660f575a5a89e6d4cb65d9f938126312bca76d8fe087b947b3a1aaac9ee", size = 134617, upload-time = "2026-01-28T18:15:36.514Z" }, { url = "https://files.pythonhosted.org/packages/8c/c7/7bb2e321574b10df20cbde462a94e2b71d05f9bbda251ef27d104668306a/psutil-7.2.2-cp37-abi3-win_arm64.whl", hash = "sha256:8c233660f575a5a89e6d4cb65d9f938126312bca76d8fe087b947b3a1aaac9ee", size = 134617, upload-time = "2026-01-28T18:15:36.514Z" },
] ]
[[package]]
name = "pwdlib"
version = "0.3.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/5f/41/a7c0d8a003c36ce3828ae3ed0391fe6a15aad65f082dbd6bec817ea95c0b/pwdlib-0.3.0.tar.gz", hash = "sha256:6ca30f9642a1467d4f5d0a4d18619de1c77f17dfccb42dd200b144127d3c83fc", size = 215810, upload-time = "2025-10-25T12:44:24.395Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/62/0c/9086a357d02a050fbb3270bf5043ac284dbfb845670e16c9389a41defc9e/pwdlib-0.3.0-py3-none-any.whl", hash = "sha256:f86c15c138858c09f3bba0a10984d4f9178158c55deaa72eac0210849b1a140d", size = 8633, upload-time = "2025-10-25T12:44:23.406Z" },
]
[package.optional-dependencies]
bcrypt = [
{ name = "bcrypt" },
]
[[package]] [[package]]
name = "py-cpuinfo" name = "py-cpuinfo"
version = "9.0.0" version = "9.0.0"