fix: runtime deployment fixes for Docker Compose stack

- Add .gitignore for __pycache__, node_modules, .playwright-mcp
- Add CLAUDE.md project instructions
- docker-compose: remove host port exposure for internal services,
  remove Ollama container (use host), add CORS origin, bake
  NEXT_PUBLIC_API_URL at build time, run alembic migrations on
  gateway startup, add CPU-only torch pre-install
- gateway: add CORS middleware, graceful Slack degradation without
  bot token, fix None guard on slack_handler
- gateway pyproject: add aiohttp dependency for slack-bolt async
- llm-pool pyproject: install litellm from GitHub (removed from PyPI),
  enable hatch direct references
- portal: enable standalone output in next.config.ts
- Remove orphaned migration 003_phase2_audit_kb.py (renamed to 004)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-24 12:26:34 -06:00
parent d936bcf361
commit 0e0ea5fb66
9 changed files with 694 additions and 293 deletions

31
.gitignore vendored Normal file
View File

@@ -0,0 +1,31 @@
# Python
__pycache__/
*.py[cod]
*.egg-info/
dist/
build/
.venv/
*.egg
# Node
node_modules/
.next/
.turbo/
# IDE
.vscode/
.idea/
*.swp
*.swo
# Environment
.env
.env.local
.env.production
# Playwright
.playwright-mcp/
# OS
.DS_Store
Thumbs.db

View File

@@ -157,6 +157,10 @@ None - no external service configuration required. LLM API keys are read from `.
- Docker Compose llm-pool and celery-worker services defined (not yet built/tested in container — deferred to integration phase) - Docker Compose llm-pool and celery-worker services defined (not yet built/tested in container — deferred to integration phase)
- `handle_message` task interface is stable: accepts `KonstructMessage.model_dump()`, returns `{message_id, response, tenant_id}` - `handle_message` task interface is stable: accepts `KonstructMessage.model_dump()`, returns `{message_id, response, tenant_id}`
## Self-Check: PASSED
All key files verified present. Both task commits (ee2f88e, 8257c55) verified in git log. 19 integration tests pass. SUMMARY.md created. STATE.md, ROADMAP.md, REQUIREMENTS.md updated.
--- ---
*Phase: 01-foundation* *Phase: 01-foundation*
*Completed: 2026-03-23* *Completed: 2026-03-23*

455
CLAUDE.md Normal file
View File

@@ -0,0 +1,455 @@
# CLAUDE.md — Konstruct
## What is Konstruct?
Konstruct is an AI workforce platform where clients subscribe to AI employees, teams, or entire AI-run companies. AI workers communicate through familiar channels — Slack, Microsoft Teams, Mattermost, Rocket.Chat, WhatsApp, Telegram, and Signal — so adoption requires zero behavior change from the customer.
Think of it as "Hire an AI department" — not another chatbot SaaS.
---
## Project Identity
- **Codename:** Konstruct
- **Domain:** TBD (check konstruct.ai, konstruct.io, konstruct.dev)
- **Tagline ideas:** "Build your AI workforce" / "AI teams that just work"
- **Inspired by:** [paperclip.ing](https://paperclip.ing)
- **Differentiation:** Channel-native AI workers (not a dashboard), tiered multi-tenancy, BYO-model support
---
## Architecture Overview
### Core Mental Model
```
Client (Slack/Teams/etc.)
┌─────────────────────┐
│ Channel Gateway │ ← Unified ingress for all messaging platforms
│ (webhook/WS) │
└────────┬────────────┘
┌─────────────────────┐
│ Message Router │ ← Tenant resolution, rate limiting, context loading
└────────┬────────────┘
┌─────────────────────┐
│ Agent Orchestrator │ ← Agent selection, tool dispatch, memory, handoffs
│ (per-tenant) │
└────────┬────────────┘
┌─────────────────────┐
│ LLM Backend Pool │ ← LiteLLM router → Ollama / vLLM / OpenAI / Anthropic / BYO
└─────────────────────┘
```
### Key Architectural Principles
1. **Channel-agnostic core** — Business logic never depends on which messaging platform the message came from. The Channel Gateway normalizes everything into a unified internal message format.
2. **Tenant-isolated agent state** — Each tenant's agents have isolated memory, tools, and configuration. No cross-tenant data leakage, ever.
3. **LLM backend as a pluggable resource** — Clients can use platform-provided models, bring their own API keys, or point to their own self-hosted inference endpoints.
4. **Agents are composable** — A single AI employee is an agent. A team is an orchestrated group of agents. A company is a hierarchy of teams with shared context and delegation.
---
## Tech Stack
### Backend (Primary: Python)
| Layer | Technology | Rationale |
|-------|-----------|-----------|
| API Framework | **FastAPI** | Async-native, OpenAPI docs, dependency injection |
| Task Queue | **Celery + Redis** or **Dramatiq** | Background jobs: LLM calls, tool execution, webhooks |
| Database | **PostgreSQL 16** | Primary data store, tenant isolation via schemas or RLS |
| Cache / Pub-Sub | **Redis / Valkey** | Session state, rate limiting, pub/sub for real-time events |
| Vector Store | **pgvector** (start) → **Qdrant** (scale) | Agent memory, RAG, conversation search |
| Object Storage | **MinIO** (self-hosted) / **S3** (cloud burst) | File attachments, documents, agent artifacts |
| LLM Gateway | **LiteLLM** | Unified API across all LLM providers, cost tracking, fallback routing |
| Agent Framework | **Custom** (evaluate LangGraph, CrewAI, or raw) | Agent orchestration, tool use, multi-agent handoffs |
### Messaging Channel SDKs
| Channel | Library / Integration |
|---------|----------------------|
| Slack | `slack-bolt` (Events API + Socket Mode) |
| Microsoft Teams | `botbuilder-python` (Bot Framework SDK) |
| Mattermost | `mattermostdriver` + webhooks |
| Rocket.Chat | REST API + Realtime API (WebSocket) |
| WhatsApp | WhatsApp Business API (Cloud API) |
| Telegram | `python-telegram-bot` (Bot API) |
| Signal | `signal-cli` or `signald` (bridge) |
### Frontend (Admin Dashboard / Client Portal)
| Layer | Technology |
|-------|-----------|
| Framework | **Next.js 14+** (App Router) |
| UI | **Tailwind CSS + shadcn/ui** |
| State | **TanStack Query** |
| Auth | **NextAuth.js** → consider **Keycloak** for enterprise |
### Infrastructure
| Layer | Technology |
|-------|-----------|
| Dev Orchestration | **Docker Compose + Portainer** |
| Prod Orchestration | **Kubernetes (k3s or Talos Linux)** |
| Core Hosting | **Hetzner Dedicated Servers** |
| Cloud Burst | **AWS / GCP** (auto-scale inference, overflow) |
| Reverse Proxy | **NPM Plus** (dev) / **Traefik** (prod K8s ingress) |
| DNS | **Technitium** (internal) / **Cloudflare** (external) |
| VPN Mesh | **Headscale** (self-hosted) + Tailscale clients |
| CI/CD | **Gitea Actions****GitHub Actions** (if public) |
| Monitoring | **Prometheus + Grafana + Loki** |
| Security | **Wazuh** (SIEM), **Trivy** (container scanning) |
---
## Repo Structure
Monorepo to start, split later when service boundaries stabilize.
```
konstruct/
├── CLAUDE.md # This file
├── docker-compose.yml # Local dev environment
├── docker-compose.prod.yml # Production-like local stack
├── k8s/ # Kubernetes manifests / Helm charts
│ ├── base/
│ └── overlays/
│ ├── staging/
│ └── production/
├── packages/
│ ├── gateway/ # Channel Gateway service
│ │ ├── channels/ # Per-channel adapters (slack, teams, etc.)
│ │ ├── normalize.py # Unified message format
│ │ └── main.py
│ ├── router/ # Message Router service
│ │ ├── tenant.py # Tenant resolution
│ │ ├── ratelimit.py
│ │ └── main.py
│ ├── orchestrator/ # Agent Orchestrator service
│ │ ├── agents/ # Agent definitions and behaviors
│ │ ├── teams/ # Multi-agent team logic
│ │ ├── tools/ # Tool registry and execution
│ │ ├── memory/ # Conversation and long-term memory
│ │ └── main.py
│ ├── llm-pool/ # LLM Backend Pool service
│ │ ├── providers/ # Provider configs (litellm router)
│ │ ├── byo/ # BYO key / endpoint management
│ │ └── main.py
│ ├── portal/ # Next.js admin dashboard
│ │ ├── app/
│ │ ├── components/
│ │ └── lib/
│ └── shared/ # Shared Python libs
│ ├── models/ # Pydantic models, DB schemas
│ ├── auth/ # Auth utilities
│ ├── messaging/ # Internal message format
│ └── config/ # Shared config / env management
├── migrations/ # Alembic DB migrations
├── scripts/ # Dev scripts, seed data, utilities
├── tests/
│ ├── unit/
│ ├── integration/
│ └── e2e/
├── docs/ # Architecture docs, ADRs, runbooks
├── pyproject.toml # Python monorepo config (uv / hatch)
└── .env.example
```
---
## Multi-Tenancy Model
Tiered isolation — the level increases with the subscription plan:
| Tier | Isolation | Target |
|------|-----------|--------|
| **Starter** | Shared infra, PostgreSQL RLS, logical separation | Solo founders, micro-businesses |
| **Team** | Dedicated DB schema, isolated Redis namespace, dedicated agent processes | SMBs, small teams |
| **Enterprise** | Dedicated namespace (K8s), dedicated DB, optional dedicated LLM inference | Larger orgs, compliance needs |
| **Self-Hosted** | Customer deploys their own Konstruct instance (Helm chart / Docker Compose) | On-prem requirements, data sovereignty |
### Tenant Resolution Flow
1. Inbound message hits Channel Gateway
2. Gateway extracts workspace/org identifier from the channel metadata (Slack workspace ID, Teams tenant ID, etc.)
3. Router maps channel org → Konstruct tenant via lookup table
4. All subsequent processing scoped to that tenant's context, models, tools, and memory
---
## AI Employee Model
### Hierarchy
```
Company (AI-run)
└── Team
└── Employee (Agent)
├── Role definition (system prompt + persona)
├── Skills (tool bindings)
├── Memory (vector store + conversation history)
├── Channels (which messaging platforms it's active on)
└── Escalation rules (when to hand off to human or another agent)
```
### Employee Configuration (example)
```yaml
employee:
name: "Mara"
role: "Customer Support Lead"
persona: |
Professional, empathetic, solution-oriented.
Fluent in English, Spanish, Portuguese.
Escalates billing disputes to human after 2 failed resolutions.
model:
primary: "anthropic/claude-sonnet-4-20250514"
fallback: "openai/gpt-4o"
local: "ollama/qwen3:32b"
tools:
- zendesk_ticket_create
- zendesk_ticket_lookup
- knowledge_base_search
- calendar_book
channels:
- slack
- whatsapp
memory:
type: "conversational + rag"
retention_days: 90
escalation:
- condition: "billing_dispute AND attempts > 2"
action: "handoff_human"
- condition: "sentiment < -0.7"
action: "handoff_human"
```
### Team Orchestration
Teams use a coordinator pattern:
1. **Coordinator agent** receives the inbound message
2. Coordinator decides which team member(s) should handle it (routing)
3. Specialist agent(s) execute their part
4. Coordinator assembles the final response or delegates follow-up
5. All inter-agent communication logged for audit
---
## LLM Backend Strategy
### Provider Hierarchy
```
┌─────────────────────────────────────────┐
│ LiteLLM Router │
│ (load balancing, fallback, cost caps) │
└────┬──────────┬──────────┬─────────┬────┘
│ │ │ │
Ollama vLLM Anthropic OpenAI
(local) (local) (API) (API)
BYO Endpoint
(customer-provided)
```
### Routing Logic
1. **Tenant config** specifies preferred provider(s) and fallback chain
2. **Cost caps** per tenant (daily/monthly spend limits)
3. **Model routing** by task type: simple queries → smaller/local models, complex reasoning → commercial APIs
4. **BYO keys** stored encrypted (AES-256), never logged, never used for other tenants
---
## Messaging Format (Internal)
All channel adapters normalize messages into this format:
```python
class KonstructMessage(BaseModel):
id: str # UUID
tenant_id: str # Konstruct tenant
channel: ChannelType # slack | teams | mattermost | rocketchat | whatsapp | telegram | signal
channel_metadata: dict # Channel-specific IDs (workspace, channel, thread)
sender: SenderInfo # User ID, display name, role
content: MessageContent # Text, attachments, structured data
timestamp: datetime
thread_id: str | None # For threaded conversations
reply_to: str | None # Parent message ID
context: dict # Extracted intent, entities, sentiment (populated downstream)
```
---
## Security & Compliance
### Non-Negotiables
- **Encryption at rest** (PostgreSQL TDE, MinIO server-side encryption)
- **Encryption in transit** (TLS 1.3 everywhere, mTLS between services)
- **Tenant isolation** enforced at every layer (DB, cache, object storage, agent memory)
- **BYO API keys** encrypted with per-tenant KEK, HSM-backed in Enterprise tier
- **Audit log** for every agent action, tool invocation, and LLM call
- **RBAC** per tenant (admin, manager, member, viewer)
- **Rate limiting** per tenant, per channel, per agent
- **PII handling** — configurable PII detection and redaction per tenant
### Future Compliance Targets
- SOC 2 Type II (when revenue supports it)
- GDPR data residency (leverage Hetzner EU + customer self-hosted option)
- HIPAA (Enterprise self-hosted tier only, with BAA)
---
## Development Workflow
### Local Dev
```bash
# Clone and setup
git clone <repo-url> && cd konstruct
cp .env.example .env
# Start all services
docker compose up -d
# Run gateway in dev mode (hot reload)
cd packages/gateway
uvicorn main:app --reload --port 8001
# Run tests
pytest tests/unit -x
pytest tests/integration -x
```
### Branch Strategy
- `main` — production-ready, protected
- `develop` — integration branch
- `feat/*` — feature branches off develop
- `fix/*` — bugfix branches
- `release/*` — release candidates
### CI Pipeline
1. Lint (`ruff check`, `ruff format --check`)
2. Type check (`mypy --strict`)
3. Unit tests (`pytest tests/unit`)
4. Integration tests (`pytest tests/integration` — spins up Docker Compose)
5. Container build + scan (`trivy image`)
6. Deploy to staging (auto on `develop` merge)
7. Deploy to production (manual approval on `release/*` merge)
---
## Milestones
### Phase 1: Foundation (Weeks 16)
- [ ] Repo scaffolding, CI/CD, Docker Compose dev environment
- [ ] PostgreSQL schema with RLS multi-tenancy
- [ ] Unified message format and Channel Gateway (start with Slack + Telegram)
- [ ] Basic agent orchestrator (single agent per tenant, no teams yet)
- [ ] LiteLLM integration with Ollama + one commercial API
- [ ] Basic admin portal (tenant CRUD, agent config)
### Phase 2: Channel Expansion + Teams (Weeks 712)
- [ ] Add channels: Mattermost, WhatsApp, Teams
- [ ] Multi-agent teams with coordinator pattern
- [ ] Conversational memory (vector store + sliding window)
- [ ] Tool framework (registry, execution, sandboxing)
- [ ] BYO API key support
- [ ] Tenant onboarding flow in portal
### Phase 3: Polish + Launch (Weeks 1318)
- [ ] Add channels: Rocket.Chat, Signal
- [ ] AI company hierarchy (teams of teams)
- [ ] Cost tracking and billing integration (Stripe)
- [ ] Agent performance analytics dashboard
- [ ] Self-hosted deployment option (Helm chart + docs)
- [ ] Public launch (Product Hunt, Hacker News, Reddit)
### Phase 4: Scale (Post-Launch)
- [ ] Kubernetes migration for production workloads
- [ ] Cloud burst infrastructure (AWS auto-scaling inference)
- [ ] Marketplace for pre-built AI employee templates
- [ ] Enterprise tier with dedicated isolation
- [ ] SOC 2 preparation
- [ ] API for programmatic agent management
---
## Coding Standards
### Python
- **Version:** 3.12+
- **Package manager:** `uv`
- **Linting:** `ruff` (replaces flake8, isort, black)
- **Type checking:** `mypy --strict` — no `Any` types in public interfaces
- **Testing:** `pytest` + `pytest-asyncio` + `httpx` (for FastAPI test client)
- **Models:** `Pydantic v2` for all data validation and serialization
- **Async:** Prefer `async def` for all I/O-bound operations
- **DB:** `SQLAlchemy 2.0` async with Alembic migrations
### TypeScript (Portal)
- **Runtime:** Node 20+ LTS
- **Framework:** Next.js 14+ (App Router)
- **Linting:** `eslint` + `prettier`
- **Type checking:** `strict: true` in tsconfig
### General
- Every PR requires at least one approval
- No secrets in code — use `.env` + secrets manager
- Write ADRs (Architecture Decision Records) in `docs/adr/` for significant decisions
- Conventional commits (`feat:`, `fix:`, `chore:`, `docs:`, `refactor:`)
---
## Key Design Decisions (ADR Stubs)
These need full ADRs written before implementation:
1. **ADR-001:** Channel Gateway — webhook-based vs. persistent WebSocket connections per channel
2. **ADR-002:** Agent memory — pgvector vs. dedicated vector DB vs. hybrid
3. **ADR-003:** Multi-tenancy — RLS vs. schema-per-tenant vs. DB-per-tenant
4. **ADR-004:** Agent framework — build custom vs. adopt LangGraph/CrewAI
5. **ADR-005:** BYO key encryption — envelope encryption strategy and key rotation
6. **ADR-006:** Inter-agent communication — direct function calls vs. message bus vs. shared context
7. **ADR-007:** Rate limiting — per-tenant token bucket implementation
8. **ADR-008:** Self-hosted distribution — Helm chart vs. Docker Compose vs. Omnibus
---
## Open Questions
- [ ] Pricing model: per-agent, per-message, per-seat, or hybrid?
- [ ] Should agents maintain persistent identity across channels (same "Mara" on Slack and WhatsApp)?
- [ ] Voice channel support? (Telephony via Twilio/Vonage — Phase 4+?)
- [ ] Agent-to-agent communication across tenants (marketplace scenario)?
- [ ] White-labeling for agencies reselling Konstruct?
---
## References
- [paperclip.ing](https://paperclip.ing) — Inspiration
- [LiteLLM docs](https://docs.litellm.ai/) — LLM gateway
- [Slack Bolt Python](https://slack.dev/bolt-python/) — Slack SDK
- [Bot Framework Python](https://github.com/microsoft/botbuilder-python) — Teams SDK
- [FastAPI docs](https://fastapi.tiangolo.com/) — API framework

View File

@@ -1,5 +1,3 @@
version: "3.9"
networks: networks:
konstruct-net: konstruct-net:
driver: bridge driver: bridge
@@ -7,7 +5,6 @@ networks:
volumes: volumes:
postgres_data: postgres_data:
redis_data: redis_data:
ollama_data:
services: services:
postgres: postgres:
@@ -20,8 +17,6 @@ services:
volumes: volumes:
- postgres_data:/var/lib/postgresql/data - postgres_data:/var/lib/postgresql/data
- ./scripts/init-db.sh:/docker-entrypoint-initdb.d/init-db.sh:ro - ./scripts/init-db.sh:/docker-entrypoint-initdb.d/init-db.sh:ro
ports:
- "5432:5432"
networks: networks:
- konstruct-net - konstruct-net
healthcheck: healthcheck:
@@ -36,8 +31,6 @@ services:
command: redis-server --save 60 1 --loglevel warning command: redis-server --save 60 1 --loglevel warning
volumes: volumes:
- redis_data:/data - redis_data:/data
ports:
- "6379:6379"
networks: networks:
- konstruct-net - konstruct-net
healthcheck: healthcheck:
@@ -46,24 +39,7 @@ services:
timeout: 5s timeout: 5s
retries: 10 retries: 10
ollama: # Ollama runs on the host (port 11434) — containers access it via host.docker.internal
image: ollama/ollama:latest
container_name: konstruct-ollama
volumes:
- ollama_data:/root/.ollama
ports:
- "11434:11434"
networks:
- konstruct-net
deploy:
resources:
reservations:
devices:
- driver: nvidia
count: all
capabilities: [gpu]
# Service starts even if no GPU is available — GPU config is optional
restart: unless-stopped
llm-pool: llm-pool:
build: build:
@@ -71,26 +47,26 @@ services:
dockerfile_inline: | dockerfile_inline: |
FROM python:3.12-slim FROM python:3.12-slim
WORKDIR /app WORKDIR /app
RUN apt-get update -qq && apt-get install -y -qq git curl > /dev/null 2>&1
RUN pip install uv RUN pip install uv
COPY pyproject.toml ./ COPY pyproject.toml ./
COPY packages/shared ./packages/shared COPY packages/shared ./packages/shared
COPY packages/llm-pool ./packages/llm-pool COPY packages/llm-pool ./packages/llm-pool
RUN uv pip install --system -e packages/shared -e packages/llm-pool RUN uv pip install --system torch --index-url https://download.pytorch.org/whl/cpu
RUN uv pip install --system -e packages/shared -e "packages/llm-pool"
CMD ["uvicorn", "llm_pool.main:app", "--host", "0.0.0.0", "--port", "8004"] CMD ["uvicorn", "llm_pool.main:app", "--host", "0.0.0.0", "--port", "8004"]
container_name: konstruct-llm-pool container_name: konstruct-llm-pool
ports: extra_hosts:
- "8004:8004" - "host.docker.internal:host-gateway"
networks: networks:
- konstruct-net - konstruct-net
depends_on: depends_on:
ollama:
condition: service_started
redis: redis:
condition: service_healthy condition: service_healthy
environment: environment:
- ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY:-} - ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY:-}
- OPENAI_API_KEY=${OPENAI_API_KEY:-} - OPENAI_API_KEY=${OPENAI_API_KEY:-}
- OLLAMA_BASE_URL=http://ollama:11434 - OLLAMA_BASE_URL=http://host.docker.internal:11434
- REDIS_URL=redis://redis:6379/0 - REDIS_URL=redis://redis:6379/0
- LOG_LEVEL=INFO - LOG_LEVEL=INFO
restart: unless-stopped restart: unless-stopped
@@ -109,6 +85,7 @@ services:
COPY package.json package-lock.json* ./ COPY package.json package-lock.json* ./
RUN npm ci --production=false RUN npm ci --production=false
COPY . . COPY . .
ENV NEXT_PUBLIC_API_URL=http://100.64.0.10:8001
RUN npm run build RUN npm run build
FROM node:22-alpine AS runner FROM node:22-alpine AS runner
WORKDIR /app WORKDIR /app
@@ -126,9 +103,9 @@ services:
environment: environment:
- NODE_ENV=production - NODE_ENV=production
- API_URL=http://gateway:8001 - API_URL=http://gateway:8001
- NEXT_PUBLIC_API_URL=http://localhost:8001 - NEXT_PUBLIC_API_URL=http://100.64.0.10:8001
- AUTH_SECRET=${AUTH_SECRET:-insecure-dev-secret-change-in-production} - AUTH_SECRET=${AUTH_SECRET:-insecure-dev-secret-change-in-production}
- AUTH_URL=http://localhost:3000 - AUTH_URL=http://100.64.0.10:3000
restart: unless-stopped restart: unless-stopped
healthcheck: healthcheck:
test: ["CMD-SHELL", "wget -q --spider http://localhost:3000 || exit 1"] test: ["CMD-SHELL", "wget -q --spider http://localhost:3000 || exit 1"]
@@ -142,14 +119,18 @@ services:
dockerfile_inline: | dockerfile_inline: |
FROM python:3.12-slim FROM python:3.12-slim
WORKDIR /app WORKDIR /app
RUN apt-get update -qq && apt-get install -y -qq git curl > /dev/null 2>&1
RUN pip install uv RUN pip install uv
COPY pyproject.toml ./ COPY pyproject.toml ./
COPY packages/shared ./packages/shared COPY packages/shared ./packages/shared
COPY packages/router ./packages/router COPY packages/router ./packages/router
COPY packages/gateway ./packages/gateway COPY packages/gateway ./packages/gateway
COPY packages/orchestrator ./packages/orchestrator COPY packages/orchestrator ./packages/orchestrator
COPY migrations ./migrations
COPY alembic.ini ./
RUN uv pip install --system torch --index-url https://download.pytorch.org/whl/cpu
RUN uv pip install --system -e packages/shared -e packages/router -e packages/gateway -e packages/orchestrator RUN uv pip install --system -e packages/shared -e packages/router -e packages/gateway -e packages/orchestrator
CMD ["uvicorn", "gateway.main:app", "--host", "0.0.0.0", "--port", "8001"] CMD ["sh", "-c", "alembic upgrade head && uvicorn gateway.main:app --host 0.0.0.0 --port 8001"]
container_name: konstruct-gateway container_name: konstruct-gateway
ports: ports:
- "8001:8001" - "8001:8001"
@@ -164,6 +145,7 @@ services:
condition: service_started condition: service_started
environment: environment:
- DATABASE_URL=postgresql+asyncpg://konstruct_app:konstruct_dev@postgres:5432/konstruct - DATABASE_URL=postgresql+asyncpg://konstruct_app:konstruct_dev@postgres:5432/konstruct
- DATABASE_ADMIN_URL=postgresql+asyncpg://postgres:postgres_dev@postgres:5432/konstruct
- REDIS_URL=redis://redis:6379/0 - REDIS_URL=redis://redis:6379/0
- CELERY_BROKER_URL=redis://redis:6379/1 - CELERY_BROKER_URL=redis://redis:6379/1
- CELERY_RESULT_BACKEND=redis://redis:6379/2 - CELERY_RESULT_BACKEND=redis://redis:6379/2
@@ -184,13 +166,19 @@ services:
dockerfile_inline: | dockerfile_inline: |
FROM python:3.12-slim FROM python:3.12-slim
WORKDIR /app WORKDIR /app
RUN apt-get update -qq && apt-get install -y -qq git curl > /dev/null 2>&1
RUN pip install uv RUN pip install uv
COPY pyproject.toml ./ COPY pyproject.toml ./
COPY packages/shared ./packages/shared COPY packages/shared ./packages/shared
COPY packages/router ./packages/router
COPY packages/gateway ./packages/gateway
COPY packages/orchestrator ./packages/orchestrator COPY packages/orchestrator ./packages/orchestrator
RUN uv pip install --system -e packages/shared -e packages/orchestrator RUN uv pip install --system torch --index-url https://download.pytorch.org/whl/cpu
RUN uv pip install --system -e packages/shared -e packages/router -e packages/gateway -e packages/orchestrator
CMD ["celery", "-A", "orchestrator.main", "worker", "--loglevel=info"] CMD ["celery", "-A", "orchestrator.main", "worker", "--loglevel=info"]
container_name: konstruct-celery-worker container_name: konstruct-celery-worker
extra_hosts:
- "host.docker.internal:host-gateway"
networks: networks:
- konstruct-net - konstruct-net
depends_on: depends_on:
@@ -208,6 +196,6 @@ services:
- LLM_POOL_URL=http://llm-pool:8004 - LLM_POOL_URL=http://llm-pool:8004
- ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY:-} - ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY:-}
- OPENAI_API_KEY=${OPENAI_API_KEY:-} - OPENAI_API_KEY=${OPENAI_API_KEY:-}
- OLLAMA_BASE_URL=http://ollama:11434 - OLLAMA_BASE_URL=http://host.docker.internal:11434
- LOG_LEVEL=INFO - LOG_LEVEL=INFO
restart: unless-stopped restart: unless-stopped

View File

@@ -1,238 +0,0 @@
"""Phase 2: audit_events table (immutable) and kb_documents/kb_chunks tables
Revision ID: 003
Revises: 002
Create Date: 2026-03-23
This migration adds:
1. audit_events — append-only audit trail for all agent actions
- REVOKE UPDATE, DELETE from konstruct_app (immutability enforced at DB level)
- GRANT SELECT, INSERT only
- RLS for tenant isolation
- Composite index on (tenant_id, created_at DESC) for efficient queries
2. kb_documents and kb_chunks — knowledge base storage
- kb_chunks has a vector(384) embedding column
- HNSW index for approximate nearest neighbor cosine search
- Full CRUD grants for kb tables (mutable)
- RLS on both tables
Key design decision: audit_events immutability is enforced at the DB level via
REVOKE. Even if application code attempts an UPDATE or DELETE, PostgreSQL will
reject it with a permission error. This provides a hard compliance guarantee
that the audit trail cannot be tampered with via the application role.
"""
from __future__ import annotations
from typing import Sequence, Union
import sqlalchemy as sa
from alembic import op
from sqlalchemy.dialects.postgresql import JSONB, UUID
# revision identifiers, used by Alembic.
revision: str = "003"
down_revision: Union[str, None] = "002"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
# =========================================================================
# 1. audit_events — immutable audit trail
# =========================================================================
op.create_table(
"audit_events",
sa.Column(
"id",
UUID(as_uuid=True),
primary_key=True,
server_default=sa.text("gen_random_uuid()"),
),
sa.Column(
"tenant_id",
UUID(as_uuid=True),
nullable=False,
),
sa.Column(
"agent_id",
UUID(as_uuid=True),
nullable=True,
),
sa.Column(
"user_id",
sa.Text,
nullable=True,
),
sa.Column(
"action_type",
sa.Text,
nullable=False,
comment="llm_call | tool_invocation | escalation",
),
sa.Column(
"input_summary",
sa.Text,
nullable=True,
),
sa.Column(
"output_summary",
sa.Text,
nullable=True,
),
sa.Column(
"latency_ms",
sa.Integer,
nullable=True,
),
sa.Column(
"metadata",
JSONB,
nullable=False,
server_default=sa.text("'{}'::jsonb"),
),
sa.Column(
"created_at",
sa.DateTime(timezone=True),
nullable=False,
server_default=sa.text("NOW()"),
),
)
# Index for efficient per-tenant queries ordered by time (most recent first)
op.create_index(
"ix_audit_events_tenant_created",
"audit_events",
["tenant_id", "created_at"],
postgresql_ops={"created_at": "DESC"},
)
# Apply Row Level Security
op.execute("ALTER TABLE audit_events ENABLE ROW LEVEL SECURITY")
op.execute("ALTER TABLE audit_events FORCE ROW LEVEL SECURITY")
op.execute("""
CREATE POLICY tenant_isolation ON audit_events
USING (tenant_id = current_setting('app.current_tenant', TRUE)::uuid)
""")
# Grant SELECT + INSERT only — immutability enforced by revoking UPDATE/DELETE
op.execute("GRANT SELECT, INSERT ON audit_events TO konstruct_app")
op.execute("REVOKE UPDATE, DELETE ON audit_events FROM konstruct_app")
# =========================================================================
# 2. kb_documents — knowledge base document metadata
# =========================================================================
op.create_table(
"kb_documents",
sa.Column(
"id",
UUID(as_uuid=True),
primary_key=True,
server_default=sa.text("gen_random_uuid()"),
),
sa.Column(
"tenant_id",
UUID(as_uuid=True),
nullable=False,
),
sa.Column(
"agent_id",
UUID(as_uuid=True),
nullable=False,
),
sa.Column("filename", sa.Text, nullable=True),
sa.Column("source_url", sa.Text, nullable=True),
sa.Column("content_type", sa.Text, nullable=True),
sa.Column(
"created_at",
sa.DateTime(timezone=True),
nullable=False,
server_default=sa.text("NOW()"),
),
)
op.create_index("ix_kb_documents_tenant", "kb_documents", ["tenant_id"])
op.create_index("ix_kb_documents_agent", "kb_documents", ["agent_id"])
# Apply Row Level Security on kb_documents
op.execute("ALTER TABLE kb_documents ENABLE ROW LEVEL SECURITY")
op.execute("ALTER TABLE kb_documents FORCE ROW LEVEL SECURITY")
op.execute("""
CREATE POLICY tenant_isolation ON kb_documents
USING (tenant_id = current_setting('app.current_tenant', TRUE)::uuid)
""")
op.execute("GRANT SELECT, INSERT, UPDATE, DELETE ON kb_documents TO konstruct_app")
# =========================================================================
# 3. kb_chunks — chunked text with vector embeddings
# =========================================================================
op.create_table(
"kb_chunks",
sa.Column(
"id",
UUID(as_uuid=True),
primary_key=True,
server_default=sa.text("gen_random_uuid()"),
),
sa.Column(
"tenant_id",
UUID(as_uuid=True),
nullable=False,
),
sa.Column(
"document_id",
UUID(as_uuid=True),
sa.ForeignKey("kb_documents.id", ondelete="CASCADE"),
nullable=False,
),
sa.Column("content", sa.Text, nullable=False),
sa.Column("chunk_index", sa.Integer, nullable=True),
sa.Column(
"created_at",
sa.DateTime(timezone=True),
nullable=False,
server_default=sa.text("NOW()"),
),
# embedding column added via raw DDL below (pgvector type)
)
# Add embedding column as vector(384) — raw DDL required for pgvector type
op.execute("ALTER TABLE kb_chunks ADD COLUMN embedding vector(384) NOT NULL DEFAULT array_fill(0, ARRAY[384])::vector")
# Remove the default after adding — embeddings must be explicitly provided
op.execute("ALTER TABLE kb_chunks ALTER COLUMN embedding DROP DEFAULT")
op.create_index("ix_kb_chunks_tenant", "kb_chunks", ["tenant_id"])
op.create_index("ix_kb_chunks_document", "kb_chunks", ["document_id"])
# HNSW index for approximate nearest-neighbor cosine search
op.execute("""
CREATE INDEX ix_kb_chunks_hnsw
ON kb_chunks
USING hnsw (embedding vector_cosine_ops)
WITH (m = 16, ef_construction = 64)
""")
# Apply Row Level Security on kb_chunks
op.execute("ALTER TABLE kb_chunks ENABLE ROW LEVEL SECURITY")
op.execute("ALTER TABLE kb_chunks FORCE ROW LEVEL SECURITY")
op.execute("""
CREATE POLICY tenant_isolation ON kb_chunks
USING (tenant_id = current_setting('app.current_tenant', TRUE)::uuid)
""")
op.execute("GRANT SELECT, INSERT, UPDATE, DELETE ON kb_chunks TO konstruct_app")
def downgrade() -> None:
op.execute("REVOKE ALL ON kb_chunks FROM konstruct_app")
op.drop_table("kb_chunks")
op.execute("REVOKE ALL ON kb_documents FROM konstruct_app")
op.drop_table("kb_documents")
op.execute("REVOKE ALL ON audit_events FROM konstruct_app")
op.drop_table("audit_events")

View File

@@ -62,16 +62,38 @@ app = FastAPI(
version="0.1.0", version="0.1.0",
) )
# ---------------------------------------------------------------------------
# CORS — allow portal origin to call gateway API
# ---------------------------------------------------------------------------
from fastapi.middleware.cors import CORSMiddleware
app.add_middleware(
CORSMiddleware,
allow_origins=[
"http://localhost:3000",
"http://127.0.0.1:3000",
"http://100.64.0.10:3000",
],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# Slack bolt app — initialized at module import time. # Slack bolt app — initialized at module import time.
# signing_secret="" is safe for local dev/testing; set via env in production. # signing_secret="" is safe for local dev/testing; set via env in production.
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
slack_app = AsyncApp( slack_app: AsyncApp | None = None
token=settings.slack_bot_token or None, if settings.slack_bot_token and settings.slack_signing_secret:
signing_secret=settings.slack_signing_secret or None, slack_app = AsyncApp(
# In HTTP mode (Events API), token_verification_enabled must be True token=settings.slack_bot_token,
# slack-bolt validates signing_secret on every inbound request signing_secret=settings.slack_signing_secret,
) )
else:
import logging
logging.getLogger(__name__).warning(
"SLACK_BOT_TOKEN or SLACK_SIGNING_SECRET not set — Slack adapter disabled"
)
# Async Redis client — shared across all request handlers # Async Redis client — shared across all request handlers
_redis: Redis | None = None # type: ignore[type-arg] _redis: Redis | None = None # type: ignore[type-arg]
@@ -88,16 +110,14 @@ def _get_redis() -> Redis: # type: ignore[type-arg]
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# Register Slack event handlers # Register Slack event handlers
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
register_slack_handlers( slack_handler: AsyncSlackRequestHandler | None = None
if slack_app is not None:
register_slack_handlers(
slack_app=slack_app, slack_app=slack_app,
redis=_get_redis(), redis=_get_redis(),
get_session=async_session_factory, get_session=async_session_factory,
) )
slack_handler = AsyncSlackRequestHandler(slack_app)
# ---------------------------------------------------------------------------
# Slack request handler — adapts slack-bolt AsyncApp to FastAPI
# ---------------------------------------------------------------------------
slack_handler = AsyncSlackRequestHandler(slack_app)
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# Register channel routers # Register channel routers
@@ -133,6 +153,8 @@ async def slack_events(request: Request) -> Response:
CRITICAL: This endpoint MUST return HTTP 200 within 3 seconds. CRITICAL: This endpoint MUST return HTTP 200 within 3 seconds.
All LLM/heavy work is dispatched to Celery inside the event handlers. All LLM/heavy work is dispatched to Celery inside the event handlers.
""" """
if slack_handler is None:
return Response(content='{"error":"Slack not configured"}', status_code=503, media_type="application/json")
return await slack_handler.handle(request) return await slack_handler.handle(request)

View File

@@ -13,6 +13,7 @@ dependencies = [
"konstruct-orchestrator", "konstruct-orchestrator",
"fastapi[standard]>=0.115.0", "fastapi[standard]>=0.115.0",
"slack-bolt>=1.22.0", "slack-bolt>=1.22.0",
"aiohttp>=3.9.0",
"python-telegram-bot>=21.0", "python-telegram-bot>=21.0",
"httpx>=0.28.0", "httpx>=0.28.0",
"redis>=5.0.0", "redis>=5.0.0",

View File

@@ -9,13 +9,15 @@ description = "LLM Backend Pool — LiteLLM router for Ollama, vLLM, OpenAI, Ant
requires-python = ">=3.12" requires-python = ">=3.12"
dependencies = [ dependencies = [
"konstruct-shared", "konstruct-shared",
# Pinned: do NOT upgrade past 1.82.5 — a September 2025 OOM regression exists # litellm removed from PyPI — installing from GitHub
# in later releases. Verify fix before bumping. "litellm @ git+https://github.com/BerriAI/litellm.git",
"litellm==1.82.5",
"fastapi[standard]>=0.115.0", "fastapi[standard]>=0.115.0",
"httpx>=0.28.0", "httpx>=0.28.0",
] ]
[tool.hatch.metadata]
allow-direct-references = true
[tool.uv.sources] [tool.uv.sources]
konstruct-shared = { workspace = true } konstruct-shared = { workspace = true }

136
uv.lock generated
View File

@@ -366,6 +366,63 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/9a/3c/c17fb3ca2d9c3acff52e30b309f538586f9f5b9c9cf454f3845fc9af4881/certifi-2026.2.25-py3-none-any.whl", hash = "sha256:027692e4402ad994f1c42e52a4997a9763c646b73e4096e4d5d6db8af1d6f0fa", size = 153684, upload-time = "2026-02-25T02:54:15.766Z" }, { url = "https://files.pythonhosted.org/packages/9a/3c/c17fb3ca2d9c3acff52e30b309f538586f9f5b9c9cf454f3845fc9af4881/certifi-2026.2.25-py3-none-any.whl", hash = "sha256:027692e4402ad994f1c42e52a4997a9763c646b73e4096e4d5d6db8af1d6f0fa", size = 153684, upload-time = "2026-02-25T02:54:15.766Z" },
] ]
[[package]]
name = "cffi"
version = "2.0.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "pycparser", marker = "implementation_name != 'PyPy'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588, upload-time = "2025-09-08T23:24:04.541Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/ea/47/4f61023ea636104d4f16ab488e268b93008c3d0bb76893b1b31db1f96802/cffi-2.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d", size = 185271, upload-time = "2025-09-08T23:22:44.795Z" },
{ url = "https://files.pythonhosted.org/packages/df/a2/781b623f57358e360d62cdd7a8c681f074a71d445418a776eef0aadb4ab4/cffi-2.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c", size = 181048, upload-time = "2025-09-08T23:22:45.938Z" },
{ url = "https://files.pythonhosted.org/packages/ff/df/a4f0fbd47331ceeba3d37c2e51e9dfc9722498becbeec2bd8bc856c9538a/cffi-2.0.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe", size = 212529, upload-time = "2025-09-08T23:22:47.349Z" },
{ url = "https://files.pythonhosted.org/packages/d5/72/12b5f8d3865bf0f87cf1404d8c374e7487dcf097a1c91c436e72e6badd83/cffi-2.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062", size = 220097, upload-time = "2025-09-08T23:22:48.677Z" },
{ url = "https://files.pythonhosted.org/packages/c2/95/7a135d52a50dfa7c882ab0ac17e8dc11cec9d55d2c18dda414c051c5e69e/cffi-2.0.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e", size = 207983, upload-time = "2025-09-08T23:22:50.06Z" },
{ url = "https://files.pythonhosted.org/packages/3a/c8/15cb9ada8895957ea171c62dc78ff3e99159ee7adb13c0123c001a2546c1/cffi-2.0.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037", size = 206519, upload-time = "2025-09-08T23:22:51.364Z" },
{ url = "https://files.pythonhosted.org/packages/78/2d/7fa73dfa841b5ac06c7b8855cfc18622132e365f5b81d02230333ff26e9e/cffi-2.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba", size = 219572, upload-time = "2025-09-08T23:22:52.902Z" },
{ url = "https://files.pythonhosted.org/packages/07/e0/267e57e387b4ca276b90f0434ff88b2c2241ad72b16d31836adddfd6031b/cffi-2.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94", size = 222963, upload-time = "2025-09-08T23:22:54.518Z" },
{ url = "https://files.pythonhosted.org/packages/b6/75/1f2747525e06f53efbd878f4d03bac5b859cbc11c633d0fb81432d98a795/cffi-2.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187", size = 221361, upload-time = "2025-09-08T23:22:55.867Z" },
{ url = "https://files.pythonhosted.org/packages/7b/2b/2b6435f76bfeb6bbf055596976da087377ede68df465419d192acf00c437/cffi-2.0.0-cp312-cp312-win32.whl", hash = "sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18", size = 172932, upload-time = "2025-09-08T23:22:57.188Z" },
{ url = "https://files.pythonhosted.org/packages/f8/ed/13bd4418627013bec4ed6e54283b1959cf6db888048c7cf4b4c3b5b36002/cffi-2.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5", size = 183557, upload-time = "2025-09-08T23:22:58.351Z" },
{ url = "https://files.pythonhosted.org/packages/95/31/9f7f93ad2f8eff1dbc1c3656d7ca5bfd8fb52c9d786b4dcf19b2d02217fa/cffi-2.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6", size = 177762, upload-time = "2025-09-08T23:22:59.668Z" },
{ url = "https://files.pythonhosted.org/packages/4b/8d/a0a47a0c9e413a658623d014e91e74a50cdd2c423f7ccfd44086ef767f90/cffi-2.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb", size = 185230, upload-time = "2025-09-08T23:23:00.879Z" },
{ url = "https://files.pythonhosted.org/packages/4a/d2/a6c0296814556c68ee32009d9c2ad4f85f2707cdecfd7727951ec228005d/cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca", size = 181043, upload-time = "2025-09-08T23:23:02.231Z" },
{ url = "https://files.pythonhosted.org/packages/b0/1e/d22cc63332bd59b06481ceaac49d6c507598642e2230f201649058a7e704/cffi-2.0.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b", size = 212446, upload-time = "2025-09-08T23:23:03.472Z" },
{ url = "https://files.pythonhosted.org/packages/a9/f5/a2c23eb03b61a0b8747f211eb716446c826ad66818ddc7810cc2cc19b3f2/cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b", size = 220101, upload-time = "2025-09-08T23:23:04.792Z" },
{ url = "https://files.pythonhosted.org/packages/f2/7f/e6647792fc5850d634695bc0e6ab4111ae88e89981d35ac269956605feba/cffi-2.0.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2", size = 207948, upload-time = "2025-09-08T23:23:06.127Z" },
{ url = "https://files.pythonhosted.org/packages/cb/1e/a5a1bd6f1fb30f22573f76533de12a00bf274abcdc55c8edab639078abb6/cffi-2.0.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3", size = 206422, upload-time = "2025-09-08T23:23:07.753Z" },
{ url = "https://files.pythonhosted.org/packages/98/df/0a1755e750013a2081e863e7cd37e0cdd02664372c754e5560099eb7aa44/cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26", size = 219499, upload-time = "2025-09-08T23:23:09.648Z" },
{ url = "https://files.pythonhosted.org/packages/50/e1/a969e687fcf9ea58e6e2a928ad5e2dd88cc12f6f0ab477e9971f2309b57c/cffi-2.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c", size = 222928, upload-time = "2025-09-08T23:23:10.928Z" },
{ url = "https://files.pythonhosted.org/packages/36/54/0362578dd2c9e557a28ac77698ed67323ed5b9775ca9d3fe73fe191bb5d8/cffi-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b", size = 221302, upload-time = "2025-09-08T23:23:12.42Z" },
{ url = "https://files.pythonhosted.org/packages/eb/6d/bf9bda840d5f1dfdbf0feca87fbdb64a918a69bca42cfa0ba7b137c48cb8/cffi-2.0.0-cp313-cp313-win32.whl", hash = "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27", size = 172909, upload-time = "2025-09-08T23:23:14.32Z" },
{ url = "https://files.pythonhosted.org/packages/37/18/6519e1ee6f5a1e579e04b9ddb6f1676c17368a7aba48299c3759bbc3c8b3/cffi-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75", size = 183402, upload-time = "2025-09-08T23:23:15.535Z" },
{ url = "https://files.pythonhosted.org/packages/cb/0e/02ceeec9a7d6ee63bb596121c2c8e9b3a9e150936f4fbef6ca1943e6137c/cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91", size = 177780, upload-time = "2025-09-08T23:23:16.761Z" },
{ url = "https://files.pythonhosted.org/packages/92/c4/3ce07396253a83250ee98564f8d7e9789fab8e58858f35d07a9a2c78de9f/cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5", size = 185320, upload-time = "2025-09-08T23:23:18.087Z" },
{ url = "https://files.pythonhosted.org/packages/59/dd/27e9fa567a23931c838c6b02d0764611c62290062a6d4e8ff7863daf9730/cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13", size = 181487, upload-time = "2025-09-08T23:23:19.622Z" },
{ url = "https://files.pythonhosted.org/packages/d6/43/0e822876f87ea8a4ef95442c3d766a06a51fc5298823f884ef87aaad168c/cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b", size = 220049, upload-time = "2025-09-08T23:23:20.853Z" },
{ url = "https://files.pythonhosted.org/packages/b4/89/76799151d9c2d2d1ead63c2429da9ea9d7aac304603de0c6e8764e6e8e70/cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c", size = 207793, upload-time = "2025-09-08T23:23:22.08Z" },
{ url = "https://files.pythonhosted.org/packages/bb/dd/3465b14bb9e24ee24cb88c9e3730f6de63111fffe513492bf8c808a3547e/cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef", size = 206300, upload-time = "2025-09-08T23:23:23.314Z" },
{ url = "https://files.pythonhosted.org/packages/47/d9/d83e293854571c877a92da46fdec39158f8d7e68da75bf73581225d28e90/cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775", size = 219244, upload-time = "2025-09-08T23:23:24.541Z" },
{ url = "https://files.pythonhosted.org/packages/2b/0f/1f177e3683aead2bb00f7679a16451d302c436b5cbf2505f0ea8146ef59e/cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205", size = 222828, upload-time = "2025-09-08T23:23:26.143Z" },
{ url = "https://files.pythonhosted.org/packages/c6/0f/cafacebd4b040e3119dcb32fed8bdef8dfe94da653155f9d0b9dc660166e/cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1", size = 220926, upload-time = "2025-09-08T23:23:27.873Z" },
{ url = "https://files.pythonhosted.org/packages/3e/aa/df335faa45b395396fcbc03de2dfcab242cd61a9900e914fe682a59170b1/cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f", size = 175328, upload-time = "2025-09-08T23:23:44.61Z" },
{ url = "https://files.pythonhosted.org/packages/bb/92/882c2d30831744296ce713f0feb4c1cd30f346ef747b530b5318715cc367/cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25", size = 185650, upload-time = "2025-09-08T23:23:45.848Z" },
{ url = "https://files.pythonhosted.org/packages/9f/2c/98ece204b9d35a7366b5b2c6539c350313ca13932143e79dc133ba757104/cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad", size = 180687, upload-time = "2025-09-08T23:23:47.105Z" },
{ url = "https://files.pythonhosted.org/packages/3e/61/c768e4d548bfa607abcda77423448df8c471f25dbe64fb2ef6d555eae006/cffi-2.0.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9", size = 188773, upload-time = "2025-09-08T23:23:29.347Z" },
{ url = "https://files.pythonhosted.org/packages/2c/ea/5f76bce7cf6fcd0ab1a1058b5af899bfbef198bea4d5686da88471ea0336/cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d", size = 185013, upload-time = "2025-09-08T23:23:30.63Z" },
{ url = "https://files.pythonhosted.org/packages/be/b4/c56878d0d1755cf9caa54ba71e5d049479c52f9e4afc230f06822162ab2f/cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c", size = 221593, upload-time = "2025-09-08T23:23:31.91Z" },
{ url = "https://files.pythonhosted.org/packages/e0/0d/eb704606dfe8033e7128df5e90fee946bbcb64a04fcdaa97321309004000/cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8", size = 209354, upload-time = "2025-09-08T23:23:33.214Z" },
{ url = "https://files.pythonhosted.org/packages/d8/19/3c435d727b368ca475fb8742ab97c9cb13a0de600ce86f62eab7fa3eea60/cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc", size = 208480, upload-time = "2025-09-08T23:23:34.495Z" },
{ url = "https://files.pythonhosted.org/packages/d0/44/681604464ed9541673e486521497406fadcc15b5217c3e326b061696899a/cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592", size = 221584, upload-time = "2025-09-08T23:23:36.096Z" },
{ url = "https://files.pythonhosted.org/packages/25/8e/342a504ff018a2825d395d44d63a767dd8ebc927ebda557fecdaca3ac33a/cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512", size = 224443, upload-time = "2025-09-08T23:23:37.328Z" },
{ url = "https://files.pythonhosted.org/packages/e1/5e/b666bacbbc60fbf415ba9988324a132c9a7a0448a9a8f125074671c0f2c3/cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4", size = 223437, upload-time = "2025-09-08T23:23:38.945Z" },
{ url = "https://files.pythonhosted.org/packages/a0/1d/ec1a60bd1a10daa292d3cd6bb0b359a81607154fb8165f3ec95fe003b85c/cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e", size = 180487, upload-time = "2025-09-08T23:23:40.423Z" },
{ url = "https://files.pythonhosted.org/packages/bf/41/4c1168c74fac325c0c8156f04b6749c8b6a8f405bbf91413ba088359f60d/cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6", size = 191726, upload-time = "2025-09-08T23:23:41.742Z" },
{ url = "https://files.pythonhosted.org/packages/ae/3a/dbeec9d1ee0844c679f6bb5d6ad4e9f198b1224f4e7a32825f47f6192b0c/cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9", size = 184195, upload-time = "2025-09-08T23:23:43.004Z" },
]
[[package]] [[package]]
name = "charset-normalizer" name = "charset-normalizer"
version = "3.4.6" version = "3.4.6"
@@ -497,6 +554,59 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
] ]
[[package]]
name = "cryptography"
version = "46.0.5"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "cffi", marker = "platform_python_implementation != 'PyPy'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/60/04/ee2a9e8542e4fa2773b81771ff8349ff19cdd56b7258a0cc442639052edb/cryptography-46.0.5.tar.gz", hash = "sha256:abace499247268e3757271b2f1e244b36b06f8515cf27c4d49468fc9eb16e93d", size = 750064, upload-time = "2026-02-10T19:18:38.255Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/f7/81/b0bb27f2ba931a65409c6b8a8b358a7f03c0e46eceacddff55f7c84b1f3b/cryptography-46.0.5-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:351695ada9ea9618b3500b490ad54c739860883df6c1f555e088eaf25b1bbaad", size = 7176289, upload-time = "2026-02-10T19:17:08.274Z" },
{ url = "https://files.pythonhosted.org/packages/ff/9e/6b4397a3e3d15123de3b1806ef342522393d50736c13b20ec4c9ea6693a6/cryptography-46.0.5-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c18ff11e86df2e28854939acde2d003f7984f721eba450b56a200ad90eeb0e6b", size = 4275637, upload-time = "2026-02-10T19:17:10.53Z" },
{ url = "https://files.pythonhosted.org/packages/63/e7/471ab61099a3920b0c77852ea3f0ea611c9702f651600397ac567848b897/cryptography-46.0.5-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4d7e3d356b8cd4ea5aff04f129d5f66ebdc7b6f8eae802b93739ed520c47c79b", size = 4424742, upload-time = "2026-02-10T19:17:12.388Z" },
{ url = "https://files.pythonhosted.org/packages/37/53/a18500f270342d66bf7e4d9f091114e31e5ee9e7375a5aba2e85a91e0044/cryptography-46.0.5-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:50bfb6925eff619c9c023b967d5b77a54e04256c4281b0e21336a130cd7fc263", size = 4277528, upload-time = "2026-02-10T19:17:13.853Z" },
{ url = "https://files.pythonhosted.org/packages/22/29/c2e812ebc38c57b40e7c583895e73c8c5adb4d1e4a0cc4c5a4fdab2b1acc/cryptography-46.0.5-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:803812e111e75d1aa73690d2facc295eaefd4439be1023fefc4995eaea2af90d", size = 4947993, upload-time = "2026-02-10T19:17:15.618Z" },
{ url = "https://files.pythonhosted.org/packages/6b/e7/237155ae19a9023de7e30ec64e5d99a9431a567407ac21170a046d22a5a3/cryptography-46.0.5-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:3ee190460e2fbe447175cda91b88b84ae8322a104fc27766ad09428754a618ed", size = 4456855, upload-time = "2026-02-10T19:17:17.221Z" },
{ url = "https://files.pythonhosted.org/packages/2d/87/fc628a7ad85b81206738abbd213b07702bcbdada1dd43f72236ef3cffbb5/cryptography-46.0.5-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:f145bba11b878005c496e93e257c1e88f154d278d2638e6450d17e0f31e558d2", size = 3984635, upload-time = "2026-02-10T19:17:18.792Z" },
{ url = "https://files.pythonhosted.org/packages/84/29/65b55622bde135aedf4565dc509d99b560ee4095e56989e815f8fd2aa910/cryptography-46.0.5-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:e9251e3be159d1020c4030bd2e5f84d6a43fe54b6c19c12f51cde9542a2817b2", size = 4277038, upload-time = "2026-02-10T19:17:20.256Z" },
{ url = "https://files.pythonhosted.org/packages/bc/36/45e76c68d7311432741faf1fbf7fac8a196a0a735ca21f504c75d37e2558/cryptography-46.0.5-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:47fb8a66058b80e509c47118ef8a75d14c455e81ac369050f20ba0d23e77fee0", size = 4912181, upload-time = "2026-02-10T19:17:21.825Z" },
{ url = "https://files.pythonhosted.org/packages/6d/1a/c1ba8fead184d6e3d5afcf03d569acac5ad063f3ac9fb7258af158f7e378/cryptography-46.0.5-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:4c3341037c136030cb46e4b1e17b7418ea4cbd9dd207e4a6f3b2b24e0d4ac731", size = 4456482, upload-time = "2026-02-10T19:17:25.133Z" },
{ url = "https://files.pythonhosted.org/packages/f9/e5/3fb22e37f66827ced3b902cf895e6a6bc1d095b5b26be26bd13c441fdf19/cryptography-46.0.5-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:890bcb4abd5a2d3f852196437129eb3667d62630333aacc13dfd470fad3aaa82", size = 4405497, upload-time = "2026-02-10T19:17:26.66Z" },
{ url = "https://files.pythonhosted.org/packages/1a/df/9d58bb32b1121a8a2f27383fabae4d63080c7ca60b9b5c88be742be04ee7/cryptography-46.0.5-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:80a8d7bfdf38f87ca30a5391c0c9ce4ed2926918e017c29ddf643d0ed2778ea1", size = 4667819, upload-time = "2026-02-10T19:17:28.569Z" },
{ url = "https://files.pythonhosted.org/packages/ea/ed/325d2a490c5e94038cdb0117da9397ece1f11201f425c4e9c57fe5b9f08b/cryptography-46.0.5-cp311-abi3-win32.whl", hash = "sha256:60ee7e19e95104d4c03871d7d7dfb3d22ef8a9b9c6778c94e1c8fcc8365afd48", size = 3028230, upload-time = "2026-02-10T19:17:30.518Z" },
{ url = "https://files.pythonhosted.org/packages/e9/5a/ac0f49e48063ab4255d9e3b79f5def51697fce1a95ea1370f03dc9db76f6/cryptography-46.0.5-cp311-abi3-win_amd64.whl", hash = "sha256:38946c54b16c885c72c4f59846be9743d699eee2b69b6988e0a00a01f46a61a4", size = 3480909, upload-time = "2026-02-10T19:17:32.083Z" },
{ url = "https://files.pythonhosted.org/packages/00/13/3d278bfa7a15a96b9dc22db5a12ad1e48a9eb3d40e1827ef66a5df75d0d0/cryptography-46.0.5-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:94a76daa32eb78d61339aff7952ea819b1734b46f73646a07decb40e5b3448e2", size = 7119287, upload-time = "2026-02-10T19:17:33.801Z" },
{ url = "https://files.pythonhosted.org/packages/67/c8/581a6702e14f0898a0848105cbefd20c058099e2c2d22ef4e476dfec75d7/cryptography-46.0.5-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5be7bf2fb40769e05739dd0046e7b26f9d4670badc7b032d6ce4db64dddc0678", size = 4265728, upload-time = "2026-02-10T19:17:35.569Z" },
{ url = "https://files.pythonhosted.org/packages/dd/4a/ba1a65ce8fc65435e5a849558379896c957870dd64fecea97b1ad5f46a37/cryptography-46.0.5-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fe346b143ff9685e40192a4960938545c699054ba11d4f9029f94751e3f71d87", size = 4408287, upload-time = "2026-02-10T19:17:36.938Z" },
{ url = "https://files.pythonhosted.org/packages/f8/67/8ffdbf7b65ed1ac224d1c2df3943553766914a8ca718747ee3871da6107e/cryptography-46.0.5-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:c69fd885df7d089548a42d5ec05be26050ebcd2283d89b3d30676eb32ff87dee", size = 4270291, upload-time = "2026-02-10T19:17:38.748Z" },
{ url = "https://files.pythonhosted.org/packages/f8/e5/f52377ee93bc2f2bba55a41a886fd208c15276ffbd2569f2ddc89d50e2c5/cryptography-46.0.5-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:8293f3dea7fc929ef7240796ba231413afa7b68ce38fd21da2995549f5961981", size = 4927539, upload-time = "2026-02-10T19:17:40.241Z" },
{ url = "https://files.pythonhosted.org/packages/3b/02/cfe39181b02419bbbbcf3abdd16c1c5c8541f03ca8bda240debc467d5a12/cryptography-46.0.5-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:1abfdb89b41c3be0365328a410baa9df3ff8a9110fb75e7b52e66803ddabc9a9", size = 4442199, upload-time = "2026-02-10T19:17:41.789Z" },
{ url = "https://files.pythonhosted.org/packages/c0/96/2fcaeb4873e536cf71421a388a6c11b5bc846e986b2b069c79363dc1648e/cryptography-46.0.5-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:d66e421495fdb797610a08f43b05269e0a5ea7f5e652a89bfd5a7d3c1dee3648", size = 3960131, upload-time = "2026-02-10T19:17:43.379Z" },
{ url = "https://files.pythonhosted.org/packages/d8/d2/b27631f401ddd644e94c5cf33c9a4069f72011821cf3dc7309546b0642a0/cryptography-46.0.5-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:4e817a8920bfbcff8940ecfd60f23d01836408242b30f1a708d93198393a80b4", size = 4270072, upload-time = "2026-02-10T19:17:45.481Z" },
{ url = "https://files.pythonhosted.org/packages/f4/a7/60d32b0370dae0b4ebe55ffa10e8599a2a59935b5ece1b9f06edb73abdeb/cryptography-46.0.5-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:68f68d13f2e1cb95163fa3b4db4bf9a159a418f5f6e7242564fc75fcae667fd0", size = 4892170, upload-time = "2026-02-10T19:17:46.997Z" },
{ url = "https://files.pythonhosted.org/packages/d2/b9/cf73ddf8ef1164330eb0b199a589103c363afa0cf794218c24d524a58eab/cryptography-46.0.5-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:a3d1fae9863299076f05cb8a778c467578262fae09f9dc0ee9b12eb4268ce663", size = 4441741, upload-time = "2026-02-10T19:17:48.661Z" },
{ url = "https://files.pythonhosted.org/packages/5f/eb/eee00b28c84c726fe8fa0158c65afe312d9c3b78d9d01daf700f1f6e37ff/cryptography-46.0.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c4143987a42a2397f2fc3b4d7e3a7d313fbe684f67ff443999e803dd75a76826", size = 4396728, upload-time = "2026-02-10T19:17:50.058Z" },
{ url = "https://files.pythonhosted.org/packages/65/f4/6bc1a9ed5aef7145045114b75b77c2a8261b4d38717bd8dea111a63c3442/cryptography-46.0.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:7d731d4b107030987fd61a7f8ab512b25b53cef8f233a97379ede116f30eb67d", size = 4652001, upload-time = "2026-02-10T19:17:51.54Z" },
{ url = "https://files.pythonhosted.org/packages/86/ef/5d00ef966ddd71ac2e6951d278884a84a40ffbd88948ef0e294b214ae9e4/cryptography-46.0.5-cp314-cp314t-win32.whl", hash = "sha256:c3bcce8521d785d510b2aad26ae2c966092b7daa8f45dd8f44734a104dc0bc1a", size = 3003637, upload-time = "2026-02-10T19:17:52.997Z" },
{ url = "https://files.pythonhosted.org/packages/b7/57/f3f4160123da6d098db78350fdfd9705057aad21de7388eacb2401dceab9/cryptography-46.0.5-cp314-cp314t-win_amd64.whl", hash = "sha256:4d8ae8659ab18c65ced284993c2265910f6c9e650189d4e3f68445ef82a810e4", size = 3469487, upload-time = "2026-02-10T19:17:54.549Z" },
{ url = "https://files.pythonhosted.org/packages/e2/fa/a66aa722105ad6a458bebd64086ca2b72cdd361fed31763d20390f6f1389/cryptography-46.0.5-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:4108d4c09fbbf2789d0c926eb4152ae1760d5a2d97612b92d508d96c861e4d31", size = 7170514, upload-time = "2026-02-10T19:17:56.267Z" },
{ url = "https://files.pythonhosted.org/packages/0f/04/c85bdeab78c8bc77b701bf0d9bdcf514c044e18a46dcff330df5448631b0/cryptography-46.0.5-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7d1f30a86d2757199cb2d56e48cce14deddf1f9c95f1ef1b64ee91ea43fe2e18", size = 4275349, upload-time = "2026-02-10T19:17:58.419Z" },
{ url = "https://files.pythonhosted.org/packages/5c/32/9b87132a2f91ee7f5223b091dc963055503e9b442c98fc0b8a5ca765fab0/cryptography-46.0.5-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:039917b0dc418bb9f6edce8a906572d69e74bd330b0b3fea4f79dab7f8ddd235", size = 4420667, upload-time = "2026-02-10T19:18:00.619Z" },
{ url = "https://files.pythonhosted.org/packages/a1/a6/a7cb7010bec4b7c5692ca6f024150371b295ee1c108bdc1c400e4c44562b/cryptography-46.0.5-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:ba2a27ff02f48193fc4daeadf8ad2590516fa3d0adeeb34336b96f7fa64c1e3a", size = 4276980, upload-time = "2026-02-10T19:18:02.379Z" },
{ url = "https://files.pythonhosted.org/packages/8e/7c/c4f45e0eeff9b91e3f12dbd0e165fcf2a38847288fcfd889deea99fb7b6d/cryptography-46.0.5-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:61aa400dce22cb001a98014f647dc21cda08f7915ceb95df0c9eaf84b4b6af76", size = 4939143, upload-time = "2026-02-10T19:18:03.964Z" },
{ url = "https://files.pythonhosted.org/packages/37/19/e1b8f964a834eddb44fa1b9a9976f4e414cbb7aa62809b6760c8803d22d1/cryptography-46.0.5-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:3ce58ba46e1bc2aac4f7d9290223cead56743fa6ab94a5d53292ffaac6a91614", size = 4453674, upload-time = "2026-02-10T19:18:05.588Z" },
{ url = "https://files.pythonhosted.org/packages/db/ed/db15d3956f65264ca204625597c410d420e26530c4e2943e05a0d2f24d51/cryptography-46.0.5-cp38-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:420d0e909050490d04359e7fdb5ed7e667ca5c3c402b809ae2563d7e66a92229", size = 3978801, upload-time = "2026-02-10T19:18:07.167Z" },
{ url = "https://files.pythonhosted.org/packages/41/e2/df40a31d82df0a70a0daf69791f91dbb70e47644c58581d654879b382d11/cryptography-46.0.5-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:582f5fcd2afa31622f317f80426a027f30dc792e9c80ffee87b993200ea115f1", size = 4276755, upload-time = "2026-02-10T19:18:09.813Z" },
{ url = "https://files.pythonhosted.org/packages/33/45/726809d1176959f4a896b86907b98ff4391a8aa29c0aaaf9450a8a10630e/cryptography-46.0.5-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:bfd56bb4b37ed4f330b82402f6f435845a5f5648edf1ad497da51a8452d5d62d", size = 4901539, upload-time = "2026-02-10T19:18:11.263Z" },
{ url = "https://files.pythonhosted.org/packages/99/0f/a3076874e9c88ecb2ecc31382f6e7c21b428ede6f55aafa1aa272613e3cd/cryptography-46.0.5-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:a3d507bb6a513ca96ba84443226af944b0f7f47dcc9a399d110cd6146481d24c", size = 4452794, upload-time = "2026-02-10T19:18:12.914Z" },
{ url = "https://files.pythonhosted.org/packages/02/ef/ffeb542d3683d24194a38f66ca17c0a4b8bf10631feef44a7ef64e631b1a/cryptography-46.0.5-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:9f16fbdf4da055efb21c22d81b89f155f02ba420558db21288b3d0035bafd5f4", size = 4404160, upload-time = "2026-02-10T19:18:14.375Z" },
{ url = "https://files.pythonhosted.org/packages/96/93/682d2b43c1d5f1406ed048f377c0fc9fc8f7b0447a478d5c65ab3d3a66eb/cryptography-46.0.5-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:ced80795227d70549a411a4ab66e8ce307899fad2220ce5ab2f296e687eacde9", size = 4667123, upload-time = "2026-02-10T19:18:15.886Z" },
{ url = "https://files.pythonhosted.org/packages/45/2d/9c5f2926cb5300a8eefc3f4f0b3f3df39db7f7ce40c8365444c49363cbda/cryptography-46.0.5-cp38-abi3-win32.whl", hash = "sha256:02f547fce831f5096c9a567fd41bc12ca8f11df260959ecc7c3202555cc47a72", size = 3010220, upload-time = "2026-02-10T19:18:17.361Z" },
{ url = "https://files.pythonhosted.org/packages/48/ef/0c2f4a8e31018a986949d34a01115dd057bf536905dca38897bacd21fac3/cryptography-46.0.5-cp38-abi3-win_amd64.whl", hash = "sha256:556e106ee01aa13484ce9b0239bca667be5004efb0aabbed28d353df86445595", size = 3467050, upload-time = "2026-02-10T19:18:18.899Z" },
]
[[package]] [[package]]
name = "cuda-bindings" name = "cuda-bindings"
version = "13.2.0" version = "13.2.0"
@@ -1377,6 +1487,7 @@ dependencies = [
{ name = "asyncpg" }, { name = "asyncpg" },
{ name = "bcrypt" }, { name = "bcrypt" },
{ name = "celery", extra = ["redis"] }, { name = "celery", extra = ["redis"] },
{ name = "cryptography" },
{ name = "fastapi", extra = ["standard"] }, { name = "fastapi", extra = ["standard"] },
{ name = "httpx" }, { name = "httpx" },
{ name = "pgvector" }, { name = "pgvector" },
@@ -1385,6 +1496,7 @@ dependencies = [
{ name = "redis" }, { name = "redis" },
{ name = "slowapi" }, { name = "slowapi" },
{ name = "sqlalchemy", extra = ["asyncio"] }, { name = "sqlalchemy", extra = ["asyncio"] },
{ name = "stripe" },
] ]
[package.metadata] [package.metadata]
@@ -1393,6 +1505,7 @@ requires-dist = [
{ name = "asyncpg", specifier = ">=0.31.0" }, { name = "asyncpg", specifier = ">=0.31.0" },
{ name = "bcrypt", specifier = ">=4.0.0" }, { name = "bcrypt", specifier = ">=4.0.0" },
{ name = "celery", extras = ["redis"], specifier = ">=5.4.0" }, { name = "celery", extras = ["redis"], specifier = ">=5.4.0" },
{ name = "cryptography", specifier = ">=42.0.0" },
{ name = "fastapi", extras = ["standard"], specifier = ">=0.115.0" }, { name = "fastapi", extras = ["standard"], specifier = ">=0.115.0" },
{ name = "httpx", specifier = ">=0.28.0" }, { name = "httpx", specifier = ">=0.28.0" },
{ name = "pgvector", specifier = ">=0.3.0" }, { name = "pgvector", specifier = ">=0.3.0" },
@@ -1401,6 +1514,7 @@ requires-dist = [
{ name = "redis", specifier = ">=5.2.0" }, { name = "redis", specifier = ">=5.2.0" },
{ name = "slowapi", specifier = ">=0.1.9" }, { name = "slowapi", specifier = ">=0.1.9" },
{ name = "sqlalchemy", extras = ["asyncio"], specifier = ">=2.0.36" }, { name = "sqlalchemy", extras = ["asyncio"], specifier = ">=2.0.36" },
{ name = "stripe", specifier = ">=10.0.0" },
] ]
[[package]] [[package]]
@@ -2119,6 +2233,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/5b/5a/bc7b4a4ef808fa59a816c17b20c4bef6884daebbdf627ff2a161da67da19/propcache-0.4.1-py3-none-any.whl", hash = "sha256:af2a6052aeb6cf17d3e46ee169099044fd8224cbaf75c76a2ef596e8163e2237", size = 13305, upload-time = "2025-10-08T19:49:00.792Z" }, { url = "https://files.pythonhosted.org/packages/5b/5a/bc7b4a4ef808fa59a816c17b20c4bef6884daebbdf627ff2a161da67da19/propcache-0.4.1-py3-none-any.whl", hash = "sha256:af2a6052aeb6cf17d3e46ee169099044fd8224cbaf75c76a2ef596e8163e2237", size = 13305, upload-time = "2025-10-08T19:49:00.792Z" },
] ]
[[package]]
name = "pycparser"
version = "3.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/1b/7d/92392ff7815c21062bea51aa7b87d45576f649f16458d78b7cf94b9ab2e6/pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29", size = 103492, upload-time = "2026-01-21T14:26:51.89Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992", size = 48172, upload-time = "2026-01-21T14:26:50.693Z" },
]
[[package]] [[package]]
name = "pydantic" name = "pydantic"
version = "2.12.5" version = "2.12.5"
@@ -3017,6 +3140,19 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/0b/c9/584bc9651441b4ba60cc4d557d8a547b5aff901af35bda3a4ee30c819b82/starlette-1.0.0-py3-none-any.whl", hash = "sha256:d3ec55e0bb321692d275455ddfd3df75fff145d009685eb40dc91fc66b03d38b", size = 72651, upload-time = "2026-03-22T18:29:45.111Z" }, { url = "https://files.pythonhosted.org/packages/0b/c9/584bc9651441b4ba60cc4d557d8a547b5aff901af35bda3a4ee30c819b82/starlette-1.0.0-py3-none-any.whl", hash = "sha256:d3ec55e0bb321692d275455ddfd3df75fff145d009685eb40dc91fc66b03d38b", size = 72651, upload-time = "2026-03-22T18:29:45.111Z" },
] ]
[[package]]
name = "stripe"
version = "14.4.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "requests" },
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/63/01/59d92650146c6340e333c22b8a238561b022e8641ba255298dfe46b4137d/stripe-14.4.1.tar.gz", hash = "sha256:e3eecfb336819326cd2ab9a93f98eb01b49c4d4d93c9d167a52a68ab99c19a88", size = 1473321, upload-time = "2026-03-06T22:59:45.822Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/35/0f/547eab75052384a72771b396a484556e5636629c031e5eafa8dc27af89ed/stripe-14.4.1-py3-none-any.whl", hash = "sha256:d6722b985dc3b4d6da01ff4fa95a975d3694a89bba96895bf17aa858073ff2b8", size = 2116642, upload-time = "2026-03-06T22:59:43.72Z" },
]
[[package]] [[package]]
name = "sympy" name = "sympy"
version = "1.14.0" version = "1.14.0"