diff --git a/.env.example b/.env.example index f7088c9..9db2b16 100644 --- a/.env.example +++ b/.env.example @@ -8,9 +8,17 @@ DB_NAME=mainloop DB_USER=mainloop DB_PASSWORD=changeme -# Claude Code Credentials -# These will be automatically populated by 'make setup-claude-creds' -# CLAUDE_CREDENTIALS= +# Substrate native-session routing +SUBSTRATE_ROUTER_ADDRESS=http://atenet-router.ate-system.svc.cluster.local:8081 +SUBSTRATE_SHIM_SECRET_NAMESPACE=mainloop-control +# Map each provider to a pre-created actor and its shim token Secret. +SUBSTRATE_ACTOR_BINDINGS={"claude":{"atespace":"","actor":"","shim_token_secret_name":""},"codex":{"atespace":"","actor":"","shim_token_secret_name":""}} +# Substrate lifecycle control; empty values use the host's ambient config. +SUBSTRATE_KUBECONFIG= +SUBSTRATE_CONTEXT= +SUBSTRATE_ATESPACE=mainloop-workspaces +SUBSTRATE_ACTOR_TEMPLATE=mainloop-workspace +SUBSTRATE_CLI=kubectl-ate # GitHub Container Registry # Your GitHub username for pushing images to ghcr.io diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 53a61da..b99f398 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -43,11 +43,6 @@ jobs: context: . tag: mainloop-frontend:test build-args: VITE_API_URL=http://localhost:8081 - - image: agent - file: claude-agent/Dockerfile - context: ./claude-agent - tag: mainloop-agent-controller:test - steps: - name: Checkout uses: actions/checkout@v4 diff --git a/.trunk/trunk.yaml b/.trunk/trunk.yaml index bf1a815..cc6e5c9 100644 --- a/.trunk/trunk.yaml +++ b/.trunk/trunk.yaml @@ -36,7 +36,6 @@ lint: - backend/src/mainloop/config.py - backend/src/mainloop/db/postgres.py - backend/src/mainloop/services/github_pr.py - - claude-agent/server.py # DL3008: Pinning apt versions causes build failures when versions are removed from repos # Terrascan Docker checks have platform differences (Linux vs macOS) and false positives # for multi-stage builds with --chown flags diff --git a/AGENTS.md b/AGENTS.md index 15549ef..9df3689 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -24,7 +24,7 @@ When documents differ, do not silently blend future design with current behavior - Keep task lifecycle, agent activity, message delivery, user attention, workspace health, and publication state as separate concepts. - Parallel work is limited by architectural cohesion, not just worker capacity. Resolve shared contracts and helpers before dispatching independent consumers. -The `claude-agent/` service and Claude Agent SDK paths are part of the existing implementation. The roadmap intends to replace them deliberately with native-agent integration; do not extend them as the new long-term architecture unless a compatibility change requires it. +Native Claude Code and Codex sessions run through the Substrate workspace adapter. Do not add the superseded worker or another workspace runtime. ## Project structure @@ -33,7 +33,6 @@ backend/ FastAPI, DBOS workflows, PostgreSQL access, and orchestration frontend/ SvelteKit 5 application and Playwright tests models/ Shared Python/Pydantic models packages/ui/ Shared frontend theme and UI package -claude-agent/ Existing Claude Agent SDK worker implementation k8s/ Kubernetes bases and overlays docs/specs/ User-visible behavior specifications scripts/ Development and test automation diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 12b8671..ceee240 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -69,7 +69,6 @@ Thank you for your interest in contributing to mainloop! This document provides mainloop/ ├── backend/ # Python FastAPI + DBOS workflows ├── frontend/ # SvelteKit + Tailwind v4 -├── claude-agent/ # Claude Code CLI container ├── models/ # Shared Pydantic models ├── packages/ui/ # Design tokens + theme └── k8s/ # Kubernetes manifests diff --git a/Makefile b/Makefile index e08340b..d19e5e4 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -.PHONY: help dev dev-stop dev-reset dev-logs dev-shell dev-legacy install clean lint lint-all fmt fmt-all setup-claude-creds setup-claude-creds-k8s build-backend build-frontend build-agent-controller build-all push-backend push-frontend push-agent-controller push-all build-all-parallel push-all-parallel deploy deploy-loop deploy-loop-all deploy-backend deploy-agent deploy-frontend-k8s deploy-manifests prod-reset kind-create kind-delete kind-load kind-secrets kind-deploy kind-reset kind-logs kind-shell test test-run test-reset test-ci debug-tasks debug-task debug-retry debug-logs debug-db +.PHONY: help dev dev-stop dev-reset dev-logs dev-shell dev-legacy install clean lint lint-all fmt fmt-all build-backend build-frontend build-all push-backend push-frontend push-all build-all-parallel push-all-parallel deploy deploy-loop deploy-loop-all deploy-backend deploy-frontend-k8s deploy-manifests prod-reset kind-create kind-delete kind-load kind-secrets kind-deploy kind-reset kind-logs kind-shell test test-run test-reset test-ci debug-tasks debug-task debug-retry debug-logs debug-db # Load .env file if it exists -include .env @@ -11,7 +11,6 @@ GHCR_USER ?= yourusername IMAGE_TAG ?= latest BACKEND_IMAGE := $(GHCR_REGISTRY)/$(GHCR_USER)/mainloop-backend:$(IMAGE_TAG) FRONTEND_IMAGE := $(GHCR_REGISTRY)/$(GHCR_USER)/mainloop-frontend:$(IMAGE_TAG) -AGENT_CONTROLLER_IMAGE := $(GHCR_REGISTRY)/$(GHCR_USER)/mainloop-agent-controller:$(IMAGE_TAG) help: ## Show this help message @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-20s\033[0m %s\n", $$1, $$2}' @@ -26,7 +25,7 @@ dev: ## Start dev environment with hot reload (DevSpace + Kind) dev-stop: ## Stop DevSpace and purge resources devspace purge --kube-context kind-$(KIND_CLUSTER_NAME) -n mainloop -dev-reset: ## Reset ALL data (database + task namespaces + restart backend) +dev-reset: ## Reset local database and restart the backend @./scripts/kind/reset-data.sh dev-clear-cache: ## Clear Vite cache (fixes stale HMR issues) @@ -70,72 +69,6 @@ fmt-all: ## Format and fix all files trunk fmt -a trunk check -a -y -setup-claude-creds: ## Login to Claude inside Linux container, credentials saved to shared volume - @echo "=== Claude Container Login ===" - @echo "This will start a container where you can login to Claude." - @echo "Credentials will be saved to the 'claude-config' Docker volume." - @echo "" - @docker build -q -t mainloop-claude-agent ./claude-agent > /dev/null - @docker volume create claude-config > /dev/null 2>&1 || true - @docker rm -f claude-login-tmp > /dev/null 2>&1 || true - @docker run -d --entrypoint "" --name claude-login-tmp \ - -v claude-config:/home/claude/.claude \ - mainloop-claude-agent sleep 3600 > /dev/null - @echo "Container started with shared claude-config volume." - @echo "Run: claude login" - @echo "Complete the browser OAuth flow, then type 'exit'" - @echo "" - @docker exec -it claude-login-tmp bash; \ - echo ""; \ - echo "Checking credentials..."; \ - if docker exec claude-login-tmp test -f /home/claude/.claude/.credentials.json; then \ - echo "✓ Credentials saved to claude-config volume"; \ - echo " All containers mounting this volume will have access."; \ - else \ - echo "⚠ No credentials found. Did you complete the login?"; \ - fi; \ - docker rm -f claude-login-tmp > /dev/null - -setup-claude-creds-mac: ## Extract Claude credentials from macOS Keychain (Mac only) - @echo "Extracting Claude credentials from macOS Keychain..." - @CREDS=$$(security find-generic-password -s "Claude Code-credentials" -a "$(USER)" -w 2>/dev/null); \ - if [ -z "$$CREDS" ]; then \ - echo "Error: Claude credentials not found in Keychain."; \ - echo "Make sure you're logged in to Claude Code on this Mac."; \ - exit 1; \ - fi; \ - if [ -f .env ]; then \ - echo "Updating CLAUDE_CREDENTIALS in .env (preserving other variables)..."; \ - grep -v "^CLAUDE_CREDENTIALS=" .env > .env.tmp || true; \ - mv .env.tmp .env; \ - else \ - echo "Creating .env file..."; \ - touch .env; \ - fi; \ - echo "CLAUDE_CREDENTIALS=$$CREDS" >> .env; \ - echo "✓ Claude credentials updated in .env" - -setup-claude-creds-k8s: ## Push Claude credentials from Docker volume to 1Password for k8s - @echo "Extracting credentials from Docker volume and pushing to 1Password..." - @if ! command -v op >/dev/null 2>&1; then \ - echo "Error: 1Password CLI (op) not found."; \ - echo "Install it: brew install --cask 1password-cli"; \ - exit 1; \ - fi; \ - docker run --rm -v claude-config:/config:ro alpine cat /config/.credentials.json > /tmp/claude-creds.json 2>/dev/null; \ - if [ ! -s /tmp/claude-creds.json ]; then \ - echo "Error: No credentials in claude-config volume"; \ - echo "Run 'make setup-claude-creds' first"; \ - rm -f /tmp/claude-creds.json; \ - exit 1; \ - fi; \ - echo "Creating/updating claude-credentials item in kubernetes vault..."; \ - op item get claude-credentials --vault kubernetes >/dev/null 2>&1 && \ - op item delete claude-credentials --vault kubernetes || true; \ - op document create /tmp/claude-creds.json --title=claude-credentials --vault=kubernetes >/dev/null; \ - rm -f /tmp/claude-creds.json; \ - echo "✓ Claude credentials pushed to 1Password vault 'kubernetes'" - # Backend commands backend-dev: ## Run backend in development mode cd backend && uv run uvicorn mainloop.api:app --reload --host 0.0.0.0 --port 8000 @@ -151,10 +84,7 @@ build-backend: ## Build backend Docker image build-frontend: ## Build frontend Docker image docker build -f frontend/Dockerfile -t $(FRONTEND_IMAGE) . -build-agent-controller: ## Build agent controller Docker image - docker build -f claude-agent/Dockerfile -t $(AGENT_CONTROLLER_IMAGE) ./claude-agent - -build-all: build-backend build-frontend build-agent-controller ## Build all Docker images +build-all: build-backend build-frontend ## Build all Docker images push-backend: build-backend ## Push backend to GHCR docker push $(BACKEND_IMAGE) @@ -162,17 +92,13 @@ push-backend: build-backend ## Push backend to GHCR push-frontend: build-frontend ## Push frontend to GHCR docker push $(FRONTEND_IMAGE) -push-agent-controller: build-agent-controller ## Push agent controller to GHCR - docker push $(AGENT_CONTROLLER_IMAGE) - -push-all: push-backend push-frontend push-agent-controller ## Push all images to GHCR +push-all: push-backend push-frontend ## Push all images to GHCR # Parallel build + push (much faster) build-all-parallel: ## Build all Docker images in parallel @echo "Building all images in parallel..." @docker build -f backend/Dockerfile -t $(BACKEND_IMAGE) . & \ docker build -f frontend/Dockerfile -t $(FRONTEND_IMAGE) . & \ - docker build -f claude-agent/Dockerfile -t $(AGENT_CONTROLLER_IMAGE) ./claude-agent & \ wait @echo "All builds complete" @@ -180,7 +106,6 @@ push-all-parallel: build-all-parallel ## Build and push all images in parallel @echo "Pushing all images in parallel..." @docker push $(BACKEND_IMAGE) & \ docker push $(FRONTEND_IMAGE) & \ - docker push $(AGENT_CONTROLLER_IMAGE) & \ wait @echo "All pushes complete" @@ -190,7 +115,6 @@ deploy: push-all-parallel ## Full deployment to k8s (parallel builds + pushes) kubectl apply -k k8s/apps/mainloop/overlays/prod --server-side --force-conflicts @echo "Restarting Kubernetes deployments in parallel..." @kubectl rollout restart deployment/mainloop-backend -n mainloop & \ - kubectl rollout restart deployment/mainloop-agent-controller -n mainloop & \ kubectl rollout restart deployment/mainloop-frontend -n mainloop & \ wait @echo "Rollouts triggered" @@ -203,7 +127,7 @@ deploy-loop: ## Pull and deploy every 10 seconds done deploy-loop-all: ## Watch all and redeploy everything (old behavior) - watchexec --poll 1000 -w backend -w frontend/src -w k8s -w models -w claude-agent \ + watchexec --poll 1000 -w backend -w frontend/src -w k8s -w models \ -e py,ts,svelte,yaml,toml,Dockerfile \ -i 'test*' -i '*_test.py' -i 'tests/' -i '__pycache__/' -i '.pytest_cache/' -i 'scripts/' \ --on-busy-update restart \ @@ -215,11 +139,6 @@ deploy-backend: ## Build, push, and restart backend only docker push $(BACKEND_IMAGE) kubectl rollout restart deployment/mainloop-backend -n mainloop -deploy-agent: ## Build, push, and restart agent controller only - docker build -f claude-agent/Dockerfile -t $(AGENT_CONTROLLER_IMAGE) ./claude-agent - docker push $(AGENT_CONTROLLER_IMAGE) - kubectl rollout restart deployment/mainloop-agent-controller -n mainloop - deploy-frontend-k8s: ## Build, push, and restart frontend only (k8s version) docker build -f frontend/Dockerfile -t $(FRONTEND_IMAGE) . docker push $(FRONTEND_IMAGE) @@ -230,13 +149,8 @@ deploy-manifests: ## Apply k8s manifests only (no image builds) PROD_CONTEXT ?= admin@internal-01 -prod-reset: ## Reset prod database + task namespaces + restart backend +prod-reset: ## Reset prod database + restart backend @echo "=== Using context: $(PROD_CONTEXT) ===" - @echo "=== Cleaning up k8s task namespaces ===" - @for ns in $$(kubectl --context $(PROD_CONTEXT) get ns -o name 2>/dev/null | grep "^namespace/task-" | cut -d/ -f2); do \ - echo "Deleting namespace: $$ns"; \ - kubectl --context $(PROD_CONTEXT) delete ns "$$ns" --wait=false 2>/dev/null || true; \ - done @echo "=== Deleting CNPG Database CR ===" kubectl --context $(PROD_CONTEXT) delete database mainloop-db-database -n mainloop --wait=true @echo "=== Recreating Database CR ===" @@ -293,27 +207,14 @@ test-k8s: kind-create kind-load kind-secrets kind-deploy ## Start local K8s test test-loop: ## Watch for changes and auto-redeploy to Kind @echo "Starting Kind deploy loop (Ctrl+C to stop)..." - @echo "Watching: backend/, frontend/src/, claude-agent/" + @echo "Watching: backend/, frontend/src/" @trap 'kill 0' INT; \ watchexec -w backend/src -w models -e py \ --on-busy-update restart -- bash -c 'make kind-load && make kind-deploy' & \ watchexec -w frontend/src -e ts,svelte,css \ --on-busy-update restart -- bash -c 'make kind-load && make kind-deploy' & \ - watchexec -w claude-agent -e py \ - --on-busy-update restart -- bash -c 'make kind-load && make kind-deploy' & \ wait -# E2E Testing -test-k8s-components: ## Test K8s namespace/secret creation (quick) - cd backend && uv run python scripts/test_k8s_components.py - -test-k8s-job: ## Test K8s job creation (creates a real job) - cd backend && uv run python scripts/test_k8s_components.py --job - -test-worker-e2e: ## Run full worker E2E test (disabled; ENABLE_E2E=1 to opt in) - @./scripts/e2e-guard.sh - cd backend && REPO_URL="$(or $(REPO_URL),https://github.com/oldsj/mainloop)" uv run python scripts/test_worker_e2e.py - # ============================================================================= # Testing (DevSpace + Playwright) # @@ -340,10 +241,6 @@ test-reset: dev-reset ## Alias for dev-reset test-ci: ## Run tests with legacy kind scripts (disabled; ENABLE_E2E=1 to opt in) @./scripts/e2e-guard.sh - @if [ -z "$(CLAUDE_CODE_OAUTH_TOKEN)" ]; then \ - echo "Error: CLAUDE_CODE_OAUTH_TOKEN not set"; \ - exit 1; \ - fi @if ! kind get clusters 2>/dev/null | grep -q "^$(KIND_CLUSTER_NAME)$$"; then \ $(MAKE) kind-create; \ fi diff --git a/README.md b/README.md index b2f69cd..602903a 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ You (phone/laptop) ▼ ┌─────────────────────────────────────────────────────┐ │ Main Thread │ -│ Claude with spawn_session tool │ +│ Native Claude Code session in Substrate │ │ │ │ user@mainloop$ research X ← inline sessions │ │ ├── [research X] thinking... ← threaded reply │ @@ -33,20 +33,20 @@ You (phone/laptop) └──────────────┘ └──────────────┘ ``` -- **Main thread**: One continuous conversation — sessions spawn inline and surface results back -- **Sessions**: Background AI work with their own conversations; appear as colored threads in your timeline +- **Main thread**: One continuous native conversation; delegated sessions surface results back +- **Sessions**: Native Claude Code or Codex work with their own conversations; appear as colored threads in your timeline - **Notifications**: Slack-style thread replies notify you when sessions need attention or complete -- **Persistence**: Conversations and sessions survive restarts via compaction + [DBOS](https://docs.dbos.dev/) +- **Persistence**: Mainloop stores conversations, delivery records, and workspace lifecycle state in PostgreSQL; native history remains with the provider CLI in Substrate +- **Runtime isolation**: Substrate workspaces use gVisor actors. The pinned fork honors the + agent image's non-root UID `10001`; microVM isolation is deferred. ## Quick Start ```bash # Copy example environment file and configure cp .env.example .env -# Edit .env with your GitHub username (GHCR_USER) and domains - -# Optional: authenticate the current Claude integration -make setup-claude-creds +# Set the Substrate router, actor bindings, and shim Secret names. +# Keep provider credentials in Mainloop's configured control-plane Secrets; actors receive synthetic placeholders only. # Start all services make dev @@ -65,7 +65,6 @@ The Kubernetes manifests under `k8s/apps/mainloop/` provide reusable bases and e mainloop/ ├── backend/ # Python FastAPI + DBOS workflows ├── frontend/ # SvelteKit + Tailwind v4 (mobile-first responsive) -├── claude-agent/ # Claude Code CLI container ├── models/ # Shared Pydantic models ├── packages/ui/ # Design tokens + theme.css └── k8s/ # Kubernetes manifests @@ -92,12 +91,12 @@ mainloop/ ## Agent Workflow -Agents are sessions spawned for development tasks. Each agent gets its own K8s namespace for isolated iteration. +Agents are native sessions spawned for development tasks. Mainloop records the session and its deliveries; the native CLI runs in the Substrate actor selected by the configured provider binding. ```text ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ Spawn │────►│ Work │────►│ PR │────►│ Close │ -│ (main) │ │ (k8s ns) │ │ (GitHub) │ │ (summary) │ +│ (main) │ │(Substrate) │ │ (GitHub) │ │ (summary) │ └─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘ ▲ │ └───────────────────┘ @@ -105,7 +104,7 @@ Agents are sessions spawned for development tasks. Each agent gets its own K8s n ``` 1. **Spawn** - Main thread creates agent for a task -2. **Work** - Agent iterates in its own K8s namespace (build, test, debug) +2. **Work** - A native Claude Code or Codex session runs in its configured Substrate actor 3. **PR** - Agent creates and merges GitHub PR when ready 4. **Close** - Agent posts summary back to main thread @@ -121,7 +120,7 @@ You stay in main thread, checking in on agents and spawning new ones as needed. **Guides**: -- [Architecture](docs/architecture.md) - System design and data flow +- [Architecture](docs/architecture.md) - Substrate workspaces, delivery, previews, and credentials - [Contributing](CONTRIBUTING.md) - Local setup, development commands, and contribution guidance ## License diff --git a/backend/.env.example b/backend/.env.example index 07ef4e6..2c87d2b 100644 --- a/backend/.env.example +++ b/backend/.env.example @@ -1,5 +1,15 @@ -# Claude Agent (optional - for direct API access) -ANTHROPIC_API_KEY=your-api-key-here +# Native sessions connect to deployment-provided Substrate actors. +SUBSTRATE_ROUTER_ADDRESS=http://atenet-router.ate-system.svc.cluster.local:8081 +SUBSTRATE_SHIM_SECRET_NAMESPACE=mainloop-control +# Map each native provider to its actor and shim token Secret name. +SUBSTRATE_ACTOR_BINDINGS={"claude":{"atespace":"","actor":"","shim_token_secret_name":""},"codex":{"atespace":"","actor":"","shim_token_secret_name":""}} + +# Substrate lifecycle control. Leave empty to use the host's ambient config. +SUBSTRATE_KUBECONFIG= +SUBSTRATE_CONTEXT= +SUBSTRATE_ATESPACE=mainloop-workspaces +SUBSTRATE_ACTOR_TEMPLATE=mainloop-workspace +SUBSTRATE_CLI=kubectl-ate # Server HOST=0.0.0.0 diff --git a/backend/Dockerfile b/backend/Dockerfile index b06f4b9..48d3f30 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -1,3 +1,21 @@ +FROM golang:1.27.0-bookworm AS substrate-cli + +ARG SUBSTRATE_REPOSITORY=https://github.com/oldsj/substrate.git +ARG SUBSTRATE_COMMIT=0f9635aed37bd5dde604a9bca1975421cd07181a + +RUN apt-get update \ + && apt-get install -y --no-install-recommends git ca-certificates \ + && rm -rf /var/lib/apt/lists/* + +WORKDIR /src + +RUN git init \ + && git remote add origin "${SUBSTRATE_REPOSITORY}" \ + && git fetch --depth=1 origin "${SUBSTRATE_COMMIT}" \ + && git checkout --detach FETCH_HEAD \ + && mkdir -p /out \ + && CGO_ENABLED=0 go build -trimpath -o /out/kubectl-ate ./cmd/kubectl-ate + # Build stage - use uv image with Python pre-installed FROM ghcr.io/astral-sh/uv:python3.13-bookworm-slim AS builder @@ -29,42 +47,31 @@ RUN --mount=type=cache,target=/root/.cache/uv \ # Development stage - includes uv for live dependency updates FROM ghcr.io/astral-sh/uv:python3.13-bookworm-slim AS dev -# Install Node.js and Claude CLI (required by claude-agent-sdk) RUN apt-get update \ && apt-get install -y --no-install-recommends curl ca-certificates \ - && curl -fsSL https://deb.nodesource.com/setup_22.x | bash - \ - && apt-get install -y --no-install-recommends nodejs \ - && npm install -g @anthropic-ai/claude-code@latest \ - && npm cache clean --force \ && apt-get clean \ - && rm -rf /var/lib/apt/lists/* /tmp/* /root/.npm + && rm -rf /var/lib/apt/lists/* /tmp/* -# Create non-root user for Claude and workspace directory -RUN useradd -m -s /bin/bash claude \ - && mkdir -p /home/claude/.claude /workspace \ - && chown -R claude:claude /home/claude /workspace +RUN useradd -m -s /bin/bash mainloop WORKDIR /app # Copy dependency files (owned by claude for live updates) -COPY --from=builder --chown=claude:claude /app/.venv /app/.venv -COPY --chown=claude:claude backend/pyproject.toml backend/uv.lock backend/README.md ./ +COPY --from=builder --chown=mainloop:mainloop /app/.venv /app/.venv +COPY --from=substrate-cli /out/kubectl-ate /usr/local/bin/kubectl-ate +COPY --chown=mainloop:mainloop backend/pyproject.toml backend/uv.lock backend/README.md ./ # Copy source code (owned by claude) -COPY --chown=claude:claude backend/src ./src +COPY --chown=mainloop:mainloop backend/src ./src # Copy shared models package (owned by claude) -COPY --chown=claude:claude models ../models - -# Copy entrypoint script -COPY --chown=claude:claude backend/entrypoint.sh /entrypoint.sh -RUN chmod +x /entrypoint.sh +COPY --chown=mainloop:mainloop models ../models # Set up PATH for venv and uv ENV PATH="/app/.venv/bin:$PATH" # Switch to non-root user -USER claude +USER mainloop EXPOSE 8000 @@ -73,47 +80,36 @@ CMD ["uvicorn", "mainloop.api:app", "--host", "0.0.0.0", "--port", "8000", "--re # Runtime stage (production) FROM python:3.13-slim AS prod -# Install Node.js and Claude CLI (required by claude-agent-sdk) +# Install the health-check client and CA certificates. RUN apt-get update \ && apt-get install -y --no-install-recommends curl ca-certificates \ - && curl -fsSL https://deb.nodesource.com/setup_22.x | bash - \ - && apt-get install -y --no-install-recommends nodejs \ - && npm install -g @anthropic-ai/claude-code@latest \ - && npm cache clean --force \ && apt-get purge -y --auto-remove ca-certificates \ && apt-get clean \ - && rm -rf /var/lib/apt/lists/* /tmp/* /root/.npm + && rm -rf /var/lib/apt/lists/* /tmp/* -# Create non-root user for Claude and workspace directory -RUN useradd -m -s /bin/bash claude \ - && mkdir -p /home/claude/.claude /workspace \ - && chown -R claude:claude /home/claude /workspace +RUN useradd -m -s /bin/bash mainloop WORKDIR /app # Copy the virtual environment from builder (owned by claude) -COPY --from=builder --chown=claude:claude /app/.venv /app/.venv +COPY --from=builder --chown=mainloop:mainloop /app/.venv /app/.venv +COPY --from=substrate-cli /out/kubectl-ate /usr/local/bin/kubectl-ate # Copy source code (owned by claude) -COPY --chown=claude:claude backend/src ./src +COPY --chown=mainloop:mainloop backend/src ./src # Copy shared models package (owned by claude) -COPY --chown=claude:claude models ../models - -# Copy entrypoint script -COPY --chown=claude:claude backend/entrypoint.sh /entrypoint.sh -RUN chmod +x /entrypoint.sh +COPY --chown=mainloop:mainloop models ../models # Set up PATH ENV PATH="/app/.venv/bin:$PATH" -# Switch to non-root user (required for Claude SDK bypass permissions) -USER claude +# Run without root privileges. +USER mainloop EXPOSE 8000 HEALTHCHECK --interval=30s --timeout=5s --start-period=5s --retries=3 \ CMD curl -f http://localhost:8000/health || exit 1 -ENTRYPOINT ["/entrypoint.sh"] CMD ["uvicorn", "mainloop.api:app", "--host", "0.0.0.0", "--port", "8000"] diff --git a/backend/README.md b/backend/README.md index 134be60..9c851ad 100644 --- a/backend/README.md +++ b/backend/README.md @@ -1,6 +1,6 @@ # mainloop-backend -FastAPI backend for the mainloop AI agent orchestrator. +FastAPI control plane for native-agent sessions and Substrate workspaces. ## Development @@ -17,7 +17,7 @@ make backend-dev ## Environment Variables -See `.env.example` for required environment variables. +See `.env.example` for the Substrate router, actor bindings, lifecycle settings, and database environment variables. Provider credentials are held by the configured actors, not by the backend. ## API Documentation diff --git a/backend/entrypoint.sh b/backend/entrypoint.sh deleted file mode 100644 index 6968148..0000000 --- a/backend/entrypoint.sh +++ /dev/null @@ -1,31 +0,0 @@ -#!/bin/bash -set -e - -echo "Starting Mainloop Backend API..." - -# Create Claude credentials file from env var if token is provided -# This is required for the Claude Agent SDK to authenticate -if [[ -n ${CLAUDE_CODE_OAUTH_TOKEN} ]]; then - mkdir -p ~/.claude - cat >~/.claude/.credentials.json <=0.11.0", "pydantic>=2.12.5", "pydantic-settings>=2.0.0", - "claude-agent-sdk>=0.1.18", "asyncpg>=0.30.0", "dbos>=2.7.0", "pydantic-ai[dbos]>=1.39.0", "kubernetes>=34.1.0", - "anthropic>=0.75.0", ] [project.scripts] diff --git a/backend/scripts/gate5_setup.py b/backend/scripts/gate5_setup.py new file mode 100644 index 0000000..7c8917c --- /dev/null +++ b/backend/scripts/gate5_setup.py @@ -0,0 +1,1118 @@ +#!/usr/bin/env python3 +r""" +Gate 5 (native-session continuity, .tasknotes/plan.md) credential-free harness: registers +the atespace, resolves and applies the WorkerPool, creates a *versioned* ActorTemplate and +waits for its golden snapshot, then creates/resumes one actor and waits for it to become +RUNNING with its actor-local headless shim confirmed ready -- all without a +provider credential, a credential server, or a native Claude/Codex session. + +Implements recovery step 2 of .tasknotes/gate5-review-and-recovery-plan-2026-09-22.md. That +review found the prior harness applied an unresolved `ko://` worker image, never registered +the atespace at the API level (a Kubernetes Namespace of the same name is not an atespace), +checked for an existing ActorTemplate with the atespace embedded in the name instead of the +CLI's required `-a` flag, and printed success once a log line appeared even when that +happened before the read that mattered -- while the underlying golden-snapshot failure was a +credential fetch built into the shared, immutable template, which this script's manifest no +longer has (see spikes/substrate-workspace-adapter/live-agent-image/entrypoint.sh). + +This setup harness remains credential-free: it installs and checks the shim token but does not +fetch provider credentials or submit a native turn. The image's authenticated /turn endpoint +runs one headless native CLI process per request; Mainloop owns delivery and retry decisions. + +Prerequisites: + kubectl and kubectl-ate. Either pass an existing digest-pinned WorkerPool image with + --worker-image, or pass ko and a checkout of the oldsj/substrate fork at + SUBSTRATE_FORK_COMMIT (see docs/spikes/substrate-workspace-adapter.md). + A running kind-substrate-preview cluster with ate-system and Envoy plus sdsmint installed + (this script accepts only the exact kind-substrate-preview context). + The live-agent-gate image already built and pushed (see live-agent-image/), its digest + passed with --image. + +Usage: + cd backend + uv run python scripts/gate5_setup.py \\ + --context kind-substrate-preview --kubeconfig /tmp/substrate-preview-kubeconfig \\ + --ate-cli "$SUBSTRATE_SRC/bin/kubectl-ate" \\ + --ko "$SUBSTRATE_SRC/bin/ko" \\ + --worker-image localhost:5001/ateom-gvisor@sha256: \\ + --atespace nonroot-check --worker-pool nonroot-check --template-version v1 \\ + --image localhost:5001/live-agent-gate@sha256:... \\ + --manifest ../spikes/substrate-workspace-adapter/k8s/actor-template.yaml.tmpl \\ + --state-file /tmp/gate5-run-state.json \\ + --egress-deny-all + + Use a fresh atespace for this check: the applied product WorkerPool requests two replicas. + --worker-pool defaults to the atespace name so its label is distinct from other namespaces. + --worker-image applies an existing digest-pinned WorkerPool image directly and does not + invoke ko; without it, --substrate-src is required and ko resolves the WorkerPool image. + + Re-running with the same --state-file reconciles the persisted actor uid against the +cluster's current state rather than blindly creating or resuming; a name collision with a +*different* uid is refused, not silently overwritten. +""" + +import argparse +import asyncio +import contextlib +import http.client +import json +import os +import re +import secrets +import socket +import string +import subprocess # nosec B404 - drives trusted local kubectl/kubectl-ate/ko binaries, argv only +import sys +import tempfile +import time +import urllib.error +import urllib.request +import uuid +from pathlib import Path + +sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "src")) + +from mainloop.runtime.substrate import ( # noqa: E402 + ActorFailedToStart, + ActorState, + GoldenState, + IdentityConflict, + IdentityOutcome, + SubstrateControl, + TransportError, + reconcile_actor_identity, + wait_for_actor_health, + wait_for_actor_running, + wait_for_eligible_worker, + wait_for_golden_snapshot, +) + +# The `patched-next` branch of https://github.com/oldsj/substrate. +SUBSTRATE_FORK_COMMIT = "0f9635aed37bd5dde604a9bca1975421cd07181a" +WORKER_SANDBOX_CLASS = "gvisor" +ACTOR_SHIM_PORT = 8090 + + +def parse_args() -> argparse.Namespace: + p = argparse.ArgumentParser(description=__doc__) + p.add_argument("--context", required=True) + p.add_argument("--kubeconfig", required=True) + p.add_argument("--ate-cli", default="kubectl-ate") + p.add_argument("--ko", default="ko") + p.add_argument( + "--substrate-src", + help="pinned oldsj/substrate checkout used to resolve a ko:// WorkerPool image", + ) + p.add_argument( + "--worker-image", + help=( + "existing WorkerPool image pinned by digest; skips ko resolution/build/push " + "and applies the WorkerPool manifest directly" + ), + ) + p.add_argument("--router-port", type=int, default=18091) + p.add_argument("--atespace", required=True) + p.add_argument( + "--worker-pool", + help="WorkerPool name and workload label (defaults to the atespace name)", + ) + p.add_argument( + "--template-version", + required=True, + help='e.g. "v2" -- never reuse one whose golden snapshot failed', + ) + p.add_argument( + "--image", + required=True, + help="already-built and pushed image digest, e.g. localhost:5001/live-agent-gate@sha256:...", + ) + p.add_argument( + "--manifest", + required=True, + help="path to the three-document product actor template", + ) + p.add_argument("--bucket-name", default="ate-snapshots") + p.add_argument("--actor-name", default="claude-gate5") + p.add_argument("--state-file", required=True) + p.add_argument("--golden-timeout", type=float, default=300) + p.add_argument("--worker-timeout", type=float, default=120) + p.add_argument("--actor-timeout", type=float, default=120) + p.add_argument("--readiness-timeout", type=float, default=60) + egress = p.add_mutually_exclusive_group(required=True) + egress.add_argument("--egress-cidr", help="CIDR to allow") + egress.add_argument( + "--egress-hostname", + action="append", + dest="egress_hostnames", + metavar="HOSTNAME", + help="provider hostname to allow (repeatable)", + ) + egress.add_argument("--egress-allow-all", action="store_true") + egress.add_argument("--egress-deny-all", action="store_true") + args = p.parse_args() + if args.worker_pool is None: + args.worker_pool = args.atespace + return args + + +def load_state(path: str) -> dict: + if not os.path.exists(path): + return {} + with open(path) as f: + return json.load(f) + + +def save_state(path: str, state: dict) -> None: + tmp = f"{path}.tmp" + fd = os.open(tmp, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600) + with os.fdopen(fd, "w") as f: + json.dump(state, f, indent=2) + os.chmod(tmp, 0o600) + os.replace(tmp, path) + os.chmod(path, 0o600) + + +def render_manifest( + path: str, + *, + atespace: str, + worker_pool_name: str, + template_name: str, + bucket_name: str, + image: str, + worker_image: str | None = None, +) -> list[str]: + """Substitutes the template placeholders and image marker, then splits the multi-document + YAML on its own '---' separators. Returns [namespace_and_workerpool_doc, + actor_template_doc].""" + with open(path) as f: + raw = f.read() + rendered = ( + string.Template(raw) + .safe_substitute( + ATESPACE=atespace, + WORKER_POOL_NAME=worker_pool_name, + TEMPLATE_NAME=template_name, + BUCKET_NAME=bucket_name, + ) + .replace("__IMAGE__", image) + ) + if worker_image is not None: + if not re.fullmatch(r"\S+@sha256:[0-9a-fA-F]{64}", worker_image): + raise RuntimeError("--worker-image must be pinned by a full sha256 digest") + worker_image_uri = "ko://github.com/agent-substrate/substrate/cmd/ateom-gvisor" + if worker_image_uri not in rendered: + raise RuntimeError( + f"expected the WorkerPool template to contain {worker_image_uri}" + ) + rendered = rendered.replace(worker_image_uri, worker_image) + docs = [d for d in rendered.split("\n---\n") if d.strip()] + if len(docs) != 3: + raise RuntimeError( + f"expected 3 YAML documents (Namespace, WorkerPool, ActorTemplate) in {path}, got {len(docs)}" + ) + namespace_and_workerpool = f"{docs[0]}\n---\n{docs[1]}\n" + return [namespace_and_workerpool, docs[2] + "\n"] + + +def render_gate_manifest(args: argparse.Namespace) -> list[str]: + """Render the product template with this run's atespace, pool, and versioned template.""" + template_name = f"live-agent-gate-{args.template_version}" + return render_manifest( + args.manifest, + atespace=args.atespace, + worker_pool_name=args.worker_pool, + template_name=template_name, + bucket_name=args.bucket_name, + image=args.image, + worker_image=args.worker_image, + ) + + +def verify_substrate_source(source: str, *, runner=subprocess.run) -> str: + """Require the exact source checkout that supplies the pinned ``ko`` module.""" + root = Path(source).expanduser().resolve() + if not (root / ".git").exists(): + raise RuntimeError(f"--substrate-src is not a git checkout: {root}") + for required in ("go.mod", ".ko.yaml"): + if not (root / required).is_file(): + raise RuntimeError(f"--substrate-src is missing {required}: {root}") + result = runner( + ["git", "-C", str(root), "rev-parse", "HEAD"], + capture_output=True, + text=True, + check=True, + timeout=15, + ) + commit = result.stdout.strip() + if commit != SUBSTRATE_FORK_COMMIT: + raise RuntimeError( + f"--substrate-src must be pinned at {SUBSTRATE_FORK_COMMIT}; found {commit!r}" + ) + return str(root) + + +IMAGE_MANIFEST_ACCEPT = ", ".join( + ( + "application/vnd.oci.image.index.v1+json", + "application/vnd.docker.distribution.manifest.v2+json", + "application/vnd.oci.image.manifest.v1+json", + ) +) + + +def verify_image_manifest(image: str, *, opener=urllib.request.urlopen) -> None: + """Verify the digest-addressed image is present in the registry workers will use. + + Local Docker RepoDigests can refer to a registry that has since been deleted. A HEAD + against the registry endpoint catches that setup error before creating an immutable + ActorTemplate and its golden actor. + """ + reference, separator, digest = image.partition("@") + registry, slash, repository = reference.partition("/") + if ( + not separator + or not slash + or not registry + or not repository + or not re.fullmatch(r"sha256:[0-9a-fA-F]{64}", digest) + ): + raise RuntimeError( + "--image must be a registry/repository pinned by a full sha256 digest" + ) + + request = urllib.request.Request( + f"http://{registry}/v2/{repository}/manifests/{digest}", + headers={"Accept": IMAGE_MANIFEST_ACCEPT}, + method="HEAD", + ) + try: + with opener(request, timeout=10) as response: + status = response.status + except urllib.error.HTTPError as exc: + raise RuntimeError( + f"image manifest preflight failed: registry returned HTTP {exc.code}" + ) from exc + except (urllib.error.URLError, OSError) as exc: + raise RuntimeError(f"image manifest preflight failed: {exc}") from exc + if not 200 <= status < 300: + raise RuntimeError( + f"image manifest preflight failed: registry returned HTTP {status}" + ) + print(f"-- registry manifest confirmed for {repository}@{digest}") + + +def get_cluster_identity( + *, context: str, kubeconfig: str, runner=subprocess.run +) -> dict[str, str]: + """Identify the selected cluster by its API URL and kube-system namespace UID.""" + base = ["kubectl", "--context", context, "--kubeconfig", kubeconfig] + config = runner( + [*base, "config", "view", "--minify", "-o", "json"], + capture_output=True, + text=True, + check=True, + timeout=20, + ) + config_doc = json.loads(config.stdout) + context_name = (config_doc.get("contexts") or [{}])[0].get("name") + api_server = ((config_doc.get("clusters") or [{}])[0].get("cluster") or {}).get( + "server" + ) + if context_name != context or not api_server: + raise RuntimeError( + f"kubeconfig did not resolve the requested context {context!r} to an API server" + ) + namespace = runner( + [*base, "get", "namespace", "kube-system", "-o", "json"], + capture_output=True, + text=True, + check=True, + timeout=20, + ) + namespace_uid = (json.loads(namespace.stdout).get("metadata") or {}).get("uid") + if not namespace_uid: + raise RuntimeError("kube-system namespace response is missing metadata.uid") + return {"api_server_url": api_server, "kube_system_namespace_uid": namespace_uid} + + +def prepare_run_state(args: argparse.Namespace, cluster: dict[str, str]) -> dict: + """Validate or persist the run intent before any cluster create/apply call.""" + if not re.fullmatch(r".+@sha256:[0-9a-fA-F]{64}", args.image): + raise RuntimeError("--image must be pinned by a full @sha256 digest") + template_name = f"live-agent-gate-{args.template_version}" + identity = { + "context": args.context, + "cluster_identity": cluster, + "atespace": args.atespace, + "worker_pool": args.worker_pool, + "template_name": template_name, + "image_digest": args.image, + "actor_name": args.actor_name, + } + state = load_state(args.state_file) + if state: + missing = { + "run_id", + "namespace_uid", + "template_uid", + "actor_uid", + } - state.keys() + invalid = set() + if ( + not isinstance(state.get("namespace_uid"), str) + or not state["namespace_uid"] + ): + invalid.add("namespace_uid") + mismatch = { + key: (state.get(key), value) + for key, value in identity.items() + if state.get(key) != value + } + if missing or invalid or mismatch: + details = [] + if missing: + details.append(f"missing identity fields {sorted(missing)}") + if invalid: + details.append(f"empty identity fields {sorted(invalid)}") + if mismatch: + details.append(f"requested identity differs: {mismatch}") + raise RuntimeError( + "state file does not match this run (" + "; ".join(details) + ")" + ) + return state + + state = { + **identity, + "run_id": str(uuid.uuid4()), + "namespace_uid": None, + "template_uid": None, + "actor_uid": None, + } + save_state(args.state_file, state) + return state + + +def create_namespace_and_record_uid( + args: argparse.Namespace, + state: dict, + namespace_doc: str, + *, + runner=subprocess.run, +) -> str: + """Create this run's Namespace and persist the UID returned by the API server.""" + if state.get("namespace_uid"): + raise RuntimeError("run state already has a Namespace UID") + result = runner( + [ + "kubectl", + "--context", + args.context, + "--kubeconfig", + args.kubeconfig, + "create", + "-f", + "-", + "-o", + "json", + ], + input=namespace_doc, + capture_output=True, + text=True, + check=True, + timeout=60, + ) + uid = (json.loads(result.stdout).get("metadata") or {}).get("uid") + if not isinstance(uid, str) or not uid: + raise RuntimeError("created lane Namespace response is missing metadata.uid") + state["namespace_uid"] = uid + save_state(args.state_file, state) + return uid + + +def verify_namespace_uid( + args: argparse.Namespace, state: dict, *, runner=subprocess.run +) -> str: + """Refuse to resume into a missing or replacement lane Namespace.""" + recorded_uid = state.get("namespace_uid") + if not isinstance(recorded_uid, str) or not recorded_uid: + raise RuntimeError("run state has no Namespace UID; refusing to resume") + try: + result = runner( + [ + "kubectl", + "--context", + args.context, + "--kubeconfig", + args.kubeconfig, + "get", + "namespace", + args.atespace, + "-o", + "json", + ], + capture_output=True, + text=True, + check=True, + timeout=20, + ) + except subprocess.CalledProcessError as error: + raise RuntimeError( + f"could not verify lane Namespace UID for {args.atespace}; refusing to resume" + ) from error + uid = (json.loads(result.stdout).get("metadata") or {}).get("uid") + if not isinstance(uid, str) or not uid: + raise RuntimeError("lane Namespace response is missing metadata.uid") + if recorded_uid != uid: + raise RuntimeError( + f"lane Namespace UID changed from {recorded_uid} to {uid}; refusing to adopt it" + ) + return uid + + +def apply_worker_pool( + doc: str, + *, + kubeconfig: str, + context: str, + ko: str, + substrate_src: str | None, + runner=subprocess.run, +) -> None: + """Resolve the WorkerPool's `ko://...` workerImage and apply it (and the Namespace doc + it's paired with) via `ko resolve | kubectl apply`. An unresolved ko:// reference + reaches the pod as an InvalidImageName, not a manifest-time error, so this step must not + be skipped even though `kubectl apply` alone would exit 0.""" + if "ko://" not in doc: + runner( + [ + "kubectl", + "--context", + context, + "--kubeconfig", + kubeconfig, + "apply", + "-f", + "-", + ], + input=doc, + capture_output=True, + text=True, + check=True, + timeout=60, + ) + return + if not substrate_src: + raise RuntimeError( + "--substrate-src is required when the WorkerPool manifest contains ko://" + ) + substrate_src = verify_substrate_source(substrate_src, runner=runner) + ko_docker_repo = os.environ.get("KO_DOCKER_REPO") + if not ko_docker_repo: + raise RuntimeError( + "KO_DOCKER_REPO must be set (e.g. localhost:5001 for kind) to resolve ko:// images" + ) + with tempfile.NamedTemporaryFile("w", suffix=".yaml", delete=False) as f: + f.write(doc) + doc_path = f.name + try: + resolved = ( + runner( # nosec B603 - argv list, ko path is an operator-supplied flag + [ko, "resolve", "-f", doc_path], + env={**os.environ, "KO_DOCKER_REPO": ko_docker_repo}, + capture_output=True, + text=True, + check=True, + timeout=180, + cwd=substrate_src, + ) + ) + runner( # nosec - argv list; kubectl is expected on PATH like git/uv + [ + "kubectl", + "--context", + context, + "--kubeconfig", + kubeconfig, + "apply", + "-f", + "-", + ], + input=resolved.stdout, + capture_output=True, + text=True, + check=True, + timeout=60, + ) + finally: + os.unlink(doc_path) + + +def egress_policy_manifest(args: argparse.Namespace) -> str: + """Return the actor's EgressPolicy as a protojson manifest. + + The argument parser admits exactly one egress mode, so a missing mode never falls through + to a permissive policy; deny-all is a policy with no rules. + """ + if args.egress_deny_all: + rules: list[dict] = [] + elif args.egress_allow_all: + rules = [{"all": {}}] + elif args.egress_hostnames: + rules = [{"hostnames": {"patterns": list(args.egress_hostnames)}}] + elif args.egress_cidr: + rules = [{"cidrs": {"cidrs": [args.egress_cidr]}}] + else: + raise ValueError("no egress mode selected") + return json.dumps({"rules": rules}) + + +def existing_egress_policy_preconditions(args: argparse.Namespace) -> dict[str, str]: + """Read the UID and version required for an update of an existing policy.""" + result = ( + subprocess.run( # nosec B603 - argv list, CLI path is an operator-supplied flag + [ + args.ate_cli, + "--kubeconfig", + args.kubeconfig, + "--context", + args.context, + "get", + "egress-policy", + args.actor_name, + "--atespace", + args.atespace, + "-o", + "json", + ], + capture_output=True, + text=True, + timeout=60, + ) + ) + if result.returncode != 0: + raise RuntimeError( + "get egress-policy failed while preparing an update " + f"(exit {result.returncode}): {result.stderr.strip()[-300:]}" + ) + try: + policy = json.loads(result.stdout) + metadata = policy["metadata"] + uid = metadata["uid"] + version = str(metadata["version"]) + if ( + not isinstance(uid, str) + or not uid + or not version.isdecimal() + or int(version) <= 0 + ): + raise ValueError("metadata.uid and positive metadata.version are required") + except (json.JSONDecodeError, KeyError, TypeError, ValueError) as exc: + raise RuntimeError( + "get egress-policy returned JSON without valid metadata.uid and " + "metadata.version preconditions" + ) from exc + return {"uid": uid, "version": version} + + +def apply_egress_policy(args: argparse.Namespace) -> None: + """Create the actor's EgressPolicy, or replace it when one already exists.""" + manifest = json.loads(egress_policy_manifest(args)) + for verb in ("create", "update"): + if verb == "update": + manifest["metadata"] = existing_egress_policy_preconditions(args) + result = subprocess.run( # nosec B603 - argv list, CLI path is an operator-supplied flag + [ + args.ate_cli, + "--kubeconfig", + args.kubeconfig, + "--context", + args.context, + verb, + "egress-policy", + args.actor_name, + "--atespace", + args.atespace, + "--filename", + "-", + ], + input=json.dumps(manifest), + capture_output=True, + text=True, + timeout=60, + ) + if result.returncode == 0: + print(f"-- egress policy {verb}d") + return + if verb == "create" and "code = AlreadyExists" in result.stderr: + continue + raise RuntimeError( + f"{verb} egress-policy failed (exit {result.returncode}): " + f"{result.stderr.strip()[-300:]}" + ) + + +@contextlib.contextmanager +def actor_router_tunnel(args: argparse.Namespace): + """Forward the Substrate router through the explicitly selected kube context.""" + command = [ + "kubectl", + "--context", + args.context, + "--kubeconfig", + args.kubeconfig, + "port-forward", + "--address", + "127.0.0.1", + "--namespace", + "ate-system", + "service/atenet-router", + f"{args.router_port}:8081", + ] + process = ( + subprocess.Popen( # nosec B603 - fixed kubectl argv, explicit kube context + command, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + text=True, + ) + ) + try: + deadline = time.monotonic() + 20 + while time.monotonic() < deadline: + if process.poll() is not None: + output = process.communicate()[0] + raise RuntimeError(f"router port-forward exited early: {output[-500:]}") + try: + with socket.create_connection( + ("127.0.0.1", args.router_port), timeout=0.2 + ): + break + except OSError: + time.sleep(0.2) + else: + raise RuntimeError("router port-forward did not become ready within 20s") + yield args.router_port + finally: + if process.poll() is None: + process.terminate() + try: + process.wait(timeout=5) + except subprocess.TimeoutExpired: + process.kill() + process.wait(timeout=5) + if process.stdout: + process.stdout.close() + + +def actor_health_check( + *, port: int, atespace: str, actor_name: str, timeout_s: float = 2 +) -> bool: + """Call the actor's non-default shim port through Substrate's HTTP CONNECT route.""" + connection = http.client.HTTPConnection("127.0.0.1", port, timeout=timeout_s) + connection.set_tunnel( + f"actor-upstream:{ACTOR_SHIM_PORT}", + headers={"ate-target-actor": f"{atespace}/{actor_name}"}, + ) + try: + connection.request("GET", "/healthz") + response = connection.getresponse() + response.read() + return response.status == 200 + except (OSError, http.client.HTTPException): + return False + finally: + connection.close() + + +def actor_shim_request( + *, + port: int, + atespace: str, + actor_name: str, + method: str, + path: str, + token: str | None = None, + body: dict | None = None, + timeout_s: float = 5, +) -> int | None: + """Send a request through the actor's CONNECT route without logging its body.""" + connection = http.client.HTTPConnection("127.0.0.1", port, timeout=timeout_s) + connection.set_tunnel( + f"actor-upstream:{ACTOR_SHIM_PORT}", + headers={"ate-target-actor": f"{atespace}/{actor_name}"}, + ) + headers = {} + request_body = None + if body is not None: + request_body = json.dumps(body) + headers["Content-Type"] = "application/json" + if token is not None: + headers["Authorization"] = f"Bearer {token}" + try: + connection.request(method, path, body=request_body, headers=headers) + response = connection.getresponse() + response.read() + return response.status + except (OSError, http.client.HTTPException): + return None + finally: + connection.close() + + +def ensure_shim_token( + *, + args: argparse.Namespace, + state: dict, + port: int, + requester=actor_shim_request, +) -> None: + """Persist a per-actor token before installing it, then verify the auth boundary.""" + token = state.get("shim_token") + if not token: + token = secrets.token_urlsafe(32) + state["shim_token"] = token + save_state(args.state_file, state) + + status = requester( + port=port, + atespace=args.atespace, + actor_name=args.actor_name, + method="POST", + path="/token", + body={"token": token}, + ) + if status == 409: + already_installed = requester( + port=port, + atespace=args.atespace, + actor_name=args.actor_name, + method="GET", + path="/turn/00000000-0000-4000-8000-000000000000", + token=token, + ) + if already_installed != 404: + raise RuntimeError( + "the actor already has a shim token that does not match the private state; " + "manual reconciliation is required" + ) + elif status != 201: + raise RuntimeError( + f"shim token installation failed (HTTP {status or 'no response'})" + ) + + checks = ( + ( + "missing-token /turn/", + "GET", + "/turn/00000000-0000-4000-8000-000000000000", + None, + None, + 401, + ), + ( + "wrong-token /turn/", + "GET", + "/turn/00000000-0000-4000-8000-000000000000", + f"{token}x", + None, + 401, + ), + ( + "authenticated unknown /turn/", + "GET", + "/turn/00000000-0000-4000-8000-000000000000", + token, + None, + 404, + ), + ( + "second /token", + "POST", + "/token", + None, + {"token": "one-time-install-probe"}, + 409, + ), + ("open /healthz", "GET", "/healthz", None, None, 200), + ) + for label, method, path, bearer, request_body, expected in checks: + observed = requester( + port=port, + atespace=args.atespace, + actor_name=args.actor_name, + method=method, + path=path, + token=bearer, + body=request_body, + ) + if observed != expected: + raise RuntimeError( + f"shim token acceptance failed at {label} " + f"(HTTP {observed or 'no response'}, expected {expected})" + ) + print( + "-- per-actor shim token installed; missing/wrong/correct and one-time checks passed" + ) + + +async def ensure_golden_template( + control: SubstrateControl, + args: argparse.Namespace, + template_name: str, + actor_template_doc: str, + state: dict, +) -> str: + existing = await control.get_actor_template(args.atespace, template_name) + if existing is None: + print(f"-- creating actor-template {args.atespace}/{template_name}") + existing = await control.create_actor_template( + args.atespace, template_name, actor_template_doc + ) + if existing.atespace != args.atespace or existing.name != template_name: + raise IdentityConflict( + "actor-template create/read returned a different identity" + ) + if not existing.uid: + raise TransportError("actor-template response is missing metadata.uid") + if state.get("template_uid") and state["template_uid"] != existing.uid: + raise IdentityConflict( + f"actor-template {args.atespace}/{template_name} uid changed from " + f"{state['template_uid']} to {existing.uid}" + ) + containers = existing.raw.get("containers") or [] + template_images = [ + item.get("image") for item in containers if isinstance(item, dict) + ] + if args.image not in template_images: + raise IdentityConflict( + f"actor-template {args.atespace}/{template_name} does not use requested " + "image digest" + ) + state["template_uid"] = existing.uid + save_state(args.state_file, state) + + if existing.golden_state is GoldenState.FAILED: + raise SystemExit( + f"actor-template {args.atespace}/{template_name} already failed its golden " + f"snapshot ({existing.error_message}); ActorTemplates are immutable -- pass a " + "new --template-version rather than reusing this one" + ) + if existing.golden_state is not GoldenState.PENDING: + print( + f"-- actor-template {args.atespace}/{template_name} already exists (state={existing.golden_state.value}), awaiting its golden snapshot" + ) + + record = await wait_for_golden_snapshot( + control, args.atespace, template_name, timeout_s=args.golden_timeout + ) + print(f"-- golden snapshot ready: {record.golden_tag}") + return record.uid or existing.uid + + +async def wait_for_worker_if_actor_is_absent( + control: SubstrateControl, args: argparse.Namespace, state: dict +) -> None: + """Reconcile actor ownership before waiting for capacity needed by a new actor.""" + live = await control.get_actor(args.atespace, args.actor_name) + outcome = reconcile_actor_identity(state.get("actor_uid"), live) + if outcome is IdentityOutcome.UNOWNED: + raise IdentityConflict( + f"actor {args.atespace}/{args.actor_name} already exists with uid {live.uid}, " + f"but {args.state_file} has no actor uid; refusing to adopt it. Use a new " + "--actor-name or reconcile the state file explicitly" + ) + if outcome is IdentityOutcome.DIVERGED: + raise IdentityConflict( + f"actor {args.atespace}/{args.actor_name} exists with uid {live.uid}, but " + f"{args.state_file} recorded {state.get('actor_uid')} from a prior run -- " + "refusing to resume or recreate it; reconcile manually or use a different " + "--actor-name" + ) + if outcome is IdentityOutcome.MATCHES: + if ( + state.get("template_uid") + and live.current_actor_template_uid != state["template_uid"] + ): + raise IdentityConflict( + f"actor {args.atespace}/{args.actor_name} references template uid " + f"{live.current_actor_template_uid!r}, but {args.state_file} recorded " + f"{state['template_uid']!r}" + ) + if live.state in {ActorState.CRASHED, ActorState.DELETING}: + raise ActorFailedToStart( + f"actor {args.atespace}/{args.actor_name} is {live.state.value}; " + "choose an explicit revert or a new --actor-name before retrying" + ) + return + + worker_selector = f"workload={args.worker_pool}" + print( + f"-- waiting for an eligible worker in namespace={args.atespace}, " + f"selector={worker_selector}, sandbox={WORKER_SANDBOX_CLASS}" + ) + await wait_for_eligible_worker( + control, + args.atespace, + worker_selector, + WORKER_SANDBOX_CLASS, + timeout_s=args.worker_timeout, + ) + + +async def ensure_actor( + control: SubstrateControl, + args: argparse.Namespace, + template_name: str, + template_uid: str, + state: dict, +) -> str: + """Reconciles any persisted identity against the cluster's current state before + deciding whether to create or resume, then waits for RUNNING. Returns the actor uid. + """ + persisted_uid = state.get("actor_uid") + live = await control.get_actor(args.atespace, args.actor_name) + outcome = reconcile_actor_identity(persisted_uid, live) + + if outcome is IdentityOutcome.UNOWNED: + raise IdentityConflict( + f"actor {args.atespace}/{args.actor_name} already exists with uid {live.uid}, " + f"but {args.state_file} has no actor uid; refusing to adopt it. Use a new " + "--actor-name or reconcile the state file explicitly" + ) + if outcome is IdentityOutcome.DIVERGED: + raise IdentityConflict( + f"actor {args.atespace}/{args.actor_name} exists with uid {live.uid}, but " + f"{args.state_file} recorded {persisted_uid} from a prior run -- refusing to " + "resume or recreate it; reconcile manually or use a different --actor-name" + ) + + if outcome is IdentityOutcome.ABSENT: + print(f"-- creating actor {args.atespace}/{args.actor_name}") + actor = await control.create_actor( + args.atespace, args.actor_name, template=template_name + ) + if actor.current_actor_template_uid != template_uid: + raise IdentityConflict( + f"created actor {args.atespace}/{args.actor_name} references template uid " + f"{actor.current_actor_template_uid!r}, expected {template_uid!r}" + ) + state["actor_uid"] = actor.uid + save_state(args.state_file, state) + else: + actor = live + if actor.current_actor_template_uid != template_uid: + raise IdentityConflict( + f"actor {args.atespace}/{args.actor_name} references template uid " + f"{actor.current_actor_template_uid!r}, requested {template_uid!r}; " + "refusing to resume it" + ) + if actor.state in {ActorState.CRASHED, ActorState.DELETING}: + raise ActorFailedToStart( + f"actor {args.atespace}/{args.actor_name} is {actor.state.value}; " + "choose an explicit revert or a new --actor-name before retrying" + ) + print( + f"-- actor {args.atespace}/{args.actor_name} already exists (uid matches persisted identity), state={actor.state.value}" + ) + + if actor.state in {ActorState.SUSPENDED, ActorState.PAUSED}: + print(f"-- resuming actor {args.atespace}/{args.actor_name}") + await control.resume_actor(args.atespace, args.actor_name) + + actor = await wait_for_actor_running( + control, args.atespace, args.actor_name, timeout_s=args.actor_timeout + ) + print(f"-- actor RUNNING: uid={actor.uid}") + return actor.uid + + +async def async_main(args: argparse.Namespace) -> None: + if args.worker_image is not None and not re.fullmatch( + r"\S+@sha256:[0-9a-fA-F]{64}", args.worker_image + ): + raise RuntimeError("--worker-image must be pinned by a full sha256 digest") + verify_image_manifest(args.image) + substrate_src = None + if args.worker_image is None: + if not args.substrate_src: + raise RuntimeError( + "pass --worker-image to reuse an existing image, or --substrate-src " + "to resolve the ko:// WorkerPool image" + ) + substrate_src = verify_substrate_source(args.substrate_src) + cluster = get_cluster_identity(context=args.context, kubeconfig=args.kubeconfig) + state = prepare_run_state(args, cluster) + resuming = state.get("namespace_uid") is not None + if resuming: + verify_namespace_uid(args, state) + control = SubstrateControl( + kubeconfig=args.kubeconfig, context=args.context, cli=args.ate_cli + ) + template_name = f"live-agent-gate-{args.template_version}" + + print(f"-- registering atespace {args.atespace}") + await control.ensure_atespace(args.atespace) + + namespace_and_workerpool_doc, actor_template_doc = render_gate_manifest(args) + namespace_doc, worker_pool_doc = namespace_and_workerpool_doc.split("\n---\n", 1) + if args.worker_image is None: + print("-- resolving and applying the WorkerPool") + else: + print("-- applying the WorkerPool with the existing worker image") + if not resuming: + create_namespace_and_record_uid(args, state, namespace_doc + "\n") + apply_worker_pool( + worker_pool_doc, + kubeconfig=args.kubeconfig, + context=args.context, + ko=args.ko, + substrate_src=substrate_src, + ) + + await wait_for_worker_if_actor_is_absent(control, args, state) + template_uid = await ensure_golden_template( + control, args, template_name, actor_template_doc, state + ) + await ensure_actor(control, args, template_name, template_uid, state) + + print("-- installing and checking the per-actor shim token through the actor route") + with actor_router_tunnel(args) as route_port: + ensure_shim_token(args=args, state=state, port=route_port) + print("-- confirming current control-service health through the actor route") + await wait_for_actor_health( + lambda: actor_health_check( + port=route_port, + atespace=args.atespace, + actor_name=args.actor_name, + ), + timeout_s=args.readiness_timeout, + ) + + print("-- applying the actor's EgressPolicy") + apply_egress_policy(args) + + print() + print(f"== actor {args.atespace}/{args.actor_name} is RUNNING, credential-free ==") + print( + "Credential injection and native-agent session launch are separate, still-gated steps" + ) + print("(recovery plan steps 3-4) -- not performed by this script.") + + +def main() -> None: + args = parse_args() + if args.context != "kind-substrate-preview": + raise SystemExit( + f"refusing to target context {args.context!r}: expected the dedicated " + "kind-substrate-preview context" + ) + try: + asyncio.run(async_main(args)) + except (subprocess.CalledProcessError, RuntimeError) as exc: + print(f"gate5_setup failed: {exc}", file=sys.stderr) + sys.exit(1) + + +if __name__ == "__main__": + main() diff --git a/backend/scripts/test_k8s_components.py b/backend/scripts/test_k8s_components.py deleted file mode 100644 index 3911365..0000000 --- a/backend/scripts/test_k8s_components.py +++ /dev/null @@ -1,179 +0,0 @@ -#!/usr/bin/env python3 -""" -Test K8s namespace and job creation directly. - -This tests the K8s integration without running the full DBOS workflow. -Useful for debugging K8s issues. - -Prerequisites: -1. kubectl configured with cluster access -2. RBAC manifests applied (make k8s-apply) - -Usage: - cd backend - uv run python scripts/test_k8s_components.py -""" - -import asyncio -import os -import sys -import uuid - -# Add src to path -sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "src")) - - -async def test_namespace(): - """Test namespace creation and deletion.""" - from mainloop.services.k8s_namespace import ( - copy_secrets_to_namespace, - create_task_namespace, - delete_task_namespace, - setup_worker_rbac, - ) - - task_id = f"test-{uuid.uuid4().hex[:8]}" - - print(f"Testing with task_id: {task_id}") - print() - - # Test 1: Create namespace - print("1. Creating namespace...") - try: - namespace = await create_task_namespace(task_id) - print(f" ✓ Created namespace: {namespace}") - except Exception as e: - print(f" ✗ Failed: {e}") - return - - # Test 2: Copy secrets - print("2. Copying secrets...") - try: - await copy_secrets_to_namespace(task_id, namespace) - print(" ✓ Secrets copied") - except Exception as e: - print(f" ✗ Failed: {e}") - - # Test 3: Setup worker RBAC - print("3. Setting up worker RBAC...") - try: - await setup_worker_rbac(task_id, namespace) - print(" ✓ Worker RBAC configured") - except Exception as e: - print(f" ✗ Failed: {e}") - - # Test 4: Verify namespace exists - print("4. Verifying namespace...") - from mainloop.services.k8s_namespace import get_k8s_client - - core_v1, _ = get_k8s_client() - try: - ns = core_v1.read_namespace(namespace) - print(f" ✓ Namespace exists with status: {ns.status.phase}") - except Exception as e: - print(f" ✗ Failed: {e}") - - # Test 5: List secrets in namespace - print("5. Checking secrets...") - try: - secrets = core_v1.list_namespaced_secret(namespace) - secret_names = [ - s.metadata.name - for s in secrets.items - if not s.metadata.name.startswith("default") - ] - print(f" ✓ Found secrets: {secret_names}") - except Exception as e: - print(f" ✗ Failed: {e}") - - # Test 6: Delete namespace - print("6. Deleting namespace...") - try: - await delete_task_namespace(task_id) - print(" ✓ Deletion initiated") - except Exception as e: - print(f" ✗ Failed: {e}") - - print() - print("Done! Check with: kubectl get ns | grep task-") - - -async def test_job(): - """Test job creation in a namespace.""" - from mainloop.services.k8s_jobs import ( - create_worker_job, - get_job_status, - ) - from mainloop.services.k8s_namespace import ( - copy_secrets_to_namespace, - create_task_namespace, - delete_task_namespace, - setup_worker_rbac, - ) - - task_id = f"test-{uuid.uuid4().hex[:8]}" - - print(f"Testing job with task_id: {task_id}") - print() - - # Setup namespace - print("1. Setting up namespace...") - namespace = await create_task_namespace(task_id) - await copy_secrets_to_namespace(task_id, namespace) - await setup_worker_rbac(task_id, namespace) - print(f" ✓ Namespace ready: {namespace}") - - # Create job - print("2. Creating job...") - try: - job_name = await create_worker_job( - task_id=task_id, - namespace=namespace, - prompt="Echo 'Hello from test job' and exit", - mode="initial", - callback_url="", # No callback for test - ) - print(f" ✓ Created job: {job_name}") - except Exception as e: - print(f" ✗ Failed: {e}") - await delete_task_namespace(task_id) - return - - # Wait a bit and check status - print("3. Waiting for job to start...") - await asyncio.sleep(5) - - status = await get_job_status(task_id, namespace) - if status: - print( - f" Active: {status['active']}, Succeeded: {status['succeeded']}, Failed: {status['failed']}" - ) - else: - print(" No status yet") - - # Show how to watch - print() - print("Watch the job with:") - print(f" kubectl get pods -n {namespace} -w") - print(f" kubectl logs -n {namespace} -l mainloop.dev/task-id={task_id} -f") - print() - print("Cleanup with:") - print(f" kubectl delete ns {namespace}") - - -async def main(): - """Parse arguments and run tests.""" - import argparse - - parser = argparse.ArgumentParser(description="Test K8s components") - parser.add_argument("--job", action="store_true", help="Test job creation (slower)") - args = parser.parse_args() - - if args.job: - await test_job() - else: - await test_namespace() - - -if __name__ == "__main__": - asyncio.run(main()) diff --git a/backend/src/mainloop/api.py b/backend/src/mainloop/api.py index d1b6e6c..64382b7 100644 --- a/backend/src/mainloop/api.py +++ b/backend/src/mainloop/api.py @@ -3,7 +3,6 @@ import logging from dataclasses import asdict from datetime import datetime -from typing import Any from dbos import DBOS from fastapi import FastAPI, Header, HTTPException, Request @@ -17,7 +16,11 @@ ConversationResponse, ) from mainloop.runtime.agent_api import router as agent_api_router -from mainloop.services.chat_handler import process_message +from mainloop.runtime.credential_reauth_api import ( + router as credential_reauth_api_router, +) +from mainloop.runtime.preview_proxy import register_preview_proxy +from mainloop.runtime.workspace_api import router as workspace_api_router from mainloop.services.github_pr import ( CommitSummary, ProjectPRSummary, @@ -116,19 +119,22 @@ async def startup_event(): # Launch DBOS DBOS.launch() - if settings.main_thread_mode == "native": - import asyncio + import asyncio - from mainloop.runtime import native_sessions + from mainloop.runtime import native_sessions - app.state.native_reconcile = asyncio.create_task( - native_sessions.reconcile_loop() - ) + app.state.native_reconcile = asyncio.create_task(native_sessions.reconcile_loop()) @app.on_event("shutdown") async def shutdown_event(): """Clean up on shutdown.""" + import asyncio + + task = getattr(app.state, "native_reconcile", None) + if task is not None: + task.cancel() + await asyncio.gather(task, return_exceptions=True) await db.disconnect() @@ -221,91 +227,14 @@ async def chat( request: ChatRequest, user_id: str = Header(alias="X-User-ID", default=None), ): - """Send a message and get an immediate response.""" - from mainloop.services.compaction import trigger_compaction - + """Record and deliver a message to the user's native main session.""" if not user_id: user_id = get_user_id_from_cf_header() - - if settings.main_thread_mode == "native": - return await _chat_native(request, user_id) - - # Ensure main thread is running (for background coordination) - main_thread_id = get_or_start_main_thread(user_id) - - # Get or create conversation - if request.conversation_id: - conversation = await db.get_conversation(request.conversation_id) - if not conversation: - raise HTTPException(status_code=404, detail="Conversation not found") - else: - conversation = await db.create_conversation(user_id) - - # Load context: summary + recent messages after last summarized point - recent_messages = await db.get_messages_after( - conversation.id, - conversation.summarized_through_id, - limit=20, - ) - - # Save user message and increment count - await db.create_message( - conversation_id=conversation.id, - role="user", - content=request.message, - ) - await db.increment_message_count(conversation.id) - - # Get or create main thread record - main_thread = await db.get_main_thread_by_user(user_id) - if not main_thread: - # Create main thread record if it doesn't exist (e.g., after DB reset) - from models import MainThread - - main_thread = MainThread(user_id=user_id, workflow_run_id=main_thread_id) - main_thread = await db.create_main_thread(main_thread) - thread_id = main_thread.id - - # Process message with summary + recent messages for context - result = await process_message( - user_id=user_id, - message=request.message, - conversation_id=conversation.id, - main_thread_id=thread_id, - summary=conversation.summary, - recent_messages=recent_messages, - ) - - # If a session was spawned, don't save a main thread response - # The user interacts with the session directly - if result.suppress_response and result.spawned_session_ids: - # Return the first spawned session info instead of a message - # Frontend will auto-switch to this session - return ChatResponse( - conversation_id=conversation.id, - message=None, - spawned_session_id=result.spawned_session_ids[0], - ) - - # Save assistant response and increment count - assistant_message = await db.create_message( - conversation_id=conversation.id, - role="assistant", - content=result.response, - ) - new_count = await db.increment_message_count(conversation.id) - - # Trigger async compaction if needed (fire-and-forget) - trigger_compaction(conversation.id, new_count) - - return ChatResponse( - conversation_id=conversation.id, - message=assistant_message, - ) + return await _chat_native(request, user_id) async def _chat_native(request: ChatRequest, user_id: str) -> ChatResponse: - """Native main thread: record + deliver to the Claude session under Herdr (ledgered). The + """Record and deliver to the native main session through the Substrate workspace. The reply is mirrored from the native journal, so the client polls the conversation.""" from mainloop.runtime import delegation, native_sessions @@ -337,8 +266,6 @@ async def get_main_thread_info(user_id: str = Header(alias="X-User-ID", default= """Main-thread mode, native identity strip, and the topic index.""" if not user_id: user_id = get_user_id_from_cf_header() - if settings.main_thread_mode != "native": - return MainThreadInfo(mode=settings.main_thread_mode) from mainloop.runtime import delegation, native_sessions binding = await delegation.ensure_main_session(user_id) @@ -395,6 +322,9 @@ async def list_topics(user_id: str = Header(alias="X-User-ID", default=None)): app.include_router(agent_api_router) +app.include_router(workspace_api_router) +app.include_router(credential_reauth_api_router) +register_preview_proxy(app) # ============= Conversation Endpoints ============= @@ -422,19 +352,16 @@ async def get_conversation(conversation_id: str): if not conversation: raise HTTPException(status_code=404, detail="Conversation not found") - if settings.main_thread_mode == "native": - from mainloop.runtime import native_sessions + from mainloop.runtime import native_sessions - async with db.connection() as conn: - main_sid = await conn.fetchval( - """SELECT b.session_id FROM native_bindings b JOIN sessions s ON s.id=b.session_id - WHERE b.role='main' AND s.conversation_id=$1""", - conversation_id, - ) - if main_sid and not native_sessions.is_rotating(main_sid): - await native_sessions.sync( - main_sid - ) # mirror new native-journal evidence first + async with db.connection() as conn: + main_sid = await conn.fetchval( + """SELECT b.session_id FROM native_bindings b JOIN sessions s ON s.id=b.session_id + WHERE b.role='main' AND s.conversation_id=$1""", + conversation_id, + ) + if main_sid and not native_sessions.is_rotating(main_sid): + await native_sessions.sync(main_sid) messages = await db.get_messages(conversation_id) return ConversationResponse( @@ -725,10 +652,6 @@ async def create_session( """Create a new session with its own conversation.""" import uuid - from dbos import SetWorkflowID - from mainloop.workflows.dbos_config import worker_queue - from mainloop.workflows.session_worker import session_worker_workflow - if not user_id: user_id = get_user_id_from_cf_header() @@ -760,20 +683,12 @@ async def create_session( ) session = await db.create_session(session) - if request.agent_kind: - # Real native agent under Herdr in the workspace pod (no DBOS worker / K8s Job). - from mainloop.runtime import native_sessions - - await native_sessions.create_binding(session.id, request.agent_kind) - await db.update_session(session.id, status=SessionStatus.ACTIVE) - await native_sessions.submit_message(session.id, request.prompt) - return await db.get_session(session.id) - - # Start session worker workflow - with SetWorkflowID(session.id): - worker_queue.enqueue(session_worker_workflow, session.id) + from mainloop.runtime import native_sessions - return session + await native_sessions.create_binding(session.id, request.agent_kind or "claude") + await db.update_session(session.id, status=SessionStatus.ACTIVE) + await native_sessions.submit_message(session.id, request.prompt) + return await db.get_session(session.id) @app.get("/sessions/{session_id}", response_model=Session) @@ -817,7 +732,7 @@ async def get_session_conversation(session_id: str): async def get_session_native( session_id: str, user_id: str = Header(alias="X-User-ID", default=None) ): - """Identity strip for a session bound to a native agent under Herdr.""" + """Identity strip for a session bound to a native agent in Substrate.""" if not user_id: user_id = get_user_id_from_cf_header() owner = await db.get_session(session_id) @@ -846,7 +761,6 @@ async def send_session_message( user_id: str = Header(alias="X-User-ID", default=None), ): """Send a message to a session's conversation.""" - from mainloop.workflows.session_worker import TOPIC_USER_MESSAGE if not user_id: user_id = get_user_id_from_cf_header() @@ -860,32 +774,15 @@ async def send_session_message( from mainloop.runtime import native_sessions - if await native_sessions.get_binding(session_id): - try: - message_id = await native_sessions.submit_message( - session_id, request.message - ) - except ValueError as exc: - raise HTTPException(status_code=409, detail=str(exc)) from exc - return {"status": "ok", "message_id": message_id} - - # Save message directly to database (don't rely on workflow) - message = await db.create_message( - conversation_id=session.conversation_id, - role="user", - content=request.message, - ) - - # If session is waiting on user, notify workflow to process response - if session.status == SessionStatus.WAITING_ON_USER: - # Notify the workflow that a new message is ready - DBOS.send( - session_id, # workflow_id is the session_id - {"message_id": message.id}, # Just notify, message already saved - topic=TOPIC_USER_MESSAGE, + if not await native_sessions.get_binding(session_id): + raise HTTPException( + status_code=409, detail="Session has no native agent binding" ) - - return {"status": "ok", "message_id": message.id} + try: + message_id = await native_sessions.submit_message(session_id, request.message) + except ValueError as exc: + raise HTTPException(status_code=409, detail=str(exc)) from exc + return {"status": "ok", "message_id": message_id} # Done: nothing more will run, so the session can be cleared from the list. @@ -915,15 +812,14 @@ async def cancel_session( from mainloop.runtime import native_sessions - if await native_sessions.get_binding(session_id): - try: - agent = await native_sessions.cancel(session_id) - except ValueError as exc: - raise HTTPException(status_code=409, detail=str(exc)) from exc - else: - # TODO: Cancel the legacy session worker workflow - await db.update_session(session_id, status=SessionStatus.CANCELLED) - agent = "not_running" + if not await native_sessions.get_binding(session_id): + raise HTTPException( + status_code=409, detail="Session has no native agent binding" + ) + try: + agent = await native_sessions.cancel(session_id) + except ValueError as exc: + raise HTTPException(status_code=409, detail=str(exc)) from exc return {"status": "cancelled", "agent": agent} @@ -997,51 +893,6 @@ async def dismiss_notification( return {"status": "ok"} -# ============= Internal Endpoints (for K8s Jobs) ============= - - -class SessionResult(BaseModel): - """Result from a session Job.""" - - session_id: str - status: str # "completed" or "failed" - result: dict[str, Any] | None = None - error: str | None = None - completed_at: str | None = None - - -@app.post("/internal/sessions/{session_id}/complete") -async def session_complete(session_id: str, result: SessionResult): - """Handle K8s Job completion callbacks for sessions. - - This is called by the job_runner when a session Job finishes. - It notifies the session workflow to add the response and continue. - """ - # Verify session exists - session = await db.get_session(session_id) - if not session: - raise HTTPException(status_code=404, detail="Session not found") - - # Send result to the session workflow via DBOS.send() - # The session workflow is waiting on TOPIC_JOB_RESULT - from mainloop.workflows.session_worker import TOPIC_JOB_RESULT - - # The session workflow uses session_id as workflow ID - workflow_id = session_id - - DBOS.send( - workflow_id, - { - "status": result.status, - "result": result.result, - "error": result.error, - }, - topic=TOPIC_JOB_RESULT, - ) - - return {"status": "ok", "session_id": session_id} - - # ============= Test Helpers (E2E only) ============= @@ -1153,8 +1004,6 @@ async def reset_test_data(all: bool = False): status_code=403, detail="Only available in test environment" ) - deleted_namespaces = [] - if all: # Full reset - truncate everything async with db.connection() as conn: @@ -1170,25 +1019,6 @@ async def reset_test_data(all: bool = False): "TRUNCATE TABLE dbos.workflow_events, dbos.operation_outputs, dbos.workflow_status CASCADE" ) - # Delete ALL task namespaces - try: - from kubernetes.client.rest import ApiException - from mainloop.services.k8s_namespace import get_k8s_client - - core_v1, _ = get_k8s_client() - namespaces = core_v1.list_namespace( - label_selector="app.kubernetes.io/managed-by=mainloop" - ) - for ns in namespaces.items: - try: - core_v1.delete_namespace(name=ns.metadata.name) - deleted_namespaces.append(ns.metadata.name) - except ApiException as e: - if e.status != 404: - logger.warning(f"Failed to delete namespace: {e}") - except Exception as e: - logger.debug(f"K8s namespace cleanup skipped: {e}") - if settings.use_mock_github: from mainloop.services.github_mock import mock_state @@ -1197,17 +1027,11 @@ async def reset_test_data(all: bool = False): return { "status": "reset", "scope": "all", - "deleted_namespaces": deleted_namespaces, } # Test-only reset async with db.connection() as conn: - # Get session IDs and workflow IDs for test users before deleting - test_sessions = await conn.fetch( - "SELECT id FROM sessions WHERE user_id LIKE 'test-%'" - ) - test_session_ids = [row["id"] for row in test_sessions] - + # Get workflow IDs for test users before deleting. test_workflow_ids = await conn.fetch( """ SELECT workflow_run_id FROM main_threads @@ -1254,31 +1078,6 @@ async def reset_test_data(all: bool = False): workflow_ids, ) - # Delete K8s namespaces for test sessions - if test_session_ids: - try: - from kubernetes.client.rest import ApiException - from mainloop.services.k8s_namespace import get_k8s_client - - core_v1, _ = get_k8s_client() - - # Get all mainloop-managed namespaces - namespaces = core_v1.list_namespace( - label_selector="app.kubernetes.io/managed-by=mainloop" - ) - - for ns in namespaces.items: - session_id = ns.metadata.labels.get("mainloop.dev/session-id", "") - if session_id in test_session_ids: - try: - core_v1.delete_namespace(name=ns.metadata.name) - deleted_namespaces.append(ns.metadata.name) - except ApiException as e: - if e.status != 404: # Ignore not found - logger.warning(f"Failed to delete namespace: {e}") - except Exception as e: - logger.debug(f"K8s namespace cleanup skipped: {e}") - # Reset mock state if mocking is enabled if settings.use_mock_github: from mainloop.services.github_mock import mock_state @@ -1288,7 +1087,6 @@ async def reset_test_data(all: bool = False): return { "status": "reset", "preserved": "non-test users", - "deleted_namespaces": deleted_namespaces, } diff --git a/backend/src/mainloop/claude_agent.py b/backend/src/mainloop/claude_agent.py deleted file mode 100644 index 6fbe77e..0000000 --- a/backend/src/mainloop/claude_agent.py +++ /dev/null @@ -1,69 +0,0 @@ -"""Claude agent orchestration using the Agent SDK.""" - -from claude_agent_sdk import ( - AssistantMessage, - ClaudeAgentOptions, - ResultMessage, - TextBlock, - query, -) -from mainloop.config import settings - -from models import AgentResponse, AgentTask - - -class ClaudeAgent: - """Client for interacting with Claude Code via the Agent SDK.""" - - def __init__(self): - """Initialize Claude agent client.""" - pass - - async def execute_task(self, task: AgentTask) -> AgentResponse: - """Execute a task via Claude Code Agent SDK.""" - try: - options = ClaudeAgentOptions( - model=settings.claude_model, - permission_mode="bypassPermissions", - cwd=settings.claude_workspace, - ) - - collected_text: list[str] = [] - - async for message in query(prompt=task.prompt, options=options): - if isinstance(message, AssistantMessage): - for block in message.content: - if isinstance(block, TextBlock): - collected_text.append(block.text) - elif isinstance(message, ResultMessage): - if message.is_error: - return AgentResponse( - task_id=task.id, - content=f"Error: {message.result or 'Unknown error'}", - ) - - return AgentResponse( - task_id=task.id, - content="\n".join(collected_text) if collected_text else "No response", - ) - - except Exception as e: - return AgentResponse(task_id=task.id, content=f"Claude SDK error: {str(e)}") - - async def stream_task(self, task: AgentTask): - """Stream task execution results.""" - options = ClaudeAgentOptions( - model=settings.claude_model, - permission_mode="bypassPermissions", - cwd=settings.claude_workspace, - ) - - async for message in query(prompt=task.prompt, options=options): - if isinstance(message, AssistantMessage): - for block in message.content: - if isinstance(block, TextBlock): - yield block.text - - -# Global agent instance -claude_agent = ClaudeAgent() diff --git a/backend/src/mainloop/config.py b/backend/src/mainloop/config.py index 297e847..696f67a 100644 --- a/backend/src/mainloop/config.py +++ b/backend/src/mainloop/config.py @@ -1,11 +1,23 @@ """Configuration management.""" +import hashlib +from typing import Literal from urllib.parse import quote_plus -from pydantic import computed_field +from pydantic import BaseModel, ConfigDict, Field, computed_field from pydantic_settings import BaseSettings, SettingsConfigDict +class SubstrateActorBinding(BaseModel): + """Deployment-provided route and token Secret for one pre-created actor.""" + + atespace: str + actor: str + shim_token_secret_name: str + + model_config = ConfigDict(extra="forbid", frozen=True) + + class Settings(BaseSettings): """Application settings.""" @@ -23,22 +35,49 @@ def database_url(self) -> str: encoded_password = quote_plus(self.db_password) return f"postgresql://{self.db_user}:{encoded_password}@{self.db_host}:{self.db_port}/{self.db_name}" - # Claude - claude_code_oauth_token: str = "" # OAuth token for Claude Code API - claude_agent_url: str = "http://claude-agent:8001" - claude_workspace: str = "/workspace" - claude_model: str = "sonnet" # Main thread model - claude_worker_model: str = "opus" # Worker model (for background tasks) - - # Native agents under Herdr (workspace pod reached over Kubernetes pod-exec) - workspace_namespace: str = "herdr-spike" - workspace_pod: str = "workspace-0" - main_pod: str = ( - "main-0" # pod that runs the native main thread (scratch cwd, no repo) + # Native sessions connect to pre-created Substrate actors through the CONNECT router. + substrate_router_address: str = ( + "http://atenet-router.ate-system.svc.cluster.local:8081" ) - - # Native main thread (context model). MAIN_THREAD_MODE=native replaces the SDK chat path. - main_thread_mode: str = "sdk" # sdk | native + substrate_shim_secret_namespace: str = "mainloop-shim-secrets" + substrate_credential_secret_namespace: str = "mainloop-control" + substrate_credential_secret_prefix: str = "mainloop-credential" + substrate_credential_account: str = "owner" + substrate_credential_owner_user_id: str = "local-dev-user" + substrate_codex_auth_path: str = "" + substrate_claude_token_path: str = "" + substrate_shim_secret_prefix: str = Field( + default="mainloop-shim", + min_length=1, + max_length=40, + pattern=r"^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$", + ) + substrate_actor_bindings: dict[ + Literal["claude", "codex"], SubstrateActorBinding + ] = Field(default_factory=dict) + substrate_resume_timeout_seconds: float = 120.0 + + def shim_token_secret_name(self, atespace: str, actor: str) -> str: + """Return the configured, stable Kubernetes Secret name for an actor shim token.""" + suffix = hashlib.sha256(f"{atespace}/{actor}".encode()).hexdigest()[:16] + return f"{self.substrate_shim_secret_prefix}-{suffix}" + + # Substrate actor lifecycle control. Empty kubeconfig/context falls back to ambient config. + substrate_kubeconfig: str = "" + substrate_context: str = "" + substrate_endpoint: str = "" + substrate_token_file: str = "" + substrate_atespace: str = "mainloop-workspaces" + substrate_actor_template: str = "mainloop-workspace" + substrate_cli: str = "kubectl-ate" + substrate_preview_base_url: str = "http://preview.localhost:8001" + substrate_preview_connect_timeout_seconds: float = 5.0 + substrate_reauth_job_image: str = "" + substrate_reauth_job_namespace: str = "mainloop-control" + substrate_reauth_callback_url: str = "http://mainloop-backend:8000/internal/reauth" + substrate_reauth_timeout_seconds: int = 1800 + + # Native main thread (context model). main_thread_model: str = "sonnet" main_thread_effort: str = "medium" # Rotation: cut to a fresh native session when the context grew by this many tokens above @@ -65,15 +104,6 @@ def frontend_origin(self) -> str: """Construct frontend origin URL from domain.""" return f"https://{self.frontend_domain}" - # K8s Job callback URL (internal service URL for Jobs to call back) - backend_internal_url: str = ( - "http://mainloop-backend.mainloop.svc.cluster.local:8000" - ) - - # Worker image for K8s Jobs (use local image for dev) - worker_image: str = "ghcr.io/oldsj/mainloop-agent-controller:latest" - worker_image_pull_policy: str = "IfNotPresent" # Use "Never" for local dev - # Test environment flag (enables test-only endpoints) is_test_env: bool = False diff --git a/backend/src/mainloop/db/postgres.py b/backend/src/mainloop/db/postgres.py index 47a6405..c6b8f55 100644 --- a/backend/src/mainloop/db/postgres.py +++ b/backend/src/mainloop/db/postgres.py @@ -163,7 +163,7 @@ def _parse_json_field(value: Any) -> list | dict | None: CREATE INDEX IF NOT EXISTS idx_sessions_project ON sessions(project_id); CREATE INDEX IF NOT EXISTS idx_sessions_anchor ON sessions(anchor_message_id); --- Native agent bindings (one per session bound to a real agent under Herdr) +-- Native agent bindings (one per session bound to a real agent in a Substrate workspace) CREATE TABLE IF NOT EXISTS native_bindings ( session_id TEXT PRIMARY KEY REFERENCES sessions(id), kind TEXT NOT NULL, @@ -171,10 +171,6 @@ def _parse_json_field(value: Any) -> list | dict | None: native_session_id TEXT, approval_policy TEXT NOT NULL, model TEXT, - herdr_pane_id TEXT, - herdr_terminal_id TEXT, - herdr_workspace_id TEXT, - pod_uid TEXT, generation INTEGER NOT NULL DEFAULT 1, journal_cursor INTEGER NOT NULL DEFAULT 0, journal_ref TEXT, @@ -197,7 +193,6 @@ def _parse_json_field(value: Any) -> list | dict | None: -- Context model (main thread window, session tree, topics). Additive to the r6 tables. ALTER TABLE native_bindings ADD COLUMN IF NOT EXISTS role TEXT NOT NULL DEFAULT 'agent'; -ALTER TABLE native_bindings ADD COLUMN IF NOT EXISTS pod TEXT; ALTER TABLE native_bindings ADD COLUMN IF NOT EXISTS parent_session_id TEXT; ALTER TABLE native_bindings ADD COLUMN IF NOT EXISTS topic_id TEXT; ALTER TABLE native_bindings ADD COLUMN IF NOT EXISTS token_hash TEXT; @@ -223,6 +218,52 @@ def _parse_json_field(value: Any) -> list | dict | None: CREATE UNIQUE INDEX IF NOT EXISTS idx_native_bindings_token ON native_bindings(token_hash) WHERE token_hash IS NOT NULL; CREATE INDEX IF NOT EXISTS idx_native_bindings_parent ON native_bindings(parent_session_id); +-- Substrate workspace-runtime adapter: durable mapping from a Mainloop session to a Substrate +-- actor. Separate from native_bindings (native-agent session identity) because a +-- session's workspace runtime is a distinct concept -- see ROADMAP.md "Workspace platform". +-- One actor per session (workspace_id = session_id) replaces the fixed workspace pod for +-- Substrate-backed sessions. ownership_generation fences resume/suspend/revert the same way +-- native_bindings.generation fences native sends: a stale caller's mutation is rejected, and a +-- retry re-inspects the actor and this row rather than creating a second one. +CREATE TABLE IF NOT EXISTS workspace_bindings ( + workspace_id TEXT PRIMARY KEY REFERENCES sessions(id), + provider TEXT NOT NULL DEFAULT 'substrate', + atespace TEXT NOT NULL, + actor_name TEXT NOT NULL, + actor_template TEXT NOT NULL, + shim_token_secret_name TEXT, + native_session_id TEXT, + preview_route TEXT, + runtime_endpoint TEXT, + observed_state TEXT NOT NULL DEFAULT 'unknown', + observed_at TIMESTAMPTZ, + external_snapshot_uri TEXT, + ownership_generation INTEGER NOT NULL DEFAULT 1, + desired_state TEXT NOT NULL DEFAULT 'active', + last_error TEXT, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + UNIQUE (atespace, actor_name) +); + +-- Workspace lifecycle belongs to the workspace, separate from task/session, agent and delivery +-- state. The manifest is declarative intent; only actor observations establish runtime state. +CREATE TABLE IF NOT EXISTS workspace_lifecycles ( + workspace_id TEXT PRIMARY KEY REFERENCES workspace_bindings(workspace_id) ON DELETE CASCADE, + desired_state TEXT NOT NULL DEFAULT 'running', + observed_state TEXT NOT NULL DEFAULT 'unknown', + manifest JSONB NOT NULL, + conditions JSONB NOT NULL DEFAULT '[]'::jsonb, + last_transition JSONB, + operation_id TEXT, + snapshot_ref TEXT, + last_activity_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); +ALTER TABLE workspace_bindings ADD COLUMN IF NOT EXISTS shim_token_secret_name TEXT; +ALTER TABLE workspace_lifecycles ADD COLUMN IF NOT EXISTS last_activity_at TIMESTAMPTZ NOT NULL DEFAULT NOW(); + -- Topics are durable records (not sessions). Supervisors (next slice) attach to a topic. CREATE TABLE IF NOT EXISTS topics ( id TEXT PRIMARY KEY, @@ -906,7 +947,7 @@ async def list_queue_items( query = f""" SELECT * FROM queue_items - WHERE {' AND '.join(conditions)} + WHERE {" AND ".join(conditions)} ORDER BY CASE priority WHEN 'urgent' THEN 1 @@ -1142,9 +1183,9 @@ def _row_to_conversation(self, row: asyncpg.Record) -> Conversation: ) async def create_message( - self, conversation_id: str, role: str, content: str + self, conversation_id: str, role: str, content: str, *, conn: Any | None = None ) -> Message: - """Create a new message in a conversation.""" + """Create a new message, optionally inside a caller-owned transaction.""" import uuid message = Message( @@ -1154,10 +1195,11 @@ async def create_message( content=content, created_at=datetime.now(timezone.utc), ) - if not self._pool: + if not self._pool and conn is None: return message - async with self.connection() as conn: - await conn.execute( + + async def insert(connection) -> None: + await connection.execute( """ INSERT INTO messages (id, conversation_id, role, content, created_at) VALUES ($1, $2, $3, $4, $5) @@ -1169,11 +1211,17 @@ async def create_message( message.created_at, ) # Update conversation's updated_at - await conn.execute( + await connection.execute( "UPDATE conversations SET updated_at = $1 WHERE id = $2", datetime.now(timezone.utc), conversation_id, ) + + if conn is not None: + await insert(conn) + else: + async with self.connection() as connection: + await insert(connection) return message async def get_messages(self, conversation_id: str) -> list[Message]: diff --git a/backend/src/mainloop/runtime/actor_provisioner.py b/backend/src/mainloop/runtime/actor_provisioner.py new file mode 100644 index 0000000..dbec165 --- /dev/null +++ b/backend/src/mainloop/runtime/actor_provisioner.py @@ -0,0 +1,163 @@ +"""Provision one branch workspace actor and its control-plane shim credential.""" + +from __future__ import annotations + +import asyncio +import base64 +import secrets +from dataclasses import dataclass +from typing import Protocol + +from kubernetes import client, config +from kubernetes.client.rest import ApiException +from mainloop.config import settings +from mainloop.runtime.substrate import ActorRecord, SubstrateControl + + +@dataclass(frozen=True, slots=True) +class ProvisionedActor: + actor: ActorRecord + shim_token_secret_name: str + + +class ActorProvisioner(Protocol): + async def create( + self, + *, + atespace: str, + actor_name: str, + template: str, + shim_token_secret_name: str, + ) -> ProvisionedActor: ... + + async def delete( + self, *, atespace: str, actor_name: str, shim_token_secret_name: str | None + ) -> None: ... + + +class SubstrateActorProvisioner: + """Uses the existing Substrate control adapter and a namespaced Secret.""" + + def __init__(self, control: SubstrateControl | None = None, core_api=None): + self.control = control or SubstrateControl() + if core_api is not None: + self.core_api = core_api + else: + try: + config.load_incluster_config() + except config.ConfigException: + config.load_kube_config( + config_file=settings.substrate_kubeconfig or None, + context=settings.substrate_context or None, + ) + self.core_api = client.CoreV1Api() + + async def create( + self, + *, + atespace: str, + actor_name: str, + template: str, + shim_token_secret_name: str, + ) -> ProvisionedActor: + token = secrets.token_urlsafe(32) + try: + await asyncio.to_thread( + self.core_api.create_namespaced_secret, + settings.substrate_shim_secret_namespace, + client.V1Secret( + metadata=client.V1ObjectMeta(name=shim_token_secret_name), + type="Opaque", + data={"token": base64.b64encode(token.encode()).decode()}, + ), + ) + except ApiException as exc: + if exc.status != 409: + raise RuntimeError( + f"workspace shim Secret creation failed (status {exc.status})" + ) from exc + + # An existing actor after a timeout belongs to this persisted binding. Inspect first; + # never issue a second create for an uncertain outcome. + actor = await self.control.get_actor(atespace, actor_name) + if actor is None: + actor = await self.control.create_actor( + atespace, actor_name, template=template + ) + return ProvisionedActor( + actor=actor, shim_token_secret_name=shim_token_secret_name + ) + + async def delete( + self, *, atespace: str, actor_name: str, shim_token_secret_name: str | None + ) -> None: + await self.control.delete_actor(atespace, actor_name, any_state=True) + if shim_token_secret_name: + try: + await asyncio.to_thread( + self.core_api.delete_namespaced_secret, + shim_token_secret_name, + settings.substrate_shim_secret_namespace, + ) + except ApiException as exc: + if exc.status != 404: + raise RuntimeError( + f"workspace shim Secret deletion failed (status {exc.status})" + ) from exc + + +class FakeActorProvisioner: + """In-memory provisioner for tests; it never accesses Kubernetes or Substrate.""" + + def __init__(self): + self.actors: dict[tuple[str, str], ActorRecord] = {} + self.secrets: set[str] = set() + + async def create( + self, + *, + atespace: str, + actor_name: str, + template: str, + shim_token_secret_name: str, + ) -> ProvisionedActor: + from mainloop.runtime.substrate import ActorState + + key = (atespace, actor_name) + actor = self.actors.get(key) or ActorRecord( + atespace=atespace, + name=actor_name, + uid=f"fake-{actor_name}", + state=ActorState.RUNNING, + external_snapshot_uri=None, + current_actor_template_uid=template, + raw={}, + ) + self.actors[key] = actor + self.secrets.add(shim_token_secret_name) + return ProvisionedActor( + actor=actor, shim_token_secret_name=shim_token_secret_name + ) + + async def delete( + self, *, atespace: str, actor_name: str, shim_token_secret_name: str | None + ) -> None: + self.actors.pop((atespace, actor_name), None) + if shim_token_secret_name: + self.secrets.discard(shim_token_secret_name) + + +_provisioner: ActorProvisioner | None = None + + +def get_actor_provisioner() -> ActorProvisioner: + global _provisioner + if _provisioner is None: + _provisioner = SubstrateActorProvisioner() + return _provisioner + + +def set_actor_provisioner(provisioner: ActorProvisioner | None) -> None: + """Replace the provisioner in tests or during application assembly.""" + global _provisioner + _provisioner = provisioner diff --git a/backend/src/mainloop/runtime/claude.py b/backend/src/mainloop/runtime/claude.py index 5d057dd..72b350e 100644 --- a/backend/src/mainloop/runtime/claude.py +++ b/backend/src/mainloop/runtime/claude.py @@ -1,7 +1,6 @@ """Fixture-only normalization for the native Claude Code stream boundary. -This module deliberately does not import ``claude_agent_sdk`` or start a Claude -process. It accepts validated, JSON-shaped observations from a native Claude +It accepts validated, JSON-shaped observations from a native Claude session and maps the observable parts to the provider-neutral runtime contract. The fixture envelope supplies the source cursor and raw-evidence reference; neither is synthesized from a process identity or a transcript message. @@ -398,8 +397,6 @@ def binding_from_init( *, binding_id: str, workspace_id: str, - herdr_session_id: str, - herdr_agent_id: str, creation_mode: Literal["created", "attached", "discovered"] = "created", ownership_generation: int = 1, ) -> NativeBinding: @@ -417,8 +414,6 @@ def binding_from_init( "provider": CLAUDE_PROVIDER, "runtime_type": "claude-native-cli", "native_session_id": native_session_id, - "herdr_session_id": herdr_session_id, - "herdr_agent_id": herdr_agent_id, "creation_mode": creation_mode, "ownership_generation": ownership_generation, "observed": _extension(event), diff --git a/backend/src/mainloop/runtime/credential_broker.py b/backend/src/mainloop/runtime/credential_broker.py new file mode 100644 index 0000000..bb46826 --- /dev/null +++ b/backend/src/mainloop/runtime/credential_broker.py @@ -0,0 +1,432 @@ +"""Control-plane storage for native-agent credentials. + +The broker stores owner credentials in Kubernetes Secrets and publishes only the value used by +the egress credential provider. Codex refresh-token exchange is deliberately not implemented: +the checked-in design records the endpoint but has no evidenced request payload or rotation +handling. +""" + +from __future__ import annotations + +import asyncio +import base64 +import json +import re +from dataclasses import dataclass +from datetime import UTC, datetime +from pathlib import Path +from typing import Mapping, Protocol + +from kubernetes import client, config +from kubernetes.client.rest import ApiException +from mainloop.config import settings + +_PROVIDERS = {"codex", "claude"} +_JWT_EXPIRY_MARGIN_SECONDS = 5 * 60 +_MAX_CODEX_AUTH_BYTES = 256 * 1024 +_MAX_CLAUDE_TOKEN_BYTES = 16 * 1024 +_DNS_LABEL = re.compile(r"^[a-z0-9](?:[-a-z0-9]{0,61}[a-z0-9])?$") +_SEED_LOCKS: dict[tuple[int, str, str], asyncio.Lock] = {} + + +class CredentialBrokerError(RuntimeError): + """Credential state could not be read or safely published.""" + + +class CredentialNeedsSignin(CredentialBrokerError): + """A provider credential is absent, expiring or rejected.""" + + def __init__(self, provider: str): + if provider not in _PROVIDERS: + raise ValueError("unsupported credential provider") + self.provider = provider + super().__init__(f"{provider.title()} needs sign-in") + + +class CredentialSecretMissing(CredentialBrokerError): + """The Secret must be created through GitOps before it can be seeded.""" + + +@dataclass(frozen=True, slots=True) +class CredentialStatus: + provider: str + available: bool + needs_signin: bool + expires_at: datetime | None = None + state: str = "needs_signin" + + +class CredentialSecretStore(Protocol): + """Read and patch pre-created Secret records without exposing their contents in logs.""" + + def read(self, namespace: str, name: str) -> dict[str, str] | None: ... + + def publish(self, namespace: str, name: str, values: Mapping[str, str]) -> None: ... + + +class KubernetesCredentialSecretStore: + """Kubernetes Secret adapter. Secrets must be provisioned through GitOps first.""" + + def __init__(self, api: client.CoreV1Api | None = None): + self._api = api + + def _client(self) -> client.CoreV1Api: + if self._api is None: + try: + config.load_incluster_config() + except config.ConfigException: + config.load_kube_config() + self._api = client.CoreV1Api() + return self._api + + def read(self, namespace: str, name: str) -> dict[str, str] | None: + try: + secret = self._client().read_namespaced_secret(name, namespace) + except ApiException as exc: + if exc.status == 404: + return None + raise CredentialBrokerError( + f"credential Secret read failed (status {exc.status})" + ) from exc + data = secret.data or {} + try: + return { + key: base64.b64decode(value, validate=True).decode("utf-8") + for key, value in data.items() + if isinstance(key, str) and isinstance(value, str) + } + except (ValueError, UnicodeDecodeError) as exc: + raise CredentialBrokerError("credential Secret data is invalid") from exc + + def publish(self, namespace: str, name: str, values: Mapping[str, str]) -> None: + body = { + "data": { + key: base64.b64encode(value.encode("utf-8")).decode("ascii") + for key, value in values.items() + } + } + try: + self._client().patch_namespaced_secret(name, namespace, body) + except ApiException as exc: + raise CredentialBrokerError( + f"credential Secret update failed (status {exc.status})" + ) from exc + + +class CredentialBroker: + """Seed protected credentials by file path and publish egress values in pre-created Secrets.""" + + def __init__( + self, + *, + store: CredentialSecretStore | None = None, + namespace: str | None = None, + secret_prefix: str | None = None, + account: str | None = None, + codex_auth_path: str | None = None, + claude_token_path: str | None = None, + ): + self.store = store or KubernetesCredentialSecretStore() + self.namespace = namespace or settings.substrate_credential_secret_namespace + self.secret_prefix = ( + secret_prefix or settings.substrate_credential_secret_prefix + ) + self.account = account or settings.substrate_credential_account + self.codex_auth_path = ( + codex_auth_path + if codex_auth_path is not None + else settings.substrate_codex_auth_path + ) + self.claude_token_path = ( + claude_token_path + if claude_token_path is not None + else settings.substrate_claude_token_path + ) + if not _DNS_LABEL.fullmatch(self.secret_prefix) or not _DNS_LABEL.fullmatch( + self.account + ): + raise ValueError("credential Secret prefix and account must be DNS labels") + + def secret_name(self, provider: str) -> str: + if provider not in _PROVIDERS: + raise ValueError("unsupported credential provider") + name = f"{self.secret_prefix}-{self.account}-{provider}" + if len(name) > 63 or not _DNS_LABEL.fullmatch(name): + raise ValueError("credential Secret name must be a DNS label") + return name + + async def _read(self, provider: str) -> dict[str, str] | None: + try: + return await asyncio.to_thread( + self.store.read, self.namespace, self.secret_name(provider) + ) + except CredentialBrokerError: + raise + except Exception as exc: + raise CredentialBrokerError("credential Secret read failed") from exc + + async def _publish(self, provider: str, values: Mapping[str, str]) -> None: + try: + await asyncio.to_thread( + self.store.publish, + self.namespace, + self.secret_name(provider), + values, + ) + except CredentialBrokerError: + raise + except Exception as exc: + raise CredentialBrokerError("credential Secret update failed") from exc + + async def seed_configured(self, provider: str) -> CredentialStatus: + lock_key = ( + id(asyncio.get_running_loop()), + self.namespace, + self.secret_name(provider), + ) + lock = _SEED_LOCKS.setdefault(lock_key, asyncio.Lock()) + async with lock: + return await self._seed_configured_once(provider) + + async def _seed_configured_once(self, provider: str) -> CredentialStatus: + existing = await self._read(provider) + if existing is None: + raise CredentialSecretMissing("credential Secret is not pre-created") + if existing: + # Never replace an existing rejected, expired or valid credential from a file. + return self._status_from_values(provider, existing) + if provider == "codex": + path = self.codex_auth_path + if not path: + raise CredentialNeedsSignin(provider) + raw = await self._read_path(path, _MAX_CODEX_AUTH_BYTES, provider) + values = self._codex_auth_values(raw) + elif provider == "claude": + path = self.claude_token_path + if not path: + raise CredentialNeedsSignin(provider) + raw = await self._read_path(path, _MAX_CLAUDE_TOKEN_BYTES, provider) + try: + values = self._claude_token_values(raw.decode("utf-8", errors="strict")) + except UnicodeDecodeError as exc: + raise CredentialBrokerError("Claude token file is invalid") from exc + else: + raise ValueError("unsupported credential provider") + + # An empty data map is the sole uninitialized state. A second sequential call sees + # the published values above and leaves them untouched. + await self._publish(provider, values) + return self._status_from_values(provider, values) + + async def _read_path(self, path: str, max_bytes: int, provider: str) -> bytes: + try: + return await asyncio.to_thread(_read_bounded_file, path, max_bytes) + except CredentialBrokerError: + raise + except Exception as exc: + # Keep the path and OS exception out of loggable errors. + raise CredentialNeedsSignin(provider) from exc + + @classmethod + def _codex_auth_values(cls, raw: bytes) -> dict[str, str]: + try: + auth = json.loads(raw) + except (UnicodeDecodeError, json.JSONDecodeError) as exc: + raise CredentialBrokerError("Codex auth file is invalid") from exc + return cls._codex_secret_values(auth, raw) + + @staticmethod + def _claude_token_values(token: str) -> dict[str, str]: + token = token.strip() + if not token: + raise CredentialBrokerError("Claude token file is empty") + return { + "oauth-token": token, + "injection-value": token, + "needs-signin": "false", + } + + async def store_codex_auth_document(self, raw: bytes) -> CredentialStatus: + if not raw or len(raw) > _MAX_CODEX_AUTH_BYTES: + raise CredentialBrokerError("Codex auth document size is invalid") + values = self._codex_auth_values(raw) + status = self._status_from_values("codex", values) + if status.needs_signin: + raise CredentialNeedsSignin("codex") + await self._publish("codex", values) + return status + + async def store_claude_token(self, token: str) -> CredentialStatus: + if ( + not isinstance(token, str) + or len(token.encode("utf-8")) > _MAX_CLAUDE_TOKEN_BYTES + ): + raise CredentialBrokerError("Claude token size is invalid") + values = self._claude_token_values(token) + await self._publish("claude", values) + return self._status_from_values("claude", values) + + @staticmethod + def _codex_secret_values(auth: object, raw: bytes) -> dict[str, str]: + if not isinstance(auth, dict) or not isinstance(auth.get("tokens"), dict): + raise CredentialBrokerError("Codex auth file has an invalid shape") + tokens = auth["tokens"] + access_token = tokens.get("access_token") + id_token = tokens.get("id_token") + refresh_token = tokens.get("refresh_token") + account_id = tokens.get("account_id") + if ( + not isinstance(access_token, str) + or not isinstance(id_token, str) + or not isinstance(refresh_token, str) + or not isinstance(account_id, str) + or not account_id.strip() + or not isinstance(auth.get("last_refresh"), (str, int, float)) + ): + raise CredentialBrokerError("Codex auth file has an invalid shape") + expiry = _jwt_expiry(access_token) + needs_signin = ( + expiry <= datetime.now(UTC).timestamp() + _JWT_EXPIRY_MARGIN_SECONDS + ) + return { + "auth.json": raw.decode("utf-8"), + "account-id": account_id, + "access-token": access_token if not needs_signin else "", + "injection-value": access_token if not needs_signin else "", + "expires-at": str(int(expiry)), + "needs-signin": "true" if needs_signin else "false", + } + + @staticmethod + def _status_from_values( + provider: str, values: Mapping[str, str] + ) -> CredentialStatus: + if not values: + return CredentialStatus(provider, False, True, state="uninitialized") + available = bool(values.get("injection-value")) + needs_signin = values.get("needs-signin") == "true" or not available + expires_at = None + state = "available" if available else "needs_signin" + raw_expiry = values.get("expires-at") + if raw_expiry: + try: + expires_at = datetime.fromtimestamp(int(raw_expiry), UTC) + except (ValueError, OverflowError, OSError): + needs_signin = True + state = "rejected" + if ( + provider == "codex" + and expires_at is not None + and expires_at.timestamp() + <= datetime.now(UTC).timestamp() + _JWT_EXPIRY_MARGIN_SECONDS + ): + needs_signin = True + state = "expired" + elif values.get("needs-signin") == "true": + state = "rejected" + return CredentialStatus( + provider, + available and not needs_signin, + needs_signin, + expires_at, + state, + ) + + async def status(self, provider: str) -> CredentialStatus: + values = await self._read(provider) + if values is None: + return CredentialStatus(provider, False, True, state="missing") + if not values: + path = ( + self.codex_auth_path if provider == "codex" else self.claude_token_path + ) + if not path: + return CredentialStatus(provider, False, True, state="uninitialized") + return await self.seed_configured(provider) + status = self._status_from_values(provider, values) + if provider == "codex" and status.needs_signin: + await self._disable_expired_codex_injection(values) + return status + + async def codex_placeholder_auth(self) -> str: + values = await self._read("codex") + if values is None: + raise CredentialNeedsSignin("codex") + if not values: + await self.seed_configured("codex") + values = await self._read("codex") + if not values: + raise CredentialNeedsSignin("codex") + status = self._status_from_values("codex", values) + if not status.available or status.needs_signin: + if status.needs_signin: + await self._disable_expired_codex_injection(values) + raise CredentialNeedsSignin("codex") + account_id = values.get("account-id", "") + if not account_id: + raise CredentialNeedsSignin("codex") + expires = int(datetime.now(UTC).timestamp()) + 365 * 24 * 60 * 60 + jwt = _synthetic_jwt(expires) + placeholder = { + "auth_mode": "chatgpt", + "tokens": { + "id_token": jwt, + "access_token": jwt, + "refresh_token": "", + "account_id": account_id, + }, + "last_refresh": datetime.now(UTC).isoformat(), + } + return json.dumps(placeholder, separators=(",", ":")) + + async def claude_placeholder_token(self) -> str: + status = await self.status("claude") + if not status.available or status.needs_signin: + raise CredentialNeedsSignin("claude") + return "sk-ant-oat01-mainloop-egress-placeholder" + + async def _disable_expired_codex_injection(self, values: Mapping[str, str]) -> None: + if not values.get("injection-value"): + return + updated = dict(values) + updated.update( + {"access-token": "", "injection-value": "", "needs-signin": "true"} + ) + await self._publish("codex", updated) + + +def _read_bounded_file(path: str, max_bytes: int) -> bytes: + with Path(path).open("rb") as source: + data = source.read(max_bytes + 1) + if not data or len(data) > max_bytes: + raise CredentialBrokerError("credential file size is invalid") + return data + + +def _jwt_expiry(token: str) -> float: + pieces = token.split(".") + if len(pieces) != 3: + raise CredentialBrokerError("Codex access token is not a JWT") + try: + payload = pieces[1] + "=" * (-len(pieces[1]) % 4) + claims = json.loads(base64.urlsafe_b64decode(payload).decode("utf-8")) + expiry = claims["exp"] + except ( + ValueError, + UnicodeDecodeError, + json.JSONDecodeError, + KeyError, + TypeError, + ) as exc: + raise CredentialBrokerError("Codex access token expiry is unavailable") from exc + if isinstance(expiry, bool) or not isinstance(expiry, (int, float)): + raise CredentialBrokerError("Codex access token expiry is invalid") + return float(expiry) + + +def _synthetic_jwt(expiry: int) -> str: + def encode(value: object) -> str: + raw = json.dumps(value, separators=(",", ":")).encode("utf-8") + return base64.urlsafe_b64encode(raw).decode("ascii").rstrip("=") + + return f"{encode({'alg': 'none'})}.{encode({'exp': expiry})}.synthetic" diff --git a/backend/src/mainloop/runtime/credential_reauth.py b/backend/src/mainloop/runtime/credential_reauth.py new file mode 100644 index 0000000..61d4559 --- /dev/null +++ b/backend/src/mainloop/runtime/credential_reauth.py @@ -0,0 +1,279 @@ +"""Credential re-auth job interface and Kubernetes implementation. + +The job image must provide ``/usr/local/bin/mainloop-reauth``. It captures provider output, +prints only a validated device challenge, and sends the resulting credential to the callback +over the in-cluster service. The credential never appears in Kubernetes logs or API responses. +""" + +from __future__ import annotations + +import asyncio +import json +import re +import secrets +from dataclasses import dataclass +from typing import Protocol + +from kubernetes import client, config +from kubernetes.client.rest import ApiException +from mainloop.config import settings + +_PROVIDERS = {"codex", "claude"} +_CHALLENGE_MARKER = "MAINLOOP_REAUTH_CHALLENGE " +_CHALLENGE_CODE = re.compile(r"^[A-Z0-9-]{4,32}$") +_SAFE_AUTH_HOSTS = {"auth.openai.com", "claude.ai", "console.anthropic.com"} + + +@dataclass(frozen=True, slots=True) +class ReauthChallenge: + url: str + code: str + + +@dataclass(frozen=True, slots=True) +class ReauthStatus: + id: str + provider: str + state: str + challenge: ReauthChallenge | None = None + + +class CredentialReauthRunner(Protocol): + async def start( + self, provider: str, *, owner: str | None = None + ) -> ReauthStatus: ... + + async def status(self, job_id: str) -> ReauthStatus | None: ... + + async def callback_provider(self, job_id: str, token: str) -> str | None: ... + + async def complete(self, job_id: str, token: str) -> None: ... + + +class FakeCredentialReauthRunner: + """In-memory runner for API and lifecycle tests. Credential values are never retained.""" + + def __init__(self): + self._jobs: dict[str, tuple[str, str, ReauthStatus]] = {} + + async def start(self, provider: str, *, owner: str | None = None) -> ReauthStatus: + del owner + if provider not in _PROVIDERS: + raise ValueError("unsupported credential provider") + job_id = f"reauth-{provider}-{secrets.token_hex(6)}" + token = secrets.token_urlsafe(32) + result = ReauthStatus(job_id, provider, "running") + self._jobs[job_id] = (provider, token, result) + return result + + async def status(self, job_id: str) -> ReauthStatus | None: + row = self._jobs.get(job_id) + return row[2] if row else None + + async def callback_provider(self, job_id: str, token: str) -> str | None: + row = self._jobs.get(job_id) + return row[0] if row and secrets.compare_digest(row[1], token) else None + + async def complete(self, job_id: str, token: str) -> None: + row = self._jobs.get(job_id) + if row and secrets.compare_digest(row[1], token): + provider, _, result = row + self._jobs[job_id] = ( + provider, + "", + ReauthStatus(job_id, provider, "completed", result.challenge), + ) + + +class KubernetesCredentialReauthRunner: + """Run one bounded control-side Job per sign-in attempt (not live-cluster verified).""" + + def __init__( + self, + batch_api: client.BatchV1Api | None = None, + core_api: client.CoreV1Api | None = None, + ): + self._batch = batch_api + self._core = core_api + self.namespace = settings.substrate_reauth_job_namespace + self._tokens: dict[str, tuple[str, str]] = {} + self._completed: set[str] = set() + + def _clients(self) -> tuple[client.BatchV1Api, client.CoreV1Api]: + if self._batch is None or self._core is None: + try: + config.load_incluster_config() + except config.ConfigException: + config.load_kube_config() + self._batch = self._batch or client.BatchV1Api() + self._core = self._core or client.CoreV1Api() + return self._batch, self._core # type: ignore[return-value] + + async def start(self, provider: str, *, owner: str | None = None) -> ReauthStatus: + del owner + if provider not in _PROVIDERS: + raise ValueError("unsupported credential provider") + image = settings.substrate_reauth_job_image + if not image: + raise RuntimeError("credential re-auth job image is not configured") + job_id = f"reauth-{provider}-{secrets.token_hex(6)}" + callback_token = secrets.token_urlsafe(32) + body = client.V1Job( + metadata=client.V1ObjectMeta( + name=job_id, + namespace=self.namespace, + labels={"app.kubernetes.io/name": "mainloop-credential-reauth"}, + ), + spec=client.V1JobSpec( + backoff_limit=0, + active_deadline_seconds=settings.substrate_reauth_timeout_seconds, + ttl_seconds_after_finished=3600, + template=client.V1PodTemplateSpec( + metadata=client.V1ObjectMeta(labels={"job-name": job_id}), + spec=client.V1PodSpec( + restart_policy="Never", + automount_service_account_token=False, + volumes=[ + client.V1Volume( + name="tmp", + empty_dir=client.V1EmptyDirVolumeSource( + medium="Memory", size_limit="64Mi" + ), + ) + ], + containers=[ + client.V1Container( + name="reauth", + image=image, + image_pull_policy="IfNotPresent", + command=["node", "/usr/local/bin/mainloop-reauth"], + args=[provider, job_id], + env=[ + client.V1EnvVar( + name="MAINLOOP_REAUTH_CALLBACK_URL", + value=settings.substrate_reauth_callback_url, + ), + client.V1EnvVar( + name="MAINLOOP_REAUTH_CALLBACK_TOKEN", + value=callback_token, + ), + client.V1EnvVar( + name="MAINLOOP_REAUTH_TIMEOUT_SECONDS", + value=str( + settings.substrate_reauth_timeout_seconds + ), + ), + ], + security_context=client.V1SecurityContext( + allow_privilege_escalation=False, + run_as_non_root=True, + run_as_user=10001, + run_as_group=10001, + read_only_root_filesystem=True, + capabilities=client.V1Capabilities(drop=["ALL"]), + ), + volume_mounts=[ + client.V1VolumeMount( + name="tmp", + mount_path="/tmp", # nosec B108 - memory-backed emptyDir + ) + ], + resources=client.V1ResourceRequirements( + requests={"cpu": "50m", "memory": "128Mi"}, + limits={"cpu": "500m", "memory": "512Mi"}, + ), + ) + ], + ), + ), + ), + ) + try: + batch, _ = self._clients() + await asyncio.to_thread(batch.create_namespaced_job, self.namespace, body) + except ApiException as exc: + raise RuntimeError( + f"credential re-auth job could not start (status {exc.status})" + ) from exc + self._tokens[job_id] = (provider, callback_token) + return ReauthStatus(job_id, provider, "running") + + async def status(self, job_id: str) -> ReauthStatus | None: + if not re.fullmatch(r"reauth-(?:codex|claude)-[a-f0-9]{12}", job_id): + return None + try: + batch, core = self._clients() + job = await asyncio.to_thread( + batch.read_namespaced_job, job_id, self.namespace + ) + except ApiException as exc: + if exc.status == 404: + return None + raise RuntimeError( + f"credential re-auth job status failed (status {exc.status})" + ) from exc + provider = "codex" if "-codex-" in job_id else "claude" + challenge = None + pods = await asyncio.to_thread( + core.list_namespaced_pod, + self.namespace, + label_selector=f"job-name={job_id}", + ) + for pod in pods.items: + if not pod.metadata or not pod.metadata.name: + continue + try: + output = await asyncio.to_thread( + core.read_namespaced_pod_log, + pod.metadata.name, + self.namespace, + tail_lines=100, + ) + except ApiException: + continue + challenge = _challenge_from_logs(output) + if challenge: + break + if job.status and job.status.succeeded: + state = "completed" if job_id in self._completed else "failed" + elif job.status and job.status.failed: + state = "failed" + self._tokens.pop(job_id, None) + else: + state = "running" + return ReauthStatus(job_id, provider, state, challenge) + + async def callback_provider(self, job_id: str, token: str) -> str | None: + row = self._tokens.get(job_id) + if row is None or not token or not secrets.compare_digest(row[1], token): + return None + return row[0] + + async def complete(self, job_id: str, token: str) -> None: + row = self._tokens.get(job_id) + if row and secrets.compare_digest(row[1], token): + self._tokens.pop(job_id, None) + self._completed.add(job_id) + + +def _challenge_from_logs(output: str) -> ReauthChallenge | None: + for line in output.splitlines()[-100:]: + if not line.startswith(_CHALLENGE_MARKER): + continue + try: + value = json.loads(line[len(_CHALLENGE_MARKER) :]) + url = value["url"] + code = value["code"] + from urllib.parse import urlsplit + + parsed = urlsplit(url) + except (KeyError, TypeError, ValueError, json.JSONDecodeError): + return None + if ( + parsed.scheme == "https" + and parsed.hostname in _SAFE_AUTH_HOSTS + and isinstance(code, str) + and _CHALLENGE_CODE.fullmatch(code) + ): + return ReauthChallenge(url, code) + return None diff --git a/backend/src/mainloop/runtime/credential_reauth_api.py b/backend/src/mainloop/runtime/credential_reauth_api.py new file mode 100644 index 0000000..7b7e06f --- /dev/null +++ b/backend/src/mainloop/runtime/credential_reauth_api.py @@ -0,0 +1,48 @@ +"""Private callback endpoint for control-side credential re-auth jobs.""" + +from __future__ import annotations + +import json + +from fastapi import APIRouter, Header, HTTPException, Request, Response +from mainloop.runtime.credential_broker import CredentialBroker, CredentialBrokerError +from mainloop.runtime.workspace_api import _reauth_runner + +router = APIRouter(tags=["internal credential re-auth"]) +_MAX_CALLBACK_BYTES = 300 * 1024 + + +@router.post("/internal/reauth/{job_id}", status_code=204) +async def receive_reauth_result( + job_id: str, + request: Request, + callback_token: str | None = Header(default=None, alias="X-Mainloop-Reauth-Token"), +): + provider = await _reauth_runner.callback_provider(job_id, callback_token or "") + if provider is None: + raise HTTPException(status_code=401, detail="Invalid re-auth callback") + raw = await request.body() + if len(raw) > _MAX_CALLBACK_BYTES: + raise HTTPException(status_code=413, detail="Re-auth result is too large") + try: + payload = json.loads(raw) + except (json.JSONDecodeError, UnicodeDecodeError) as exc: + raise HTTPException(status_code=400, detail="Invalid re-auth result") from exc + if not isinstance(payload, dict) or payload.get("provider") != provider: + raise HTTPException(status_code=400, detail="Invalid re-auth result") + broker = CredentialBroker() + try: + if provider == "codex" and isinstance(payload.get("auth"), dict): + await broker.store_codex_auth_document( + json.dumps(payload["auth"], separators=(",", ":")).encode("utf-8") + ) + elif provider == "claude" and isinstance(payload.get("token"), str): + await broker.store_claude_token(payload["token"]) + else: + raise CredentialBrokerError("invalid re-auth result") + except CredentialBrokerError as exc: + raise HTTPException( + status_code=400, detail="Credential result was rejected" + ) from exc + await _reauth_runner.complete(job_id, callback_token or "") + return Response(status_code=204) diff --git a/backend/src/mainloop/runtime/herdr.py b/backend/src/mainloop/runtime/herdr.py deleted file mode 100644 index 0897227..0000000 --- a/backend/src/mainloop/runtime/herdr.py +++ /dev/null @@ -1,219 +0,0 @@ -"""Thin Herdr adapter: drives ``agentctl`` in the workspace pod over Kubernetes pod-exec. - -Herdr owns liveness, naming and delivery of input. This adapter never reads a reply from a -terminal; replies, receipts and completion come from the native journals (``journal.py``), -which ``agentctl journal`` prints from the PVC. - -Transport: Kubernetes API pod-exec with a Role limited to pods get/list + pods/exec create in -the workspace namespace. ``TransportError`` means the outcome of the call is unknown. -""" - -from __future__ import annotations - -import asyncio -import json -import logging -import shlex -from dataclasses import dataclass - -from kubernetes import client, config -from kubernetes.client.rest import ApiException -from kubernetes.stream import stream -from mainloop.config import settings - -logger = logging.getLogger(__name__) - - -class TransportError(RuntimeError): - """The exec channel failed; whether the command ran is unknown.""" - - -class WorkspaceUnavailable(RuntimeError): - """The workspace pod is not Ready; nothing was attempted.""" - - -@dataclass(frozen=True, slots=True) -class ExecResult: - exit_code: int - stdout: str - stderr: str - - -@dataclass(frozen=True, slots=True) -class PodState: - name: str - uid: str | None - ready: bool - - -@dataclass(frozen=True, slots=True) -class JournalSlice: - file: str | None - total_lines: int - lines: list[tuple[int, str]] - - -_api: client.CoreV1Api | None = None - - -def _core() -> client.CoreV1Api: - global _api - if _api is None: - try: - config.load_incluster_config() - except config.ConfigException: - config.load_kube_config() - _api = client.CoreV1Api() - return _api - - -class HerdrWorkspace: - """One workspace pod running a Herdr server and ``agentctl``.""" - - def __init__( - self, - namespace: str | None = None, - pod: str | None = None, - container: str = "workspace", - ): - self.namespace = namespace or settings.workspace_namespace - self.pod = pod or settings.workspace_pod - self.container = container - - # -- transport ------------------------------------------------------------------------- - def _exec_sync(self, command: list[str], timeout: float) -> ExecResult: - try: - _core() # loads the cluster config once - # stream() swaps the ApiClient request function while it runs, which is not - # thread-safe: each exec gets its own client so concurrent polls cannot clash. - resp = stream( - client.CoreV1Api(client.ApiClient()).connect_get_namespaced_pod_exec, - self.pod, - self.namespace, - container=self.container, - command=command, - stderr=True, - stdin=False, - stdout=True, - tty=False, - _preload_content=False, - ) - resp.run_forever(timeout=timeout) - out, err = resp.read_stdout(), resp.read_stderr() - code = resp.returncode - resp.close() - except ( - ApiException, - OSError, - RuntimeError, - ) as exc: # websocket errors are OSError/Runtime - raise TransportError(f"exec failed: {type(exc).__name__}") from exc - if code is None: - raise TransportError("exec did not report an exit status") - return ExecResult(int(code), out or "", err or "") - - async def _exec(self, command: list[str], timeout: float = 45) -> ExecResult: - return await asyncio.to_thread(self._exec_sync, command, timeout) - - async def pod_state(self) -> PodState: - try: - pod = await asyncio.to_thread( - _core().read_namespaced_pod, self.pod, self.namespace - ) - except ApiException as exc: - if exc.status == 404: - return PodState(self.pod, None, False) - raise TransportError(f"pod read failed: {exc.status}") from exc - ready = any( - c.type == "Ready" and c.status == "True" - for c in (pod.status.conditions or []) - ) - if pod.metadata.deletion_timestamp is not None: - ready = False - return PodState(self.pod, pod.metadata.uid, ready) - - async def require_ready(self) -> PodState: - state = await self.pod_state() - if not state.ready: - raise WorkspaceUnavailable(f"workspace pod {self.pod} is not Ready") - return state - - # -- agentctl verbs -------------------------------------------------------------------- - async def agent_status(self, name: str) -> dict | None: - """Herdr liveness hint; ``None`` when Herdr has no such agent.""" - res = await self._exec(["agentctl", "status", name]) - text = res.stdout.strip() - if res.exit_code != 0 or not text: - return None - return json.loads(text.splitlines()[-1]) - - async def start( - self, - binding: str, - name: str, - *, - native_id: str | None, - resume: bool, - extra: dict[str, str] | None = None, - ) -> dict: - """Start (or resume) an agent. ``extra`` maps agentctl options (``--cwd-rel``, ``--model``, - ``--effort``, ``--standing-b64``, ``--token``) to values; secrets travel as argv over the - authenticated exec channel and are written to 0600 files on the PVC by agentctl. - """ - args = ["agentctl", "start", binding, "--name", name] - if native_id: - args += ["--resume" if resume else "--new-id", native_id] - for opt, value in (extra or {}).items(): - args += [opt, value] - res = await self._exec(args, timeout=100) - if res.exit_code != 0: - raise RuntimeError( - f"agent start failed (exit {res.exit_code}): {res.stderr.strip()[-200:]}" - ) - last = res.stdout.strip().splitlines()[-1] - return json.loads(last) if last.startswith("{") else {"note": last} - - async def send(self, name: str, text: str) -> None: - """Deliver one prompt. Raises TransportError if the outcome is unknown; never retries.""" - res = await self._exec(["agentctl", "send", name, text]) - if res.exit_code != 0: - raise RuntimeError( - f"send failed (exit {res.exit_code}): {res.stderr.strip()[-200:]}" - ) - - async def stop(self, name: str) -> None: - res = await self._exec(["agentctl", "stop", name], timeout=60) - if res.exit_code != 0: - raise RuntimeError( - f"agent stop failed (exit {res.exit_code}): {res.stderr.strip()[-200:]}" - ) - - async def native_id(self, name: str) -> str | None: - res = await self._exec(["agentctl", "native-id", name]) - return res.stdout.strip() or None if res.exit_code == 0 else None - - async def journal(self, name: str, native_id: str, from_line: int) -> JournalSlice: - res = await self._exec(["agentctl", "journal", name, native_id, str(from_line)]) - if res.exit_code != 0: - raise TransportError(f"journal read failed (exit {res.exit_code})") - file = None - total = from_line - lines: list[tuple[int, str]] = [] - for raw in res.stdout.split("\n"): - if not raw: - continue - if raw.startswith("#nofile"): - return JournalSlice(None, 0, []) - if raw.startswith("#file\t"): - _, file, count = raw.split("\t") - total = int(count) - continue - num, _, rest = raw.partition("\t") - if num.isdigit(): - lines.append((int(num), rest)) - return JournalSlice(file, total, lines) - - -def agentctl_quote(*parts: str) -> str: - """Only for logging/evidence; commands are passed as argv, never through a shell.""" - return " ".join(shlex.quote(p) for p in parts) diff --git a/backend/src/mainloop/runtime/journal.py b/backend/src/mainloop/runtime/journal.py index 45cd3fa..af34c73 100644 --- a/backend/src/mainloop/runtime/journal.py +++ b/backend/src/mainloop/runtime/journal.py @@ -1,6 +1,6 @@ """Read real native journals (Claude transcript JSONL, Codex rollout JSONL). -The journal is the authority for receipts, replies, completion and model. Herdr only +The journal is the authority for receipts, replies, completion and model. The actor shim delivers input and reports liveness. ``NativeEvent`` carries no text, so reply text is extracted here from the raw record; the same record is also passed through the existing adapters (``ClaudeSessionNormalizer``, ``observe_codex_event``) after a small translation @@ -50,7 +50,7 @@ class JournalEvent: def unwrap_paste(text: str) -> str: - """Claude Code wraps pasted (Herdr-delivered) input in ```` tags.""" + """Unwrap pasted input that Claude Code returns in ```` tags.""" match = _PASTED.match(text) return (match.group(1) if match else text).strip() @@ -70,15 +70,13 @@ def _text_blocks(content: Any, block_types: tuple[str, ...]) -> str: return "\n".join(p for p in parts if p) -def _binding(kind: str, native_id: str, agent: str) -> NativeBinding: +def _binding(kind: str, native_id: str) -> NativeBinding: return NativeBinding( binding_id=f"{kind}-{native_id}", - workspace_id="herdr-spike/workspace-0", + workspace_id="fixture/workspace-0", provider=kind, runtime_type=f"{kind}-native-cli", native_session_id=native_id, - herdr_session_id="mainloop-spike", - herdr_agent_id=agent, creation_mode="created", ownership_generation=1, ) @@ -92,9 +90,9 @@ def _iso(value: Any) -> str | None: def parse_claude( - lines: Iterable[tuple[int, str]], *, file_ref: str, native_id: str, agent: str + lines: Iterable[tuple[int, str]], *, file_ref: str, native_id: str ) -> list[JournalEvent]: - normalizer = ClaudeSessionNormalizer(_binding("claude", native_id, agent)) + normalizer = ClaudeSessionNormalizer(_binding("claude", native_id)) out: list[JournalEvent] = [] for cursor, line in lines: try: @@ -208,9 +206,9 @@ def _claude_normalize( def parse_codex( - lines: Iterable[tuple[int, str]], *, file_ref: str, native_id: str, agent: str + lines: Iterable[tuple[int, str]], *, file_ref: str, native_id: str ) -> list[JournalEvent]: - binding = _binding("codex", native_id, agent) + binding = _binding("codex", native_id) out: list[JournalEvent] = [] model: str | None = None for cursor, line in lines: @@ -290,12 +288,11 @@ def parse_journal( *, file_ref: str, native_id: str, - agent: str, ) -> list[JournalEvent]: if kind == "claude": - return parse_claude(lines, file_ref=file_ref, native_id=native_id, agent=agent) + return parse_claude(lines, file_ref=file_ref, native_id=native_id) if kind == "codex": - return parse_codex(lines, file_ref=file_ref, native_id=native_id, agent=agent) + return parse_codex(lines, file_ref=file_ref, native_id=native_id) raise ValueError(f"no journal reader for kind {kind}") diff --git a/backend/src/mainloop/runtime/native_sessions.py b/backend/src/mainloop/runtime/native_sessions.py index 21667ea..f2d1b82 100644 --- a/backend/src/mainloop/runtime/native_sessions.py +++ b/backend/src/mainloop/runtime/native_sessions.py @@ -1,4 +1,4 @@ -"""Sessions bound to a real native agent (Claude Code / Codex) under Herdr in the workspace pod. +"""Sessions bound to a native Claude Code or Codex agent in a Substrate workspace. Control-plane rules implemented here: - A user message is recorded, then a delivery row is persisted as ``sending`` *before* the @@ -7,8 +7,8 @@ - The native journal is the receipt: a prompt record after the recorded cursor proves delivery, the turn-completion record proves completion, and the assistant text in between is mirrored into the session conversation (deterministic ids, so repeated syncs are idempotent). -- After pod replacement the agent is not live in Herdr; the next delivery restarts it with the - native resume flag against the same native session id, then sends. +- The actor-local shim owns native CLI process lifetime and journal access. Before sending, the + adapter checks the existing native session and never replays a prompt blindly. Context model (plan r7): a binding has a ``role``. ``main`` is the conversation agent whose window Mainloop owns by rotation (a lineage of disposable native sessions; ``rotate``); ``child`` is a @@ -27,24 +27,37 @@ from mainloop.config import settings from mainloop.db import db +from mainloop.runtime import workspace_adapter from mainloop.runtime.agent_api import hash_token, token_for -from mainloop.runtime.herdr import HerdrWorkspace, TransportError, WorkspaceUnavailable +from mainloop.runtime.credential_broker import CredentialNeedsSignin from mainloop.runtime.journal import completed_turns, parse_journal from mainloop.runtime.standing import content_hash +from mainloop.runtime.substrate import TransportError +from mainloop.runtime.substrate_workspace import ( + JournalSlice, + SubstrateWorkspace, + WorkspaceUnavailable, +) +from mainloop.sse import notify_session_needs_input -from models import NativeDeliveryInfo, NativeSessionInfo, SessionStatus +from models import ( + NativeDeliveryInfo, + NativeSessionInfo, + SessionNotification, + SessionStatus, +) logger = logging.getLogger(__name__) APPROVAL_POLICY = "bypass-permissions" SEND_RECEIPT_GRACE = timedelta(seconds=60) -# A prompt seen in the journal whose turn never completes (agent exited or wedged, pod replaced): +# A prompt seen in the journal whose turn never completes (agent exited, wedged, or actor replaced): # after this long, or as soon as the agent is no longer live, it becomes 'uncertain' (never # replayed, never blocking) instead of holding the session in flight forever. DELIVERED_MAX_AGE = timedelta(minutes=30) _NS = uuid.UUID("6f0f7f0e-3f1e-4a3c-9d3b-0e4b6f5c2a11") _locks: dict[str, asyncio.Lock] = {} -_workspaces: dict[str, HerdrWorkspace] = {} +_workspaces: dict[tuple[str, ...], SubstrateWorkspace] = {} _rotating: set[str] = set() OPEN_STATES = ("recorded", "sending", "delivered") # Ended by the user or by failure. Agent activity never moves a session out of these. @@ -80,12 +93,43 @@ def is_rotating(session_id: str) -> bool: return session_id in _rotating -def workspace_for(binding: dict) -> HerdrWorkspace: - """One Herdr workspace pod per binding: ``main-0`` for the main thread, else ``workspace-0``.""" - pod = binding.get("pod") or settings.workspace_pod - if pod not in _workspaces: - _workspaces[pod] = HerdrWorkspace(pod=pod) - return _workspaces[pod] +def workspace_for(binding: dict) -> SubstrateWorkspace: + """Map a native binding to its branch actor or configured shared actor.""" + agent = binding["kind"] + atespace = binding.get("workspace_atespace") + actor = binding.get("workspace_actor_name") + secret_name = binding.get("workspace_shim_token_secret_name") + workspace_route = (atespace, actor, secret_name) + if any(workspace_route) and not all(workspace_route): + raise RuntimeError("native workspace binding has an incomplete actor route") + if not all(workspace_route): + actor_binding = settings.substrate_actor_bindings.get(agent) + if actor_binding is None: + raise RuntimeError( + f"no Substrate actor binding is configured for native agent {agent}" + ) + atespace = actor_binding.atespace + actor = actor_binding.actor + secret_name = actor_binding.shim_token_secret_name + key = ( + binding["session_id"], + atespace, + actor, + secret_name, + agent, + ) + if key not in _workspaces: + _workspaces[key] = SubstrateWorkspace( + atespace=atespace, + actor=actor, + agent=agent, + shim_token_secret_name=secret_name, + logical_session_id=binding["session_id"], + native_session_id=binding.get("native_session_id"), + ) + workspace = _workspaces[key] + workspace.set_native_session_id(binding.get("native_session_id")) + return workspace def rotation_due( @@ -116,19 +160,28 @@ def agent_name(session_id: str, kind: str) -> str: return f"ml-{kind}-{session_id[:8]}" -async def get_binding(session_id: str) -> dict | None: - async with db.connection() as conn: - row = await conn.fetchrow( - "SELECT * FROM native_bindings WHERE session_id=$1", session_id - ) +async def get_binding(session_id: str, *, conn=None) -> dict | None: + query = """SELECT b.*, + w.atespace AS workspace_atespace, + w.actor_name AS workspace_actor_name, + w.shim_token_secret_name AS workspace_shim_token_secret_name + FROM native_bindings b + LEFT JOIN workspace_bindings w ON w.workspace_id=b.session_id + WHERE b.session_id=$1""" + if conn is None: + async with db.connection() as connection: + row = await connection.fetchrow(query, session_id) + else: + row = await conn.fetchrow(query, session_id) return dict(row) if row else None async def _update_binding(session_id: str, **fields) -> None: - sets = ", ".join(f"{k}=${i + 2}" for i, k in enumerate(fields)) + sets = [f"{k}=${i + 2}" for i, k in enumerate(fields)] + sets.append("updated_at=NOW()") async with db.connection() as conn: await conn.execute( - f"UPDATE native_bindings SET {sets}, updated_at=NOW() WHERE session_id=$1", # nosec B608 - column names come from code, values are bound + f"UPDATE native_bindings SET {', '.join(sets)} WHERE session_id=$1", # nosec B608 - column names come from code, values are bound session_id, *fields.values(), ) @@ -155,15 +208,6 @@ async def _set_delivery( ) -def config_name(binding: dict) -> str: - """Agentctl binding config (ConfigMap ``.env``) for this binding.""" - if binding["role"] == "main": - return "claude-main" - if binding["role"] == "child": - return f"{binding['kind']}-child" - return binding["kind"] - - async def create_binding( session_id: str, kind: str, @@ -171,38 +215,44 @@ async def create_binding( role: str = "agent", parent_session_id: str | None = None, topic_id: str | None = None, + conn=None, ) -> dict: # Claude takes the native session id up front (--session-id); Codex reports it in its journal. native_id = str(uuid.uuid4()) if kind == "claude" else None name = "ml-main" if role == "main" else agent_name(session_id, kind) - pod = settings.main_pod if role == "main" else None token_hash = ( hash_token(token_for(session_id)) if role in ("main", "child") else None ) - async with db.connection() as conn: - await conn.execute( + + async def insert_binding(connection) -> None: + await connection.execute( """INSERT INTO native_bindings (session_id, kind, agent_name, native_session_id, approval_policy, - role, pod, parent_session_id, topic_id, token_hash, model) - VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11)""", + role, parent_session_id, topic_id, token_hash, model) + VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10)""", session_id, kind, name, native_id, APPROVAL_POLICY if role != "main" else "restricted: Bash(mainloop:*) only", role, - pod, parent_session_id, topic_id, token_hash, settings.main_thread_model if role == "main" else None, ) if role == "main" and native_id: - await conn.execute( + await connection.execute( "INSERT INTO native_lineage (session_id, seq, native_session_id, started_reason) VALUES ($1,1,$2,'create')", session_id, native_id, ) - return await get_binding(session_id) # type: ignore[return-value] + + if conn is None: + async with db.connection() as connection: + await insert_binding(connection) + return await get_binding(session_id) # type: ignore[return-value] + await insert_binding(conn) + return await get_binding(session_id, conn=conn) # type: ignore[return-value] async def _open_count(session_id: str) -> int: @@ -214,6 +264,64 @@ async def _open_count(session_id: str) -> int: ) +async def _record_delivery_message( + *, + session_id: str, + conversation_id: str, + text: str, + state: str, + source: str, +) -> str: + """Record the message and delivery under the workspace lock used by suspension.""" + async with db.connection() as conn: + async with conn.transaction(): + binding = await conn.fetchrow( + """SELECT workspace_id,desired_state FROM workspace_bindings + WHERE workspace_id=$1 FOR UPDATE""", + session_id, + ) + if binding and binding.get("desired_state") == "deleting": + raise ValueError( + "The workspace is being deleted; start another workspace." + ) + lifecycle = ( + await conn.fetchrow( + """SELECT desired_state, observed_state FROM workspace_lifecycles + WHERE workspace_id=$1""", + session_id, + ) + if binding + else None + ) + if lifecycle and ( + lifecycle["desired_state"] == "suspended" + or lifecycle["observed_state"] in {"suspending", "suspended"} + ): + raise ValueError( + "The workspace is suspending or suspended; resume it before sending a message." + ) + + message = await db.create_message( + conversation_id=conversation_id, + role="user", + content=text, + conn=conn, + ) + await conn.execute( + "INSERT INTO native_deliveries (message_id, session_id, state, source) VALUES ($1,$2,$3,$4)", + message.id, + session_id, + state, + source, + ) + if binding: + await conn.execute( + "UPDATE workspace_lifecycles SET last_activity_at=NOW(), updated_at=NOW() WHERE workspace_id=$1", + session_id, + ) + return message.id + + async def submit_message(session_id: str, text: str, *, source: str = "user") -> str: """Record a message and its delivery intent, then deliver in the background. @@ -222,6 +330,10 @@ async def submit_message(session_id: str, text: str, *, source: str = "user") -> task brief to a fresh child). A ``queued`` delivery is sent by ``sync`` once the agent is idle. """ session = await db.get_session(session_id) + # Branch workspace turns touch and wake their actor before the delivery is recorded. Static + # agent bindings have no workspace_bindings row and continue through their existing path. + if await workspace_adapter.get_workspace(session_id) is not None: + await workspace_adapter.touch_workspace(session_id, reason="turn") if source == "user" and session.status in ENDED_STATUSES: raise ValueError(f"This session is {session.status.value}; start a new one.") if source == "user" and session_id in _rotating: @@ -242,20 +354,16 @@ async def submit_message(session_id: str, text: str, *, source: str = "user") -> if busy or (source == "report" and session_id in _rotating) else "recorded" ) - message = await db.create_message( - conversation_id=session.conversation_id, role="user", content=text + message_id = await _record_delivery_message( + session_id=session_id, + conversation_id=session.conversation_id, + text=text, + state=state, + source=source, ) - async with db.connection() as conn: - await conn.execute( - "INSERT INTO native_deliveries (message_id, session_id, state, source) VALUES ($1,$2,$3,$4)", - message.id, - session_id, - state, - source, - ) if state == "recorded": - asyncio.create_task(_deliver(session_id, message.id, text)) - return message.id + asyncio.create_task(_deliver(session_id, message_id, text)) + return message_id async def _start_extra(binding: dict) -> tuple[dict[str, str], str | None]: @@ -271,6 +379,7 @@ async def _start_extra(binding: dict) -> tuple[dict[str, str], str | None]: ), "--token": token_for(binding["session_id"]), "--standing-b64": base64.b64encode(standing.encode()).decode(), + "--approval-policy": binding["approval_policy"], } if binding["role"] == "main": extra["--model"] = settings.main_thread_model @@ -279,37 +388,33 @@ async def _start_extra(binding: dict) -> tuple[dict[str, str], str | None]: async def _ensure_agent(session_id: str, binding: dict) -> dict: - """Make sure the agent is live in Herdr, resuming the native session after pod replacement.""" + """Check native session readiness in its Substrate actor.""" ws = workspace_for(binding) - pod = await ws.require_ready() + await ws.require_ready() name = binding["agent_name"] + resume = binding["journal_ref"] is not None status = await ws.agent_status(name) + extra, standing_hash = await _start_extra(binding) fields: dict = {} if status is None: # A journal already seen for this native session id means an earlier run: resume it. - resume = binding["journal_ref"] is not None - extra, standing_hash = await _start_extra(binding) - ident = await ws.start( - config_name(binding), + await ws.start( + binding["kind"], name, native_id=binding["native_session_id"], resume=resume, extra=extra, ) - fields.update( - herdr_pane_id=ident.get("pane_id"), - herdr_terminal_id=ident.get("terminal_id"), - herdr_workspace_id=ident.get("workspace_id"), - generation=binding["generation"] + (1 if resume else 0), - ) + fields.update(generation=binding["generation"] + (1 if resume else 0)) if standing_hash: fields["standing_hash"] = standing_hash else: - fields.update( - herdr_pane_id=status.get("pane_id"), - herdr_terminal_id=status.get("terminal_id"), - ) - fields["pod_uid"] = pod.uid + ws.set_resume_history(resume) + ws.set_startup_options(extra) + # An already running agent may have resumed from a parked actor snapshot. + await ws.prepare_credentials() + if standing_hash and standing_hash != binding.get("standing_hash"): + fields["standing_hash"] = standing_hash await _update_binding(session_id, **fields) return await get_binding(session_id) # type: ignore[return-value] @@ -330,6 +435,12 @@ async def _deliver(session_id: str, message_id: str, text: str) -> None: ) ).total_lines await _set_delivery(message_id, "sending", cursor_before=cursor_before) + except CredentialNeedsSignin as exc: + await _set_delivery( + message_id, "failed", detail=f"not sent: {exc.provider} needs sign-in" + ) + await _notify_credential_signin(session_id, exc.provider) + return except Exception as exc: logger.exception("delivery not attempted for %s", message_id) await _set_delivery( @@ -357,6 +468,43 @@ async def _deliver(session_id: str, message_id: str, text: str) -> None: await sync(session_id) +async def _notify_credential_signin( + session_id: str, provider: str, *, prompt_sent: bool = False +) -> None: + session = await db.get_session(session_id) + if session is None: + return + title = f"{provider.title()} needs sign-in" + preview = ( + "The agent rejected a provider request. Sign in before sending again." + if prompt_sent + else "No prompt was sent. Open the workspace page to start sign-in." + ) + notification = SessionNotification( + id=f"credential-signin-{provider}-{session_id}", + session_id=session_id, + user_id=session.user_id, + title=title, + preview=preview, + ) + async with db.connection() as conn: + await conn.execute( + """INSERT INTO session_notifications + (id, session_id, user_id, title, preview, read, created_at) + VALUES ($1,$2,$3,$4,$5,FALSE,$6) + ON CONFLICT (id) DO UPDATE SET + title=EXCLUDED.title, preview=EXCLUDED.preview, read=FALSE, + created_at=EXCLUDED.created_at""", + notification.id, + notification.session_id, + notification.user_id, + notification.title, + notification.preview, + notification.created_at, + ) + await notify_session_needs_input(notification.user_id, session_id, title, preview) + + async def sync(session_id: str) -> None: """Mirror new journal evidence into Postgres, then run the follow-up actions (queued deliveries, child fallback report, rotation) that are only safe outside the binding lock. @@ -419,11 +567,16 @@ async def _sync_locked(session_id: str) -> dict | None: return None await _update_binding(session_id, native_session_id=nid) binding["native_session_id"] = nid - jl = await ws.journal( + jl = await _journal_through_high_water( + ws, binding["agent_name"], binding["native_session_id"], binding["journal_cursor"], ) + if await ws.credential_rejected(): + await _notify_credential_signin( + session_id, binding["kind"], prompt_sent=True + ) except (TransportError, WorkspaceUnavailable) as exc: logger.info("sync skipped for %s: %s", session_id, exc) return None @@ -435,7 +588,6 @@ async def _sync_locked(session_id: str) -> dict | None: jl.lines, file_ref=ref, native_id=binding["native_session_id"], - agent=binding["agent_name"], ) session = await db.get_session(session_id) async with db.connection() as conn: @@ -567,12 +719,36 @@ async def _sync_locked(session_id: str) -> dict | None: return follow +async def _journal_through_high_water( + ws: SubstrateWorkspace, name: str, native_id: str, from_line: int +) -> JournalSlice: + """Read bounded journal pages through the first page's captured line high-water mark.""" + first = await ws.journal(name, native_id, from_line) + if first.file is None or not first.lines: + return first + high_water = first.total_lines + lines = list(first.lines) + cursor = lines[-1][0] + while cursor < high_water: + page = await ws.journal(name, native_id, cursor) + if page.file != first.file: + raise TransportError("native journal changed while paging") + additions = [line for line in page.lines if cursor < line[0] <= high_water] + if not additions: + raise TransportError( + "native journal page did not advance to its high-water mark" + ) + lines.extend(additions) + cursor = additions[-1][0] + return JournalSlice(first.file, high_water, lines) + + async def cancel(session_id: str) -> str: """End a native session: stop its agent and close its open deliveries. The status is set first and is sticky, so no later sync brings the session back, and open deliveries are failed so the reconcile loop stops visiting it. Returns ``stopped``, - ``not_running`` (Herdr had no such agent) or ``unknown`` (the stop could not be + ``not_running`` (the actor had no active turn) or ``unknown`` (the stop could not be confirmed; the agent may still be running, and it is not retried blindly). """ binding = await get_binding(session_id) @@ -716,6 +892,7 @@ async def rotate( async def reconcile_loop(interval: float = 3.0) -> None: """Background mirror for sessions with open work, so replies, reports and rotation do not depend on a browser polling.""" + next_idle_check = 0.0 while True: try: async with db.connection() as conn: @@ -730,6 +907,10 @@ async def reconcile_loop(interval: float = 3.0) -> None: for sid in ids: if sid not in _rotating: await sync(sid) + loop = asyncio.get_running_loop() + if loop.time() >= next_idle_check: + await workspace_adapter.suspend_idle_workspaces() + next_idle_check = loop.time() + 60.0 except Exception: logger.exception("reconcile loop iteration failed") await asyncio.sleep(interval) @@ -762,13 +943,13 @@ async def identity(session_id: str) -> NativeSessionInfo | None: for r in rows ] ws = workspace_for(binding) - ready, live, uid, note = False, None, None, None + ready, live, _uid, note = False, None, None, None try: - pod = await ws.pod_state() - ready, uid = pod.ready, pod.uid + workspace = await ws.workspace_state() + ready = workspace.ready if ready: live = (await ws.agent_status(binding["agent_name"])) is not None - except TransportError as exc: + except (TransportError, WorkspaceUnavailable) as exc: note = f"workspace unreachable: {exc}" if any(d.state == "uncertain" for d in deliveries): note = "delivery unknown: the last prompt was not replayed; check the reply, then send again if needed" @@ -782,11 +963,7 @@ async def identity(session_id: str) -> NativeSessionInfo | None: native_session_id=binding["native_session_id"], model=binding["model"], approval_policy=binding["approval_policy"], - herdr_pane_id=binding["herdr_pane_id"], - herdr_terminal_id=binding["herdr_terminal_id"], - herdr_workspace_id=binding["herdr_workspace_id"], - workspace_pod=ws.pod, - workspace_pod_uid=uid, + workspace_name=ws.workspace_name, workspace_ready=ready, agent_live=live, generation=binding["generation"], diff --git a/backend/src/mainloop/runtime/preview_proxy.py b/backend/src/mainloop/runtime/preview_proxy.py new file mode 100644 index 0000000..8abac10 --- /dev/null +++ b/backend/src/mainloop/runtime/preview_proxy.py @@ -0,0 +1,856 @@ +"""Owner-scoped wildcard preview proxy through the Substrate router CONNECT tunnel.""" + +from __future__ import annotations + +import asyncio +import base64 +import contextlib +import hashlib +import http.client +import io +import json +import logging +import os +import re +import socket +import struct +from dataclasses import dataclass +from urllib.parse import urlsplit + +from fastapi import FastAPI, Request, WebSocket +from fastapi.responses import HTMLResponse, Response, StreamingResponse +from mainloop.config import settings +from mainloop.db import db +from mainloop.runtime import workspace_adapter +from mainloop.runtime.substrate_workspace import SubstrateWorkspace + +_WORKSPACE_LABEL = re.compile(r"^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$") +_PORT_LABEL = re.compile( + r"^(?P[1-9][0-9]{0,4})--(?P[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)$" +) +_MAX_REQUEST_BYTES = 10 * 1024 * 1024 +_MAX_HEADER_BYTES = 64 * 1024 +_MAX_WEBSOCKET_MESSAGE_BYTES = 16 * 1024 * 1024 +_PREVIEW_TOUCH_INTERVAL_SECONDS = 20.0 +_RESPONSE_READ_BLOCK_BYTES = 64 * 1024 +_SHIM_PORT = 8090 +_LOGGER = logging.getLogger(__name__) +_ACTIVE_PREVIEW_LEASES: dict[str, tuple[asyncio.Task[None], int]] = {} +_HOP_HEADERS = { + "connection", + "keep-alive", + "proxy-authenticate", + "proxy-authorization", + "proxy-connection", + "te", + "trailer", + "transfer-encoding", + "upgrade", +} +_PRIVATE_HEADERS = { + "authorization", + "cookie", + "cf-access-authenticated-user-email", + "x-user-id", +} + + +@dataclass(frozen=True, slots=True) +class PreviewHost: + port: int + workspace_id: str + + +@dataclass(frozen=True, slots=True) +class PreviewTarget: + workspace_id: str + user_id: str + atespace: str + actor: str + agent: str + shim_secret_name: str + manifest: dict + observed_state: str = "unknown" + + +class _PreviewPreForwardFailure(ConnectionError): + """The actor request was not sent, so retrying cannot duplicate application work.""" + + +class _PreviewForwardedFailure(ConnectionError): + """The actor request may have been applied, so its outcome is uncertain.""" + + +@dataclass(slots=True) +class _UpstreamHTTP: + sock: socket.socket + reader: object + status: int + headers: http.client.HTTPMessage + + def close(self) -> None: + try: + self.reader.close() + finally: + self.sock.close() + + +def parse_preview_host( + host_header: str, base_url: str | None = None +) -> PreviewHost | None: + """Parse `--.` without accepting path-like labels.""" + parsed_base = urlsplit(base_url or settings.substrate_preview_base_url) + domain = parsed_base.hostname + if not domain: + return None + try: + parsed_host = urlsplit(f"//{host_header}") + except ValueError: + return None + hostname = (parsed_host.hostname or "").lower().rstrip(".") + suffix = "." + domain.lower().rstrip(".") + if not hostname.endswith(suffix): + return None + label = hostname[: -len(suffix)] + match = _PORT_LABEL.fullmatch(label) + if not match: + return None + try: + host_port = parsed_host.port + except ValueError: + return None + if parsed_base.port is not None and host_port != parsed_base.port: + return None + port = int(match.group("port")) + workspace_id = match.group("workspace") + if port > 65535 or not _WORKSPACE_LABEL.fullmatch(workspace_id): + return None + return PreviewHost(port, workspace_id) + + +def _secret_name_for_workspace(atespace: str, actor: str) -> str: + for binding in settings.substrate_actor_bindings.values(): + if binding.atespace == atespace and binding.actor == actor: + return binding.shim_token_secret_name + return settings.shim_token_secret_name(atespace, actor) + + +async def _resolve_target(workspace_id: str, user_id: str) -> PreviewTarget | None: + async with db.connection() as conn: + row = await conn.fetchrow( + """SELECT b.*, + s.user_id, l.manifest, l.observed_state AS lifecycle_state, + n.kind AS agent_kind + FROM workspace_bindings b + JOIN sessions s ON s.id=b.workspace_id + LEFT JOIN workspace_lifecycles l ON l.workspace_id=b.workspace_id + LEFT JOIN native_bindings n ON n.session_id=b.workspace_id + WHERE b.workspace_id=$1""", + workspace_id, + ) + if row is None or row["user_id"] != user_id: + return None + record = dict(row) + manifest = record.get("manifest") or {} + if isinstance(manifest, str): + try: + manifest = json.loads(manifest) + except json.JSONDecodeError: + manifest = {} + if not isinstance(manifest, dict): + manifest = {} + atespace = record["atespace"] + actor = record["actor_name"] + return PreviewTarget( + workspace_id=record["workspace_id"], + user_id=user_id, + atespace=atespace, + actor=actor, + agent=( + record.get("agent_kind") + if record.get("agent_kind") in {"claude", "codex"} + else "claude" + ), + shim_secret_name=record.get("shim_token_secret_name") + or _secret_name_for_workspace(atespace, actor), + manifest=manifest, + observed_state=record.get("lifecycle_state") + or ("running" if record.get("observed_state") == "ready" else "unknown"), + ) + + +def declared_ports(manifest: dict) -> dict[int, str]: + """Read only explicit HTTP preview ports from current and dev-section manifest shapes.""" + declared: dict[int, str] = {} + candidates = [manifest.get("forwardPorts", []), manifest.get("ports", [])] + dev = manifest.get("dev") + if isinstance(dev, dict): + candidates.append(dev.get("ports", [])) + for candidate in candidates: + if not isinstance(candidate, (list, tuple)): + continue + for item in candidate: + if isinstance(item, bool): + continue + if isinstance(item, int): + port, name = item, f"Port {item}" + elif isinstance(item, dict): + port = item.get("number", item.get("port")) + name = item.get("name") + if not isinstance(name, str) or not name.strip(): + name = f"Port {port}" + else: + continue + if ( + isinstance(port, int) + and not isinstance(port, bool) + and 1 <= port <= 65535 + ): + declared[port] = name + return declared + + +async def _reported_ports(target: PreviewTarget) -> tuple[int, ...]: + workspace = SubstrateWorkspace( + atespace=target.atespace, + actor=target.actor, + agent=target.agent, + shim_token_secret_name=target.shim_secret_name, + timeout=settings.substrate_preview_connect_timeout_seconds, + ) + try: + return tuple( + port for port in await workspace.listening_ports() if port != _SHIM_PORT + ) + except Exception: + # Declared ports remain usable while the shim is waking or port discovery is unavailable. + return () + + +async def workspace_preview_ports( + workspace_id: str, user_id: str +) -> list[dict[str, object]] | None: + target = await _resolve_target(workspace_id, user_id) + if target is None: + return None + ports = declared_ports(target.manifest) + if target.observed_state == "running": + for port in await _reported_ports(target): + ports.setdefault(port, f"Port {port}") + return [ + {"port": port, "name": ports[port], "url": preview_url(workspace_id, port)} + for port in sorted(ports) + ] + + +def preview_url(workspace_id: str, port: int) -> str: + base = urlsplit(settings.substrate_preview_base_url) + host = base.hostname or "preview.localhost" + netloc = f"{port}--{workspace_id}.{host}" + if base.port is not None: + netloc += f":{base.port}" + return f"{base.scheme or 'http'}://{netloc}" + + +async def _touch_preview(workspace_id: str) -> None: + await workspace_adapter.touch_workspace(workspace_id, reason="preview") + + +def _preview_user_id(headers) -> str | None: + """Use only an explicitly trusted ingress identity or the Kind-only local identity.""" + if os.environ.get("SUBSTRATE_PREVIEW_LOCAL_DEV_MODE", "").strip().lower() in { + "1", + "true", + "yes", + }: + return "local-dev-user" + if os.environ.get("SUBSTRATE_PREVIEW_TRUSTED_INGRESS", "").strip().lower() not in { + "1", + "true", + "yes", + }: + return None + value = headers.get("cf-access-authenticated-user-email", "").strip() + return value or None + + +async def _keep_preview_awake(workspace_id: str) -> None: + while True: + await asyncio.sleep(_PREVIEW_TOUCH_INTERVAL_SECONDS) + try: + await _touch_preview(workspace_id) + except Exception as exc: + # Activity accounting must not terminate an otherwise healthy preview stream. + _LOGGER.warning( + "preview activity touch failed for workspace %s (%s)", + workspace_id, + type(exc).__name__, + ) + + +@contextlib.asynccontextmanager +async def _preview_activity_lease(workspace_id: str): + lease = _ACTIVE_PREVIEW_LEASES.get(workspace_id) + if lease is None: + task = asyncio.create_task(_keep_preview_awake(workspace_id)) + _ACTIVE_PREVIEW_LEASES[workspace_id] = (task, 1) + else: + task, count = lease + _ACTIVE_PREVIEW_LEASES[workspace_id] = (task, count + 1) + try: + yield + finally: + current_task, count = _ACTIVE_PREVIEW_LEASES[workspace_id] + if count == 1: + del _ACTIVE_PREVIEW_LEASES[workspace_id] + current_task.cancel() + await asyncio.gather(current_task, return_exceptions=True) + else: + _ACTIVE_PREVIEW_LEASES[workspace_id] = (current_task, count - 1) + + +def _read_head(reader) -> tuple[int, http.client.HTTPMessage]: + status_line = reader.readline(8192) + if not status_line.endswith(b"\r\n"): + raise ValueError("invalid upstream status line") + parts = status_line.decode("latin1").strip().split(" ", 2) + if len(parts) < 2 or not parts[0].startswith("HTTP/"): + raise ValueError("invalid upstream status line") + status = int(parts[1]) + headers = http.client.parse_headers(reader, _class=http.client.HTTPMessage) + if sum(len(key) + len(value) for key, value in headers.items()) > _MAX_HEADER_BYTES: + raise ValueError("upstream headers exceeded the bounded size") + return status, headers + + +def _connect_router( + target: PreviewTarget, + port: int, + timeout: float, + *, + method: str, + path: str, + headers: list[tuple[str, str]], + body: bytes, +) -> _UpstreamHTTP: + router = urlsplit(settings.substrate_router_address) + if router.scheme != "http" or not router.hostname: + raise ValueError("preview router address must be an HTTP origin") + sock = None + reader = None + forwarding_started = False + try: + sock = socket.create_connection( + (router.hostname, router.port or 80), timeout=timeout + ) + sock.settimeout(timeout) + reader = sock.makefile("rb") + upstream_host = f"actor-upstream:{port}" + connect_request = ( + f"CONNECT {upstream_host} HTTP/1.1\r\n" + f"Host: {upstream_host}\r\n" + f"ate-target-actor: {target.atespace}/{target.actor}\r\n" + "Connection: keep-alive\r\n\r\n" + ).encode("ascii") + sock.sendall(connect_request) + connect_status, _ = _read_head(reader) + if connect_status != 200: + raise _PreviewPreForwardFailure( + f"router CONNECT returned HTTP {connect_status}" + ) + request_headers = [ + (name, value) + for name, value in headers + if name.lower() not in _HOP_HEADERS | _PRIVATE_HEADERS | {"content-length"} + ] + request_headers.extend( + [ + ("Host", upstream_host), + ("Connection", "close"), + ("Content-Length", str(len(body))), + ] + ) + wire = ( + f"{method} {path} HTTP/1.1\r\n" + + "".join(f"{name}: {value}\r\n" for name, value in request_headers) + + "\r\n" + ) + # A failing sendall may still have transmitted a prefix of a mutating request. + forwarding_started = True + sock.sendall(wire.encode("latin1") + body) + status, response_headers = _read_head(reader) + return _UpstreamHTTP(sock, reader, status, response_headers) + except Exception as exc: + if reader is not None: + with contextlib.suppress(Exception): + reader.close() + if sock is not None: + with contextlib.suppress(Exception): + sock.close() + if isinstance(exc, (_PreviewPreForwardFailure, _PreviewForwardedFailure)): + raise + failure = ( + _PreviewForwardedFailure + if forwarding_started + else _PreviewPreForwardFailure + ) + raise failure("preview router connection failed") from exc + + +def _response_headers(headers: http.client.HTTPMessage) -> list[tuple[str, str]]: + connection_tokens = { + part.strip().lower() + for value in headers.get_all("connection", []) + for part in value.split(",") + } + blocked = _HOP_HEADERS | connection_tokens + return [ + (name.lower(), value) + for name, value in headers.items() + if name.lower() not in blocked + ] + + +def _response_body(reader, headers: http.client.HTTPMessage, no_body: bool): + if no_body: + return + transfer = headers.get("transfer-encoding", "").lower() + if "chunked" in transfer: + while True: + line = reader.readline(8192) + if not line or len(line) >= 8192 or not line.endswith(b"\r\n"): + raise ValueError("invalid upstream chunk header") + raw_size = line.split(b";", 1)[0].strip() + if not re.fullmatch(rb"[0-9a-fA-F]+", raw_size): + raise ValueError("invalid upstream chunk size") + size = int(raw_size, 16) + if size == 0: + trailer_bytes = 0 + while True: + trailer = reader.readline(8192) + trailer_bytes += len(trailer) + if not trailer or trailer_bytes > _MAX_HEADER_BYTES: + raise ValueError("invalid upstream chunk trailers") + if trailer in (b"\r\n", b"\n"): + break + return + remaining = size + while remaining: + chunk = reader.read(min(_RESPONSE_READ_BLOCK_BYTES, remaining)) + if not chunk: + raise ValueError("truncated upstream response") + remaining -= len(chunk) + yield chunk + if reader.read(2) != b"\r\n": + raise ValueError("truncated upstream response") + length = headers.get("content-length") + if length is not None: + if not re.fullmatch(r"[0-9]+", length.strip()): + raise ValueError("invalid upstream content length") + remaining = int(length) + while remaining: + chunk = reader.read(min(_RESPONSE_READ_BLOCK_BYTES, remaining)) + if not chunk: + raise ValueError("truncated upstream response") + remaining -= len(chunk) + yield chunk + return + while True: + chunk = reader.read(_RESPONSE_READ_BLOCK_BYTES) + if not chunk: + return + yield chunk + + +def _next_response_chunk(iterator) -> tuple[bool, bytes]: + try: + return True, next(iterator) + except StopIteration: + return False, b"" + + +async def _stream_response_body( + upstream: _UpstreamHTTP, + workspace_id: str, + no_body: bool, +): + iterator = iter(_response_body(upstream.reader, upstream.headers, no_body)) + try: + async with _preview_activity_lease(workspace_id): + while True: + available, chunk = await asyncio.to_thread( + _next_response_chunk, iterator + ) + if not available: + return + yield chunk + finally: + upstream.close() + + +def _waking_page() -> HTMLResponse: + return HTMLResponse( + "Workspace waking up" + "

Workspace waking up

" + "

Reload this preview in a few seconds.

", + status_code=503, + headers={"retry-after": "2", "cache-control": "no-store"}, + ) + + +async def _preview_http(request: Request, parsed: PreviewHost) -> Response: + user_id = _preview_user_id(request.headers) + if user_id is None: + return Response("Authentication required", status_code=401) + target = await _resolve_target(parsed.workspace_id, user_id) + if target is None: + return Response("Workspace not found", status_code=404) + await _touch_preview(parsed.workspace_id) + allowed = set(declared_ports(target.manifest)) + allowed.update(await _reported_ports(target)) + if parsed.port not in allowed: + return Response("Preview port is not declared or listening", status_code=403) + chunks = bytearray() + async for chunk in request.stream(): + chunks.extend(chunk) + if len(chunks) > _MAX_REQUEST_BYTES: + return Response("Preview request is too large", status_code=413) + body = bytes(chunks) + raw_path = request.scope.get( + "raw_path", request.url.path.encode("ascii", errors="ignore") + ) + path = raw_path.decode("ascii", errors="replace") + if request.url.query: + path += "?" + request.url.query + headers = [ + (key.decode("latin1"), value.decode("latin1")) + for key, value in request.headers.raw + ] + timeout = settings.substrate_preview_connect_timeout_seconds + upstream = None + for attempt in range(2): + try: + upstream = await asyncio.to_thread( + _connect_router, + target, + parsed.port, + timeout, + method=request.method, + path=path, + headers=headers, + body=body, + ) + break + except _PreviewPreForwardFailure: + if attempt == 1: + return _waking_page() + except _PreviewForwardedFailure: + return Response( + "Preview request was forwarded but its outcome is unknown", + status_code=502, + headers={"cache-control": "no-store"}, + ) + except Exception: + return Response("Preview router is unavailable", status_code=502) + if upstream is None: + return _waking_page() + no_body = request.method == "HEAD" or upstream.status in (204, 304) + + response = StreamingResponse( + _stream_response_body(upstream, parsed.workspace_id, no_body), + status_code=upstream.status, + media_type=None, + ) + response.raw_headers = [ + (key.encode("latin1"), value.encode("latin1")) + for key, value in _response_headers(upstream.headers) + ] + return response + + +async def _read_async_head( + reader: asyncio.StreamReader, +) -> tuple[int, http.client.HTTPMessage]: + raw = await reader.readuntil(b"\r\n\r\n") + if len(raw) > _MAX_HEADER_BYTES: + raise ValueError("upstream headers exceeded the bounded size") + status_line, _, header_bytes = raw.partition(b"\r\n") + parts = status_line.decode("latin1").split(" ", 2) + if len(parts) < 2 or not parts[0].startswith("HTTP/"): + raise ValueError("invalid upstream status line") + return int(parts[1]), http.client.parse_headers(io.BytesIO(header_bytes)) + + +def _encode_websocket_frame(opcode: int, payload: bytes, *, masked: bool) -> bytes: + first = 0x80 | opcode + mask_bit = 0x80 if masked else 0 + size = len(payload) + if size < 126: + header = bytes([first, mask_bit | size]) + elif size < 65536: + header = bytes([first, mask_bit | 126]) + struct.pack("!H", size) + else: + header = bytes([first, mask_bit | 127]) + struct.pack("!Q", size) + if not masked: + return header + payload + mask = os.urandom(4) + masked_payload = bytes( + value ^ mask[index % 4] for index, value in enumerate(payload) + ) + return header + mask + masked_payload + + +async def _read_websocket_frame( + reader: asyncio.StreamReader, +) -> tuple[bool, int, bytes]: + first, second = await reader.readexactly(2) + final = bool(first & 0x80) + opcode = first & 0x0F + masked = bool(second & 0x80) + size = second & 0x7F + if size == 126: + size = struct.unpack("!H", await reader.readexactly(2))[0] + elif size == 127: + size = struct.unpack("!Q", await reader.readexactly(8))[0] + if size > _MAX_WEBSOCKET_MESSAGE_BYTES: + raise ValueError("websocket message exceeded the bounded size") + mask = await reader.readexactly(4) if masked else b"" + payload = await reader.readexactly(size) + if masked: + payload = bytes(value ^ mask[index % 4] for index, value in enumerate(payload)) + return final, opcode, payload + + +async def _relay_websocket( + websocket: WebSocket, + reader: asyncio.StreamReader, + writer: asyncio.StreamWriter, +) -> None: + async def downstream_to_actor() -> None: + while True: + message = await websocket.receive() + if message["type"] == "websocket.disconnect": + code = message.get("code", 1000) + writer.write( + _encode_websocket_frame(8, struct.pack("!H", code), masked=True) + ) + await writer.drain() + return + text = message.get("text") + data = message.get("bytes") + if text is not None: + opcode, payload = 1, text.encode("utf-8") + elif data is not None: + opcode, payload = 2, data + else: + continue + writer.write(_encode_websocket_frame(opcode, payload, masked=True)) + await writer.drain() + + async def actor_to_downstream() -> None: + fragments = bytearray() + message_opcode: int | None = None + while True: + final, opcode, payload = await _read_websocket_frame(reader) + if opcode == 8: + code = ( + struct.unpack("!H", payload[:2])[0] if len(payload) >= 2 else 1000 + ) + await websocket.close(code=code) + return + if opcode == 9: + writer.write(_encode_websocket_frame(10, payload, masked=True)) + await writer.drain() + continue + if opcode == 10: + continue + if opcode in (1, 2): + fragments.clear() + message_opcode = opcode + fragments.extend(payload) + elif opcode == 0 and message_opcode is not None: + fragments.extend(payload) + else: + raise ValueError("invalid upstream websocket frame") + if len(fragments) > _MAX_WEBSOCKET_MESSAGE_BYTES: + raise ValueError("websocket message exceeded the bounded size") + if final and message_opcode is not None: + completed = bytes(fragments) + if message_opcode == 1: + await websocket.send_text(completed.decode("utf-8")) + else: + await websocket.send_bytes(completed) + fragments.clear() + message_opcode = None + + tasks = { + asyncio.create_task(downstream_to_actor()), + asyncio.create_task(actor_to_downstream()), + } + try: + done, pending = await asyncio.wait(tasks, return_when=asyncio.FIRST_COMPLETED) + for task in done: + task.result() + for task in pending: + task.cancel() + if pending: + await asyncio.gather(*pending, return_exceptions=True) + finally: + writer.close() + try: + await writer.wait_closed() + except OSError: + pass + + +async def _preview_websocket(websocket: WebSocket, parsed: PreviewHost) -> None: + user_id = _preview_user_id(websocket.headers) + if user_id is None: + await websocket.close(code=4401, reason="Authentication required") + return + target = await _resolve_target(parsed.workspace_id, user_id) + if target is None: + await websocket.close(code=4404, reason="Workspace not found") + return + await _touch_preview(parsed.workspace_id) + allowed = set(declared_ports(target.manifest)) + allowed.update(await _reported_ports(target)) + if parsed.port not in allowed: + await websocket.close(code=4403, reason="Preview port is not available") + return + + router = urlsplit(settings.substrate_router_address) + if router.scheme != "http" or not router.hostname: + await websocket.close(code=1011, reason="Preview router is unavailable") + return + raw_path = websocket.scope.get( + "raw_path", websocket.url.path.encode("ascii", errors="ignore") + ) + path = raw_path.decode("ascii", errors="replace") + if websocket.url.query: + path += "?" + websocket.url.query + key = websocket.headers.get("sec-websocket-key") + if not key: + await websocket.close(code=1002, reason="Invalid websocket handshake") + return + offered_protocols = [ + part.strip() + for part in websocket.headers.get("sec-websocket-protocol", "").split(",") + if part.strip() + ] + timeout = settings.substrate_preview_connect_timeout_seconds + + for attempt in range(2): + writer = None + forwarding_started = False + try: + reader, writer = await asyncio.wait_for( + asyncio.open_connection(router.hostname, router.port or 80), timeout + ) + writer.write( + ( + f"CONNECT actor-upstream:{parsed.port} HTTP/1.1\r\n" + f"Host: actor-upstream:{parsed.port}\r\n" + f"ate-target-actor: {target.atespace}/{target.actor}\r\n" + "Connection: keep-alive\r\n\r\n" + ).encode("ascii") + ) + await writer.drain() + connect_status, _ = await asyncio.wait_for( + _read_async_head(reader), timeout + ) + if connect_status != 200: + if attempt == 0: + writer.close() + await writer.wait_closed() + continue + break + + upstream_host = f"actor-upstream:{parsed.port}" + handshake_headers = [ + ("Host", upstream_host), + ("Upgrade", "websocket"), + ("Connection", "Upgrade"), + ("Sec-WebSocket-Key", key), + ("Sec-WebSocket-Version", "13"), + ] + origin = websocket.headers.get("origin") + if origin: + handshake_headers.append(("Origin", origin)) + if offered_protocols: + handshake_headers.append( + ("Sec-WebSocket-Protocol", ", ".join(offered_protocols)) + ) + excluded = ( + _HOP_HEADERS + | _PRIVATE_HEADERS + | { + "host", + "origin", + "sec-websocket-key", + "sec-websocket-version", + "sec-websocket-protocol", + "sec-websocket-extensions", + } + ) + for name, value in websocket.headers.items(): + if name.lower() not in excluded: + handshake_headers.append((name, value)) + wire = ( + f"GET {path} HTTP/1.1\r\n" + + "".join(f"{name}: {value}\r\n" for name, value in handshake_headers) + + "\r\n" + ) + forwarding_started = True + writer.write(wire.encode("latin1")) + await writer.drain() + status, headers = await asyncio.wait_for(_read_async_head(reader), timeout) + if status != 101: + writer.close() + await writer.wait_closed() + break + expected = base64.b64encode( + hashlib.sha1( + (key + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11").encode(), + usedforsecurity=False, + ).digest() + ).decode("ascii") + if headers.get("sec-websocket-accept") != expected: + raise ValueError("upstream websocket handshake was invalid") + protocol = headers.get("sec-websocket-protocol") + if protocol and protocol not in offered_protocols: + raise ValueError("upstream selected an unoffered websocket protocol") + await websocket.accept(subprotocol=protocol) + async with _preview_activity_lease(parsed.workspace_id): + await _relay_websocket(websocket, reader, writer) + return + except Exception: + if writer is not None: + writer.close() + try: + await writer.wait_closed() + except OSError: + pass + if forwarding_started or attempt == 1: + break + await websocket.close(code=1013, reason="Workspace waking up; retry shortly") + + +def register_preview_proxy(app: FastAPI) -> None: + """Install wildcard host handling without changing ordinary Mainloop API routing.""" + + @app.middleware("http") + async def preview_http_middleware(request: Request, call_next): + parsed = parse_preview_host(request.headers.get("host", "")) + if parsed is None: + return await call_next(request) + return await _preview_http(request, parsed) + + @app.websocket("/{path:path}") + async def preview_websocket_route(websocket: WebSocket, path: str): + del path + parsed = parse_preview_host(websocket.headers.get("host", "")) + if parsed is None: + await websocket.close(code=4404, reason="Preview route not found") + return + await _preview_websocket(websocket, parsed) diff --git a/backend/src/mainloop/runtime/substrate.py b/backend/src/mainloop/runtime/substrate.py new file mode 100644 index 0000000..17efb6b --- /dev/null +++ b/backend/src/mainloop/runtime/substrate.py @@ -0,0 +1,685 @@ +"""Thin Substrate adapter: drives ``kubectl ate`` (control-plane CLI over gRPC to +``ate-api-server``) to manage per-session actors as Mainloop workspaces. + +The actor image is designed for headless, per-turn native CLI invocations. This adapter talks to +Substrate's cluster-level control plane: +actors are created, suspended, resumed, reverted and deleted through ``ateapipb.Control`` (see +the pinned checkout's ``pkg/proto/ateapipb/ateapi.proto`` and +``cmd/kubectl-ate/internal/cmd/actor.go``). ``TransportError`` means the outcome of the call is +unknown; callers must inspect the actor before retrying rather than blindly re-creating it. +""" + +from __future__ import annotations + +import asyncio +import json +import logging +import shlex +import time +from dataclasses import dataclass +from enum import StrEnum +from typing import Callable + +from mainloop.config import settings + +logger = logging.getLogger(__name__) + + +class TransportError(RuntimeError): + """The ``kubectl ate`` call failed or timed out; whether it took effect is unknown.""" + + +class ActorState(StrEnum): + """Mirrors ``ateapipb.ActorState``; unrecognized strings map to UNSPECIFIED.""" + + UNSPECIFIED = "ACTOR_STATE_UNSPECIFIED" + RESUMING = "ACTOR_STATE_RESUMING" + RUNNING = "ACTOR_STATE_RUNNING" + SUSPENDING = "ACTOR_STATE_SUSPENDING" + SUSPENDED = "ACTOR_STATE_SUSPENDED" + PAUSING = "ACTOR_STATE_PAUSING" + PAUSED = "ACTOR_STATE_PAUSED" + CRASHED = "ACTOR_STATE_CRASHED" + DELETING = "ACTOR_STATE_DELETING" + REVERTING = "ACTOR_STATE_REVERTING" + + @classmethod + def parse(cls, raw: str | None) -> "ActorState": + try: + return cls(raw) + except ValueError: + return cls.UNSPECIFIED + + +# ActorState -> WorkspaceBinding.observed_state (models.native_agent). Only RUNNING is ready; +# terminal/absent states are unavailable; states mid-transition or unrecognized are unknown. +OBSERVED_STATE = { + ActorState.RUNNING: "ready", + ActorState.SUSPENDED: "unavailable", + ActorState.PAUSED: "unavailable", + ActorState.CRASHED: "unavailable", + ActorState.DELETING: "unavailable", + ActorState.RESUMING: "unknown", + ActorState.SUSPENDING: "unknown", + ActorState.PAUSING: "unknown", + ActorState.REVERTING: "unknown", + ActorState.UNSPECIFIED: "unknown", +} + + +@dataclass(frozen=True, slots=True) +class ExecResult: + exit_code: int + stdout: str + stderr: str + + +@dataclass(frozen=True, slots=True) +class ActorRecord: + """Parsed subset of an ``ateapipb.Actor`` (protojson via ``kubectl ate ... -o json``).""" + + atespace: str + name: str + uid: str | None + state: ActorState + external_snapshot_uri: str | None + current_actor_template_uid: str | None + raw: dict + + +def _actor_from_json(doc: dict) -> ActorRecord: + metadata = doc.get("metadata") or {} + status = doc.get("status") or {} + snapshot = status.get("externalSnapshot") or {} + return ActorRecord( + atespace=metadata.get("atespace", ""), + name=metadata.get("name", ""), + uid=metadata.get("uid"), + state=ActorState.parse(status.get("state")), + external_snapshot_uri=snapshot.get("snapshotUri"), + current_actor_template_uid=snapshot.get("actorTemplateUid"), + raw=doc, + ) + + +class GoldenState(StrEnum): + """Mirrors the ``ateapipb.GoldenSnapshotStatus`` lifecycle for an ActorTemplate.""" + + PENDING = "pending" + READY = "ready" + FAILED = "failed" + + +@dataclass(frozen=True, slots=True) +class ActorTemplateRecord: + """Parsed subset of an ``ateapipb.ActorTemplate``, including golden-snapshot status.""" + + atespace: str + name: str + uid: str | None + golden_state: GoldenState + golden_tag: str | None + error_message: str + raw: dict + + +def _actor_template_from_json(doc: dict) -> ActorTemplateRecord: + metadata = doc.get("metadata") or {} + status = doc.get("status") or {} + golden = status.get("goldenSnapshotStatus") or {} + error_message = golden.get("errorMessage", "") + golden_tag_ref = golden.get("goldenTag") + golden_tag = golden_tag_ref.get("name") if golden_tag_ref else None + if error_message: + golden_state = GoldenState.FAILED + elif golden_tag: + golden_state = GoldenState.READY + else: + golden_state = GoldenState.PENDING + return ActorTemplateRecord( + atespace=metadata.get("atespace", ""), + name=metadata.get("name", ""), + uid=metadata.get("uid"), + golden_state=golden_state, + golden_tag=golden_tag, + error_message=error_message, + raw=doc, + ) + + +class WaitTimeout(RuntimeError): + """A bounded poll reached its deadline without observing a terminal outcome. Callers + must treat this as failure, never as an implied success.""" + + +class GoldenSnapshotFailed(RuntimeError): + """The template controller reported an error while building the golden snapshot.""" + + +class GoldenSnapshotTimeout(WaitTimeout): + """The golden snapshot did not reach a terminal state within the bound. Not success.""" + + +class NoEligibleWorker(WaitTimeout): + """No worker registered for the atespace within the bound. Not success.""" + + +class IdentityOutcome(StrEnum): + """Result of reconciling a persisted actor UID against the cluster's current state, + before a retry decides whether to create, resume, or refuse to touch an actor.""" + + ABSENT = "absent" # no live actor with this name; safe to create fresh + UNOWNED = "unowned" # live actor exists but no actor uid was persisted + MATCHES = "matches" # live actor's uid matches the persisted identity + DIVERGED = "diverged" # live actor exists under this name with a different uid + + +class IdentityConflict(RuntimeError): + """A live actor exists under the expected name but with a different uid than the + identity persisted from a prior run. Recreating or resuming it blindly could operate + on someone else's actor; this must be surfaced, not silently resolved.""" + + +def reconcile_actor_identity( + persisted_uid: str | None, live: ActorRecord | None +) -> IdentityOutcome: + if live is None: + return IdentityOutcome.ABSENT + if persisted_uid is None: + return IdentityOutcome.UNOWNED + if persisted_uid == live.uid: + return IdentityOutcome.MATCHES + return IdentityOutcome.DIVERGED + + +class SubstrateControl: + """Wraps ``kubectl ate`` for one (kubeconfig, context) pair. No retries, no caching.""" + + def __init__( + self, + *, + kubeconfig: str | None = None, + context: str | None = None, + endpoint: str | None = None, + token_file: str | None = None, + cli: str | None = None, + ): + self.kubeconfig = ( + kubeconfig if kubeconfig is not None else settings.substrate_kubeconfig + ) + self.context = context if context is not None else settings.substrate_context + self.endpoint = ( + endpoint if endpoint is not None else settings.substrate_endpoint + ) + self.token_file = ( + token_file if token_file is not None else settings.substrate_token_file + ) + self.cli = cli or settings.substrate_cli + + def _base_args(self) -> list[str]: + args = [self.cli] + if self.endpoint: + args += ["--endpoint", self.endpoint] + if self.token_file: + args += ["--token-file", self.token_file] + if self.kubeconfig: + args += ["--kubeconfig", self.kubeconfig] + if self.context: + args += ["--context", self.context] + return args + + async def _exec( + self, args: list[str], timeout: float = 45, stdin: str | None = None + ) -> ExecResult: + command = self._base_args() + args + try: + proc = await asyncio.create_subprocess_exec( + *command, + stdin=asyncio.subprocess.PIPE if stdin is not None else None, + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.PIPE, + ) + try: + out, err = await asyncio.wait_for( + proc.communicate(stdin.encode() if stdin is not None else None), + timeout=timeout, + ) + except TimeoutError as exc: + proc.kill() + await proc.wait() + raise TransportError( + f"{cli_quote(*command)} timed out after {timeout}s" + ) from exc + except OSError as exc: + raise TransportError(f"exec failed: {type(exc).__name__}: {exc}") from exc + if proc.returncode is None: + raise TransportError("kubectl ate did not report an exit status") + return ExecResult( + proc.returncode, out.decode(errors="replace"), err.decode(errors="replace") + ) + + async def get_actor(self, atespace: str, name: str) -> ActorRecord | None: + res = await self._exec( + ["get", "actor", name, "--atespace", atespace, "-o", "json"] + ) + if res.exit_code != 0: + if "not found" in res.stderr.lower() or "NotFound" in res.stderr: + return None + raise TransportError( + f"get actor failed (exit {res.exit_code}): {res.stderr.strip()[-300:]}" + ) + text = res.stdout.strip() + if not text: + return None + return _actor_from_json(json.loads(text)) + + async def create_actor( + self, atespace: str, name: str, *, template: str, tag: str | None = None + ) -> ActorRecord: + args = [ + "create", + "actor", + name, + "--atespace", + atespace, + "--template", + template, + "-o", + "json", + ] + if tag: + args += ["--tag", tag] + res = await self._exec(args, timeout=90) + if res.exit_code != 0: + raise RuntimeError( + f"create actor failed (exit {res.exit_code}): {res.stderr.strip()[-300:]}" + ) + return await self._require(atespace, name, res) + + async def resume_actor(self, atespace: str, name: str) -> ActorRecord: + res = await self._exec( + ["resume", "actor", name, "--atespace", atespace, "-o", "json"], timeout=90 + ) + if res.exit_code != 0: + raise RuntimeError( + f"resume actor failed (exit {res.exit_code}): {res.stderr.strip()[-300:]}" + ) + return await self._require(atespace, name, res) + + async def suspend_actor(self, atespace: str, name: str) -> ActorRecord: + res = await self._exec( + ["suspend", "actor", name, "--atespace", atespace, "-o", "json"], timeout=90 + ) + if res.exit_code != 0: + raise RuntimeError( + f"suspend actor failed (exit {res.exit_code}): {res.stderr.strip()[-300:]}" + ) + return await self._require(atespace, name, res) + + async def revert_actor(self, atespace: str, name: str) -> ActorRecord: + """Discard current execution, roll back to the last completed snapshot. Explicit only: + callers must have surfaced the possible loss of unsnapshotted work before calling this. + """ + res = await self._exec( + ["revert", "actor", name, "--atespace", atespace, "-o", "json"], timeout=90 + ) + if res.exit_code != 0: + raise RuntimeError( + f"revert actor failed (exit {res.exit_code}): {res.stderr.strip()[-300:]}" + ) + return await self._require(atespace, name, res) + + async def delete_actor( + self, atespace: str, name: str, *, any_state: bool = False + ) -> None: + args = ["delete", "actor", name, "--atespace", atespace] + if any_state: + args.append("--any-state") + res = await self._exec(args, timeout=60) + if res.exit_code != 0 and "not found" not in res.stderr.lower(): + raise RuntimeError( + f"delete actor failed (exit {res.exit_code}): {res.stderr.strip()[-300:]}" + ) + + async def _require(self, atespace: str, name: str, res: ExecResult) -> ActorRecord: + text = res.stdout.strip() + if text: + return _actor_from_json(json.loads(text)) + # Some verbs may not echo the actor; read it back rather than guessing its state. + actor = await self.get_actor(atespace, name) + if actor is None: + raise TransportError(f"actor {atespace}/{name} not found after operation") + return actor + + async def atespace_exists(self, name: str) -> bool: + res = await self._exec(["get", "atespaces", name, "-o", "json"]) + if res.exit_code == 0: + return True + if "not found" in res.stderr.lower(): + return False + raise TransportError( + f"get atespace failed (exit {res.exit_code}): {res.stderr.strip()[-300:]}" + ) + + async def ensure_atespace(self, name: str) -> None: + """Register the atespace via the control-plane API. Idempotent: a Kubernetes + Namespace of the same name is a separate, unrelated object and does not register + an atespace, so this call is required before an ActorTemplate or actor can be + created in it (``create actor-template``/``create actor`` require it to exist). + """ + res = await self._exec(["create", "atespace", name, "-o", "json"]) + if res.exit_code == 0: + return + if "already exists" in res.stderr.lower() or "AlreadyExists" in res.stderr: + return + raise TransportError( + f"create atespace failed (exit {res.exit_code}): {res.stderr.strip()[-300:]}" + ) + + async def get_actor_template( + self, atespace: str, name: str + ) -> ActorTemplateRecord | None: + res = await self._exec( + ["get", "actor-template", name, "-a", atespace, "-o", "json"] + ) + if res.exit_code != 0: + if "not found" in res.stderr.lower() or "NotFound" in res.stderr: + return None + raise TransportError( + f"get actor-template failed (exit {res.exit_code}): " + f"{res.stderr.strip()[-300:]}" + ) + text = res.stdout.strip() + if not text: + return None + return _actor_template_from_json(json.loads(text)) + + async def create_actor_template( + self, atespace: str, name: str, manifest: str + ) -> ActorTemplateRecord: + """``manifest`` is the protojson-shaped ActorTemplate document; its own + ``metadata.atespace``/``metadata.name`` select where it is created. The atespace + must already exist (``ensure_atespace``); templates are immutable, so a changed + manifest needs a new name, never a reused one. The pinned CLI prints a table by + default, so JSON is requested explicitly. If the create result is uncertain, read + the named template back before reporting failure; a later invocation also starts + with that read and cannot blindly repeat the create.""" + try: + res = await self._exec( + ["create", "actor-template", "-f", "-", "-o", "json"], + stdin=manifest, + ) + except TransportError as create_error: + return await self._read_uncertain_actor_template_create( + atespace, name, create_error + ) + + text = res.stdout.strip() + if res.exit_code != 0: + reason = ( + f"create actor-template failed (exit {res.exit_code}): " + f"{res.stderr.strip()[-300:]}" + ) + return await self._read_uncertain_actor_template_create( + atespace, name, TransportError(reason) + ) + if not text: + return await self._read_uncertain_actor_template_create( + atespace, + name, + TransportError("create actor-template returned no output"), + ) + try: + return _actor_template_from_json(json.loads(text)) + except (json.JSONDecodeError, TypeError) as parse_error: + return await self._read_uncertain_actor_template_create( + atespace, + name, + TransportError( + f"create actor-template returned invalid JSON: {parse_error}" + ), + ) + + async def _read_uncertain_actor_template_create( + self, atespace: str, name: str, create_error: TransportError + ) -> ActorTemplateRecord: + try: + existing = await self.get_actor_template(atespace, name) + except TransportError as read_error: + raise TransportError( + f"actor-template create outcome is uncertain and read-back failed: " + f"{read_error}" + ) from create_error + if existing is not None: + return existing + raise create_error + + async def get_eligible_workers( + self, namespace: str, selector: str, sandbox_class: str + ) -> int: + """Count active workers with free actor capacity matching this WorkerPool. + + The pinned CLI's ``-a`` filter means "already hosting an actor in an atespace", + so it excludes the idle worker needed before the first actor exists. Namespace, + pool labels, and sandbox class identify the intended pool instead. + """ + res = await self._exec( + [ + "get", + "workers", + "-n", + namespace, + "-l", + selector, + "--sandbox-class", + sandbox_class, + "-o", + "json", + ] + ) + if res.exit_code != 0: + raise TransportError( + f"get workers failed (exit {res.exit_code}): {res.stderr.strip()[-300:]}" + ) + text = res.stdout.strip() + if not text: + return 0 + try: + doc = json.loads(text) + except json.JSONDecodeError as exc: + raise TransportError(f"get workers returned invalid JSON: {exc}") from exc + if not isinstance(doc, dict): + raise TransportError("get workers JSON is missing its 'workers' list") + # The pinned CLI uses protojson's default omission behavior: a valid + # ListWorkers response with zero matches is `{}`, not `{"workers": []}`. + # Treat only that empty object as the empty list; a non-empty object + # without the field is still a contract error. + if "workers" not in doc: + if doc: + raise TransportError("get workers JSON is missing its 'workers' list") + workers = [] + else: + workers = doc["workers"] + if not isinstance(workers, list): + raise TransportError("get workers JSON field 'workers' is not a list") + return sum( + _worker_is_eligible( + worker, + namespace=namespace, + selector=selector, + sandbox_class=sandbox_class, + ) + for worker in workers + ) + + +def cli_quote(*parts: str) -> str: + """Only for logging/evidence; commands are passed as argv, never through a shell.""" + return " ".join(shlex.quote(p) for p in parts) + + +def _worker_is_eligible( + worker: object, *, namespace: str, selector: str, sandbox_class: str +) -> bool: + if not isinstance(worker, dict): + return False + if worker.get("workerNamespace") != namespace: + return False + if worker.get("sandboxClass") != sandbox_class: + return False + key, separator, value = selector.partition("=") + if not separator or not key or not value: + raise ValueError(f"unsupported worker label selector: {selector!r}") + labels = worker.get("labels") or {} + if not isinstance(labels, dict) or labels.get(key) != value: + return False + + status = worker.get("status") or {} + if not isinstance(status, dict) or status.get("state") != "WORKER_STATE_ACTIVE": + return False + capacity = status.get("capacity") or {} + allocated = status.get("allocated") or {} + if isinstance(capacity, dict) and capacity.get("actors") is not None: + allocated_actors = ( + allocated.get("actors", 0) if isinstance(allocated, dict) else 0 + ) + try: + if int(capacity["actors"]) - int(allocated_actors or 0) <= 0: + return False + except (TypeError, ValueError) as exc: + raise TransportError("worker actor capacity is not an integer") from exc + return True + + +async def wait_for_golden_snapshot( + control: SubstrateControl, + atespace: str, + name: str, + *, + timeout_s: float, + poll_interval_s: float = 3, + sleep=asyncio.sleep, + clock=time.monotonic, +) -> ActorTemplateRecord: + """Poll an ActorTemplate's golden-snapshot status to a terminal outcome. Raises + ``GoldenSnapshotFailed``/``GoldenSnapshotTimeout`` rather than returning normally on + anything but a completed golden tag -- a caller must never infer success from a log + line or from reaching this function without an exception being possible.""" + deadline = clock() + timeout_s + while True: + record = await control.get_actor_template(atespace, name) + if record is None: + raise TransportError( + f"actor-template {atespace}/{name} disappeared while awaiting its " + "golden snapshot" + ) + if record.golden_state is GoldenState.FAILED: + raise GoldenSnapshotFailed( + f"golden snapshot for {atespace}/{name} failed: {record.error_message}" + ) + if record.golden_state is GoldenState.READY: + return record + if clock() >= deadline: + raise GoldenSnapshotTimeout( + f"golden snapshot for {atespace}/{name} did not complete within " + f"{timeout_s}s (last state: {record.golden_state.value})" + ) + await sleep(poll_interval_s) + + +async def wait_for_eligible_worker( + control: SubstrateControl, + namespace: str, + selector: str, + sandbox_class: str, + *, + timeout_s: float, + poll_interval_s: float = 3, + sleep=asyncio.sleep, + clock=time.monotonic, +) -> int: + """Poll for at least one worker matching the intended WorkerPool. Raises on timeout rather + than proceeding to create an actor-template/actor against a pool with no capacity. + """ + deadline = clock() + timeout_s + while True: + count = await control.get_eligible_workers(namespace, selector, sandbox_class) + if count > 0: + return count + if clock() >= deadline: + raise NoEligibleWorker( + f"no eligible worker for namespace={namespace}, selector={selector}, " + f"sandbox_class={sandbox_class} within {timeout_s}s" + ) + await sleep(poll_interval_s) + + +class ActorHealthTimeout(WaitTimeout): + """The actor route did not return a healthy response within the readiness bound.""" + + +async def wait_for_actor_health( + health_check: Callable[[], bool], + *, + timeout_s: float, + poll_interval_s: float = 2, + sleep=asyncio.sleep, + clock=time.monotonic, +) -> None: + """Wait for a live actor-route ``/healthz`` check to succeed. + + A restored actor need not replay startup logs, so readiness is based on current service + health rather than a boot marker. + """ + deadline = clock() + timeout_s + while True: + if health_check(): + return + if clock() >= deadline: + raise ActorHealthTimeout( + f"actor /healthz did not return 200 within {timeout_s}s" + ) + await sleep(poll_interval_s) + + +class ActorFailedToStart(RuntimeError): + """An actor reached a terminal, non-running state (e.g. CRASHED) while awaiting + readiness. Distinct from ``WaitTimeout``: this is a reported failure, not a bound + running out.""" + + +_ACTOR_TERMINAL_FAILURE_STATES = frozenset({ActorState.CRASHED, ActorState.DELETING}) + + +async def wait_for_actor_running( + control: SubstrateControl, + atespace: str, + name: str, + *, + timeout_s: float, + poll_interval_s: float = 2, + sleep=asyncio.sleep, + clock=time.monotonic, +) -> ActorRecord: + """Poll an actor to RUNNING. Raises ``ActorFailedToStart`` on a terminal failure state + and ``WaitTimeout`` on exceeding the bound -- never returns normally except on RUNNING, + so a caller can never mistake "still starting" for success.""" + deadline = clock() + timeout_s + while True: + actor = await control.get_actor(atespace, name) + if actor is None: + raise TransportError( + f"actor {atespace}/{name} disappeared while awaiting readiness" + ) + if actor.state is ActorState.RUNNING: + return actor + if actor.state in _ACTOR_TERMINAL_FAILURE_STATES: + raise ActorFailedToStart( + f"actor {atespace}/{name} reached {actor.state.value} while awaiting readiness" + ) + if clock() >= deadline: + raise WaitTimeout( + f"actor {atespace}/{name} did not reach RUNNING within {timeout_s}s " + f"(last state: {actor.state.value})" + ) + await sleep(poll_interval_s) diff --git a/backend/src/mainloop/runtime/substrate_workspace.py b/backend/src/mainloop/runtime/substrate_workspace.py new file mode 100644 index 0000000..9409066 --- /dev/null +++ b/backend/src/mainloop/runtime/substrate_workspace.py @@ -0,0 +1,545 @@ +"""Native-session transport to a pre-created Substrate actor through its CONNECT router. + +The actor-local shim owns the native CLI turn and journal files. Delivery errors after +``POST /turn`` are unknown; callers must reconcile the journal and never replay blindly. +""" + +from __future__ import annotations + +import asyncio +import base64 +import http.client +import json +import logging +import re +import socket +from dataclasses import dataclass +from urllib.parse import urlencode, urlsplit + +from kubernetes import client, config +from kubernetes.client.rest import ApiException +from mainloop.config import settings +from mainloop.runtime.credential_broker import CredentialBroker +from mainloop.runtime.substrate import TransportError + +logger = logging.getLogger(__name__) + + +class WorkspaceUnavailable(RuntimeError): + """The Substrate actor is not ready; no turn was attempted.""" + + +@dataclass(frozen=True, slots=True) +class WorkspaceState: + name: str + uid: str | None + ready: bool + + +@dataclass(frozen=True, slots=True) +class JournalSlice: + file: str | None + total_lines: int + lines: list[tuple[int, str]] + + +_AGENT = re.compile(r"^(claude|codex)$") +_DNS_LABEL = re.compile(r"^[a-z0-9](?:[-a-z0-9]{0,61}[a-z0-9])?$") +_MAX_RESPONSE_BYTES = 2 * 1024 * 1024 +_JOURNAL_PAGE_SIZE = 200 +_ACTOR_PORT = 8090 +_SECRET_API: client.CoreV1Api | None = None + + +@dataclass(frozen=True, slots=True) +class _Response: + status: int + body: str + + +def _secret_api() -> client.CoreV1Api: + global _SECRET_API + if _SECRET_API is None: + try: + config.load_incluster_config() + except config.ConfigException: + config.load_kube_config() + _SECRET_API = client.CoreV1Api() + return _SECRET_API + + +def _read_secret_token(secret_name: str, namespace: str) -> str: + try: + secret = _secret_api().read_namespaced_secret(secret_name, namespace) + except ApiException as exc: + if exc.status == 404: + raise WorkspaceUnavailable( + "Substrate shim token Secret is unavailable" + ) from exc + raise TransportError( + f"Substrate shim token Secret read failed (status {exc.status})" + ) from exc + encoded = (secret.data or {}).get("token") + if not isinstance(encoded, str): + raise WorkspaceUnavailable("Substrate shim token Secret has no token key") + try: + token = base64.b64decode(encoded, validate=True).decode("utf-8").strip() + except (ValueError, UnicodeDecodeError) as exc: + raise WorkspaceUnavailable("Substrate shim token Secret is invalid") from exc + if not token: + raise WorkspaceUnavailable("Substrate shim token Secret is empty") + return token + + +def _read_headers(reader) -> tuple[int, http.client.HTTPMessage]: + status_line = reader.readline(8192) + if not status_line.endswith(b"\r\n"): + raise ValueError("invalid HTTP status line") + parts = status_line.decode("latin1").strip().split(" ", 2) + if len(parts) < 2 or not parts[0].startswith("HTTP/"): + raise ValueError("invalid HTTP status line") + status = int(parts[1]) + headers = http.client.parse_headers(reader) + return status, headers + + +def _read_body(reader, headers: http.client.HTTPMessage) -> bytes: + transfer_encoding = headers.get("transfer-encoding", "").lower() + if "chunked" in transfer_encoding: + chunks: list[bytes] = [] + size = 0 + while True: + line = reader.readline(8192) + if not line: + raise ValueError("truncated chunked HTTP response") + chunk_size = int(line.split(b";", 1)[0].strip(), 16) + if chunk_size == 0: + while reader.readline(8192) not in (b"\r\n", b"\n", b""): + pass + return b"".join(chunks) + size += chunk_size + if size > _MAX_RESPONSE_BYTES: + raise ValueError("HTTP response exceeded the bounded size") + chunk = reader.read(chunk_size) + if len(chunk) != chunk_size or reader.read(2) != b"\r\n": + raise ValueError("truncated chunked HTTP response") + chunks.append(chunk) + length = headers.get("content-length") + if length is not None: + size = int(length) + if size < 0 or size > _MAX_RESPONSE_BYTES: + raise ValueError("HTTP response exceeded the bounded size") + body = reader.read(size) + if len(body) != size: + raise ValueError("truncated HTTP response") + return body + body = reader.read(_MAX_RESPONSE_BYTES + 1) + if len(body) > _MAX_RESPONSE_BYTES: + raise ValueError("HTTP response exceeded the bounded size") + return body + + +def _router_request( + *, + host: str, + port: int, + actor: str, + atespace: str, + actor_port: int, + timeout: float, + method: str, + path: str, + token: str | None, + body: dict | None, +) -> _Response: + target = f"actor-upstream:{actor_port}" + sock = socket.create_connection((host, port), timeout=timeout) + sock.settimeout(timeout) + reader = sock.makefile("rb") + try: + connect = ( + f"CONNECT {target} HTTP/1.1\r\n" + f"Host: {target}\r\n" + f"ate-target-actor: {atespace}/{actor}\r\n" + "Connection: keep-alive\r\n\r\n" + ).encode("ascii") + sock.sendall(connect) + connect_status, _ = _read_headers(reader) + if connect_status != 200: + return _Response(connect_status, "") + + encoded_body = ( + json.dumps(body, separators=(",", ":")).encode() + if body is not None + else b"" + ) + headers = [f"Host: {target}", "Connection: close"] + if token is not None: + headers.append(f"Authorization: Bearer {token}") + if body is not None: + headers.extend( + [ + "Content-Type: application/json", + f"Content-Length: {len(encoded_body)}", + ] + ) + request = ( + f"{method} {path} HTTP/1.1\r\n" + "\r\n".join(headers) + "\r\n\r\n" + ).encode("ascii") + sock.sendall(request + encoded_body) + status, response_headers = _read_headers(reader) + response_body = _read_body(reader, response_headers) + return _Response(status, response_body.decode("utf-8", errors="replace")) + finally: + reader.close() + sock.close() + + +class SubstrateWorkspace: + """Drive one native agent in a pre-created Substrate actor.""" + + def __init__( + self, + *, + atespace: str, + actor: str, + agent: str, + shim_token_secret_name: str, + logical_session_id: str | None = None, + native_session_id: str | None = None, + router_address: str | None = None, + timeout: float | None = None, + credential_broker: CredentialBroker | None = None, + ): + if not _DNS_LABEL.fullmatch(atespace) or not _DNS_LABEL.fullmatch(actor): + raise ValueError("Substrate atespace and actor must be DNS labels") + if not _AGENT.fullmatch(agent): + raise ValueError("Substrate native agent must be claude or codex") + self.atespace = atespace + self.actor = actor + self.workspace_name = actor + self.agent = agent + if logical_session_id is not None and not re.fullmatch( + r"[A-Za-z0-9._:-]{1,256}", logical_session_id + ): + raise ValueError("Substrate logical session id is invalid") + self._logical_session_id = logical_session_id + self.native_session_id = native_session_id + self._resume_history = False + address = urlsplit(router_address or settings.substrate_router_address) + if ( + address.scheme != "http" + or not address.hostname + or address.username is not None + or address.password is not None + or address.path not in ("", "/") + or address.query + or address.fragment + ): + raise ValueError("Substrate router address must be an HTTP origin") + self.router_host = address.hostname + self.router_port = address.port or 80 + self.actor_port = _ACTOR_PORT + self.timeout = timeout or settings.substrate_resume_timeout_seconds + self.secret_namespace = settings.substrate_shim_secret_namespace + self.secret_name = shim_token_secret_name + if not _DNS_LABEL.fullmatch(self.secret_name): + raise ValueError("Substrate shim Secret name must be a DNS label") + self._token_value: str | None = None + self._token_installed = False + self._startup_options: dict[str, str] | None = None + self.credential_broker = credential_broker or CredentialBroker() + + def set_native_session_id(self, native_session_id: str | None) -> None: + self.native_session_id = native_session_id + + @property + def logical_session_id(self) -> str | None: + return self._logical_session_id + + async def _token(self) -> str: + if self._token_value is None: + self._token_value = await asyncio.to_thread( + _read_secret_token, self.secret_name, self.secret_namespace + ) + return self._token_value + + async def _install_token(self) -> None: + """Bootstrap a newly provisioned shim from its control-plane Secret.""" + if self._token_installed: + return + response = await self._request( + "POST", "/token", body={"token": await self._token()}, authenticated=False + ) + if response.status not in (201, 409): + raise RuntimeError( + f"Substrate shim token bootstrap failed (HTTP {response.status})" + ) + self._token_installed = True + + async def _request( + self, + method: str, + path: str, + *, + body: dict | None = None, + authenticated: bool = True, + ) -> _Response: + if authenticated: + await self._install_token() + token = await self._token() if authenticated else None + response = await self._exchange(method, path, token=token, body=body) + if response.status == 401 and authenticated: + self._token_value = None + if method == "POST" and path == "/turn": + raise RuntimeError( + "Substrate shim rejected its bearer token (HTTP 401); prompt was not retried" + ) + token = await self._token() + response = await self._exchange(method, path, token=token, body=body) + if response.status == 401: + self._token_value = None + if response.status == 503: + raise WorkspaceUnavailable( + "Substrate router or actor capacity is unavailable" + ) + return response + + async def _exchange( + self, method: str, path: str, *, token: str | None, body: dict | None + ) -> _Response: + try: + response = await asyncio.to_thread( + _router_request, + host=self.router_host, + port=self.router_port, + actor=self.actor, + atespace=self.atespace, + actor_port=self.actor_port, + timeout=self.timeout, + method=method, + path=path, + token=token, + body=body, + ) + except (OSError, TimeoutError, ValueError, http.client.HTTPException) as exc: + raise TransportError( + f"Substrate router request failed: {type(exc).__name__}" + ) from exc + return response + + def _json(self, response: _Response, *, method: str) -> dict: + if response.status == 401: + raise RuntimeError("Substrate shim rejected its bearer token (HTTP 401)") + if response.status == 409: + raise RuntimeError("Substrate shim rejected the concurrent turn (HTTP 409)") + if not 200 <= response.status < 300: + raise RuntimeError( + f"Substrate shim {method} failed (HTTP {response.status})" + ) + try: + document = json.loads(response.body) + except json.JSONDecodeError as exc: + raise TransportError("Substrate shim returned invalid JSON") from exc + if not isinstance(document, dict): + raise TransportError("Substrate shim returned an invalid response") + return document + + async def workspace_state(self) -> WorkspaceState: + try: + response = await self._request("GET", "/healthz", authenticated=False) + except WorkspaceUnavailable: + return WorkspaceState(self.actor, None, False) + if response.status == 503: + return WorkspaceState(self.actor, None, False) + if response.status != 200: + raise TransportError( + f"Substrate health check failed (HTTP {response.status})" + ) + return WorkspaceState(self.actor, None, True) + + async def require_ready(self) -> WorkspaceState: + state = await self.workspace_state() + if not state.ready: + raise WorkspaceUnavailable( + f"Substrate actor {self.atespace}/{self.actor} is not ready" + ) + return state + + async def prepare_credentials(self) -> None: + """Install provider-shaped placeholders; the real credential stays in its Secret.""" + if self.agent == "codex": + name = "codex-auth" + placeholder = await self.credential_broker.codex_placeholder_auth() + else: + name = "claude-token" + placeholder = await self.credential_broker.claude_placeholder_token() + response = await self._request( + "PUT", + "/credential", + body={"name": name, "contents": placeholder}, + ) + if response.status not in (200, 201): + raise RuntimeError( + f"Substrate {self.agent.title()} placeholder delivery failed (HTTP {response.status})" + ) + + async def agent_status(self, name: str) -> dict | None: + if not name: + raise ValueError("native agent name is required") + query = urlencode( + {"agent": self.agent, "session_key": self._require_session_key()} + ) + response = await self._request("GET", f"/turn/status?{query}") + if response.status == 404: + return None + return self._json(response, method="GET /turn/status") + + async def start( + self, + binding: str, + name: str, + *, + native_id: str | None, + resume: bool, + extra: dict[str, str] | None = None, + ) -> dict: + del binding, name + self.native_session_id = native_id + self._resume_history = resume + self.set_startup_options(extra) + await self.require_ready() + await self.prepare_credentials() + query = urlencode({"agent": self.agent}) + response = await self._request("GET", f"/agent/ready?{query}") + self._json(response, method="GET /agent/ready") + # The actor-local file is synthetic; the egress provider injects the current token. + # Starting the native CLI remains the shim's turn API's responsibility. + return {"actor": self.actor} + + def set_startup_options(self, extra: dict[str, str] | None) -> None: + """Keep the role-specific options for each shim-launched native turn.""" + if not extra: + self._startup_options = None + return + self._startup_options = { + "cwd_rel": extra["--cwd-rel"], + "token": extra["--token"], + "standing_b64": extra["--standing-b64"], + "approval_policy": extra["--approval-policy"], + **( + {"model": extra["--model"], "effort": extra["--effort"]} + if "--model" in extra and "--effort" in extra + else {} + ), + } + + async def send(self, name: str, text: str) -> None: + if not name: + raise ValueError("native agent name is required") + payload = { + "agent": self.agent, + "prompt": text, + "session_key": self._require_session_key(), + "resume": self._resume_history, + } + if self._startup_options is not None: + payload["startup_options"] = self._startup_options + if self.native_session_id: + payload["session_id"] = self.native_session_id + response = await self._request("POST", "/turn", body=payload) + self._json(response, method="POST /turn") + + async def stop(self, name: str) -> None: + if not name: + raise ValueError("native agent name is required") + response = await self._request( + "POST", + "/turn/stop", + body={"agent": self.agent, "session_key": self._require_session_key()}, + ) + self._json(response, method="POST /turn/stop") + + async def listening_ports(self) -> tuple[int, ...]: + """Return ports reported by the authenticated actor shim.""" + response = await self._request("GET", "/ports") + document = self._json(response, method="GET /ports") + ports = document.get("ports") + if not isinstance(ports, list) or any( + isinstance(port, bool) + or not isinstance(port, int) + or port < 1 + or port > 65535 + for port in ports + ): + raise TransportError("Substrate shim returned invalid listening ports") + return tuple(sorted(set(ports))) + + async def _latest_turn(self) -> dict | None: + query = urlencode( + {"agent": self.agent, "session_key": self._require_session_key()} + ) + response = await self._request("GET", f"/turn/status?{query}") + if response.status == 404: + return None + return self._json(response, method="GET /turn/status") + + async def credential_rejected(self) -> bool: + """Return only the shim's sanitized provider-auth rejection signal.""" + turn = await self._latest_turn() + return bool(turn and turn.get("credential_rejected") is True) + + async def native_id(self, name: str) -> str | None: + if not name: + raise ValueError("native agent name is required") + turn = await self._latest_turn() + native_id = turn.get("native_session_id") if turn else None + if isinstance(native_id, str) and native_id: + self.native_session_id = native_id + return native_id + return None + + async def journal(self, name: str, native_id: str, from_line: int) -> JournalSlice: + if not name: + raise ValueError("native agent name is required") + query = urlencode( + { + "agent": self.agent, + "session_key": self._require_session_key(), + "id": native_id, + "from": max(0, from_line), + "limit": _JOURNAL_PAGE_SIZE, + } + ) + response = await self._request("GET", f"/journal?{query}") + if response.status == 404: + return JournalSlice(None, 0, []) + document = self._json(response, method="GET /journal") + file = document.get("file") + total = document.get("total_lines") + raw_lines = document.get("lines") + if (file is not None and not isinstance(file, str)) or not isinstance( + total, int + ): + raise TransportError("Substrate journal response has invalid metadata") + if not isinstance(raw_lines, list): + raise TransportError("Substrate journal response has invalid lines") + lines: list[tuple[int, str]] = [] + for item in raw_lines: + if ( + not isinstance(item, dict) + or not isinstance(item.get("line"), int) + or not isinstance(item.get("text"), str) + ): + raise TransportError("Substrate journal response has an invalid line") + lines.append((item["line"], item["text"])) + return JournalSlice(file, total, lines) + + def _require_session_key(self) -> str: + if self._logical_session_id is None: + raise RuntimeError("Substrate transport is not bound to a logical session") + return self._logical_session_id + + def set_resume_history(self, resume: bool) -> None: + """Set the create-versus-resume intent for this session's next turn.""" + self._resume_history = resume diff --git a/backend/src/mainloop/runtime/workspace_adapter.py b/backend/src/mainloop/runtime/workspace_adapter.py new file mode 100644 index 0000000..9d6ad34 --- /dev/null +++ b/backend/src/mainloop/runtime/workspace_adapter.py @@ -0,0 +1,1326 @@ +"""Per-session Substrate workspace bindings: the durable mapping between a Mainloop session, +a Substrate actor, its snapshot, cross-referenced native session id, and Mainloop's ownership +generation for that actor. This is the adapter boundary described in ``.tasknotes/plan.md``: +Mainloop owns creation intent, desired state, retry policy and audit; Substrate owns isolated +actor compute and snapshots. Produces ``models.native_agent.WorkspaceBinding`` -- the existing +contract type -- rather than a parallel workspace model. + +Rules carried over from ``native_sessions.py`` / ``contracts.py`` rather than reinvented: +- Identity is persisted before an uncertain external call, and a retry re-inspects the actor and + this row before creating or mutating anything (no blind replay, no second writer). +- Every mutation is fenced by ``ownership_generation``: a stale caller's write is rejected, not + silently applied. +- ``CRASHED`` and revert are never automatic. Reverting is only reachable through + ``revert_workspace`` with an explicit acknowledgement that unsnapshotted work may be lost. +""" + +from __future__ import annotations + +import asyncio +import json +import logging +import uuid +from datetime import UTC, datetime, timedelta + +from mainloop.config import settings +from mainloop.db import db +from mainloop.runtime.contracts import ContractError, StaleOwnership +from mainloop.runtime.substrate import ( + OBSERVED_STATE, + ActorRecord, + ActorState, + SubstrateControl, + TransportError, +) + +from models import ( + CapabilityResult, + CapabilityState, + WorkspaceAgentKind, + WorkspaceBinding, + WorkspaceCondition, + WorkspaceConditionStatus, + WorkspaceDesiredState, + WorkspaceLifecycle, + WorkspaceManifest, + WorkspaceObservedState, + WorkspaceTransition, +) + +logger = logging.getLogger(__name__) + +_locks: dict[str, asyncio.Lock] = {} + +LIFECYCLE_STATE = { + ActorState.RUNNING: WorkspaceObservedState.RUNNING, + ActorState.SUSPENDING: WorkspaceObservedState.SUSPENDING, + ActorState.SUSPENDED: WorkspaceObservedState.SUSPENDED, + ActorState.RESUMING: WorkspaceObservedState.RESUMING, + ActorState.CRASHED: WorkspaceObservedState.FAILED, + ActorState.DELETING: WorkspaceObservedState.FAILED, + ActorState.PAUSED: WorkspaceObservedState.UNKNOWN, + ActorState.PAUSING: WorkspaceObservedState.UNKNOWN, + ActorState.REVERTING: WorkspaceObservedState.UNKNOWN, + ActorState.UNSPECIFIED: WorkspaceObservedState.UNKNOWN, +} + +BLOCKING_DELIVERY_STATES = {"recorded", "queued", "sending", "delivered", "uncertain"} + + +def _lock(session_id: str) -> asyncio.Lock: + return _locks.setdefault(session_id, asyncio.Lock()) + + +def actor_name(session_id: str) -> str: + return f"ml-{session_id[:16]}" + + +def _control() -> SubstrateControl: + return SubstrateControl() + + +async def get_workspace(session_id: str) -> dict | None: + async with db.connection() as conn: + row = await conn.fetchrow( + "SELECT * FROM workspace_bindings WHERE workspace_id=$1", session_id + ) + return dict(row) if row else None + + +def _binding_from_row(row: dict) -> WorkspaceBinding: + capabilities: tuple[CapabilityResult, ...] = () + if row["observed_state"] == "unavailable" and row["last_error"]: + # A row-level note (e.g. "actor missing", "actor CRASHED") becomes a typed capability + # result rather than free text on the contract model, per the proved/partial evidence rule. + note = row["last_error"] + capabilities = ( + CapabilityResult( + capability=( + "actor_crashed" if "CRASHED" in note.upper() else "actor_health" + ), + state=CapabilityState.PROVED, + # SubstrateControl always talks to a real (possibly Kind) cluster, so this + # in-binding health signal is "live" by construction. The four plan-mandated + # integration-gate CapabilityResults are separate records in the proof note, + # scored "fixture" or "live" by whatever trial produced their evidence_ref. + scope="live", + evidence_ref=f"substrate://{row['atespace']}/{row['actor_name']}", + detail=note, + ), + ) + return WorkspaceBinding( + workspace_id=row["workspace_id"], + runtime_endpoint=row["runtime_endpoint"] or row["preview_route"] or "unrouted", + observed_at=row["observed_at"] or row["updated_at"], + observed_state=row["observed_state"], + capabilities=capabilities, + ) + + +async def _insert_row( + session_id: str, + atespace: str, + name: str, + template: str, + actor: ActorRecord, + now: datetime, +) -> None: + route = f"{atespace}/{name}" + async with db.connection() as conn: + await conn.execute( + """INSERT INTO workspace_bindings + (workspace_id, atespace, actor_name, actor_template, preview_route, + runtime_endpoint, observed_state, observed_at, external_snapshot_uri) + VALUES ($1,$2,$3,$4,$5,$5,$6,$7,$8)""", + session_id, + atespace, + name, + template, + route, + OBSERVED_STATE[actor.state], + now, + actor.external_snapshot_uri, + ) + + +CRASHED_NOTE = ( + "actor CRASHED: it stopped running and lost anything since its last completed snapshot. " + "Substrate's actor record has no snapshot timestamp, so snapshot age cannot be reported here. " + "Resume is rejected in this state; only an explicit revert_workspace(acknowledge_loss=True) " + "restores it, discarding any unsnapshotted work." +) + + +async def _update_observed( + session_id: str, actor: ActorRecord, now: datetime, *, last_error: str | None = None +) -> None: + if last_error is None and actor.state == ActorState.CRASHED: + last_error = CRASHED_NOTE + async with db.connection() as conn: + await conn.execute( + """UPDATE workspace_bindings + SET observed_state=$2, observed_at=$3, external_snapshot_uri=$4, + last_error=$5, updated_at=NOW() + WHERE workspace_id=$1""", + session_id, + OBSERVED_STATE[actor.state], + now, + actor.external_snapshot_uri, + last_error, + ) + await ensure_workspace_lifecycle(session_id) + await _record_observation(session_id, actor=actor, binding_error=last_error) + + +async def _mark_missing(session_id: str, now: datetime) -> None: + async with db.connection() as conn: + await conn.execute( + """UPDATE workspace_bindings + SET observed_state='unavailable', observed_at=$2, last_error=$3, updated_at=NOW() + WHERE workspace_id=$1""", + session_id, + now, + "actor not found where this row expected one; not recreated automatically " + "(inspect and reconcile explicitly, or delete this row to allow a fresh actor)", + ) + + +async def _bump_generation(session_id: str, expected: int) -> None: + async with db.connection() as conn: + tag = await conn.execute( + """UPDATE workspace_bindings SET ownership_generation=ownership_generation+1, + updated_at=NOW() WHERE workspace_id=$1 AND ownership_generation=$2""", + session_id, + expected, + ) + if tag.endswith(" 0"): + raise StaleOwnership( + f"workspace_bindings.{session_id} is no longer at generation {expected}" + ) + + +def plan_ensure(row_exists: bool, actor_found: bool) -> str: + """Retry-safe provision-or-attach policy, factored out of ``ensure_workspace`` so it is + directly testable without a database or cluster. + + ``create``: no row and no actor -- first provision. ``attach``: an actor already exists + (whether or not this process created it), so observe it rather than creating another. + ``surface_gap``: this row believes it owns an actor that Substrate no longer has -- never + silently recreate under the same name; a human or a later explicit call must reconcile it. + """ + if actor_found: + return "attach" + return "create" if not row_exists else "surface_gap" + + +async def ensure_workspace( + session_id: str, *, control: SubstrateControl | None = None +) -> WorkspaceBinding: + """Idempotent provision-or-attach. Never creates a second actor for a row that already + believes it owns one; a gap between this row and Substrate's view is surfaced, not papered + over.""" + control = control or _control() + async with _lock(session_id): + row = await get_workspace(session_id) + atespace = settings.substrate_atespace + template = settings.substrate_actor_template + name = row["actor_name"] if row else actor_name(session_id) + try: + actor = await control.get_actor(atespace, name) + except TransportError: + if row is not None: + return _binding_from_row( + row + ) # unreachable now; last known state stands + raise + now = datetime.now(UTC) + action = plan_ensure(row is not None, actor is not None) + if action == "surface_gap": + await _mark_missing(session_id, now) + return _binding_from_row(await get_workspace(session_id)) # type: ignore[arg-type] + if action == "create": + actor = await control.create_actor(atespace, name, template=template) + await _insert_row(session_id, atespace, name, template, actor, now) + await ensure_workspace_lifecycle(session_id) + await _record_observation(session_id, actor=actor) + else: + await _update_observed(session_id, actor, now) + return _binding_from_row(await get_workspace(session_id)) # type: ignore[arg-type] + + +async def observe_workspace( + session_id: str, *, control: SubstrateControl | None = None +) -> WorkspaceBinding | None: + """Read-only reconcile: refresh observed_state/observed_at without changing desired state.""" + control = control or _control() + row = await get_workspace(session_id) + if row is None: + return None + now = datetime.now(UTC) + try: + actor = await control.get_actor(row["atespace"], row["actor_name"]) + except TransportError as exc: + logger.info("workspace observe skipped for %s: %s", session_id, exc) + return _binding_from_row(row) + if actor is None: + await _mark_missing(session_id, now) + else: + await _update_observed(session_id, actor, now) + return _binding_from_row(await get_workspace(session_id)) # type: ignore[arg-type] + + +async def resume_workspace( + session_id: str, *, control: SubstrateControl | None = None +) -> WorkspaceLifecycle: + return await _request_workspace_state( + session_id, WorkspaceDesiredState.RUNNING, control=control + ) + + +async def touch_workspace(workspace_id: str, *, reason: str) -> WorkspaceLifecycle: + """Record turn or preview activity and wake a parked workspace when needed.""" + if reason not in {"turn", "delivery", "preview"}: + raise ValueError("reason must be turn, delivery, or preview") + lifecycle = await ensure_workspace_lifecycle(workspace_id) + if lifecycle is None: + raise ContractError(f"no workspace binding for {workspace_id}") + async with db.connection() as conn: + async with conn.transaction(): + binding = await conn.fetchrow( + "SELECT workspace_id,desired_state FROM workspace_bindings WHERE workspace_id=$1 FOR UPDATE", + workspace_id, + ) + if binding is None: + raise ContractError(f"no workspace binding for {workspace_id}") + if binding.get("desired_state") == "deleting": + raise ContractError("The workspace is being deleted.") + current = await conn.fetchrow( + """SELECT desired_state, observed_state FROM workspace_lifecycles + WHERE workspace_id=$1 FOR UPDATE""", + workspace_id, + ) + if current is None: + raise ContractError(f"no workspace lifecycle for {workspace_id}") + await conn.execute( + "UPDATE workspace_lifecycles SET last_activity_at=NOW(), updated_at=NOW() WHERE workspace_id=$1", + workspace_id, + ) + should_wake = current["desired_state"] == "suspended" or current[ + "observed_state" + ] in {"suspending", "suspended"} + if should_wake: + return await resume_workspace(workspace_id) + return await get_workspace_lifecycle(workspace_id) or lifecycle + + +async def suspend_idle_workspaces() -> int: + """Suspend idle dev workspaces via the normal generation and delivery fence.""" + async with db.connection() as conn: + rows = await conn.fetch( + """SELECT l.workspace_id + FROM workspace_lifecycles l + JOIN sessions s ON s.id=l.workspace_id + LEFT JOIN LATERAL ( + SELECT MAX(created_at) AS last_delivery_at + FROM native_deliveries WHERE session_id=l.workspace_id + ) d ON TRUE + WHERE l.desired_state='running' AND l.observed_state='running' + AND l.manifest->'dev' IS NOT NULL + AND COALESCE((l.manifest->'dev'->>'idle_timeout_minutes')::integer, 0) > 0 + AND GREATEST(l.last_activity_at, COALESCE(d.last_delivery_at, l.last_activity_at)) + < NOW() - ((l.manifest->'dev'->>'idle_timeout_minutes')::integer * INTERVAL '1 minute')""" + ) + suspended = 0 + for row in rows: + try: + result = await suspend_workspace_if_idle(row["workspace_id"]) + if result is not None: + suspended += 1 + except ContractError: + # The fenced path records the reason; open deliveries are expected to be skipped. + continue + except Exception: + logger.exception( + "Idle workspace suspend failed for %s", row["workspace_id"] + ) + return suspended + + +async def suspend_workspace_if_idle( + workspace_id: str, *, control: SubstrateControl | None = None +) -> WorkspaceLifecycle | None: + """Recheck activity under the workspace row lock before reserving a suspend.""" + return await _request_workspace_state( + workspace_id, + WorkspaceDesiredState.SUSPENDED, + control=control, + only_if_idle=True, + ) + + +async def suspend_workspace( + session_id: str, *, control: SubstrateControl | None = None +) -> WorkspaceLifecycle: + return await _request_workspace_state( + session_id, WorkspaceDesiredState.SUSPENDED, control=control + ) + + +async def revert_workspace( + session_id: str, *, acknowledge_loss: bool, control: SubstrateControl | None = None +) -> WorkspaceBinding: + """Roll back to the actor's last completed snapshot, discarding any unsnapshotted work. + Never called implicitly by this module -- the caller (API layer) must have shown the user + the actor is CRASHED (or otherwise irrecoverable) and gotten explicit confirmation first. + """ + if not acknowledge_loss: + raise ContractError( + "revert_workspace requires acknowledge_loss=True: it discards unsnapshotted work" + ) + control = control or _control() + async with _lock(session_id): + row = await get_workspace(session_id) + if row is None: + raise ContractError(f"no workspace binding for session {session_id}") + actor = await control.revert_actor(row["atespace"], row["actor_name"]) + now = datetime.now(UTC) + await _update_observed( + session_id, + actor, + now, + last_error="reverted to last completed snapshot; any unsnapshotted work was lost", + ) + await _bump_generation(session_id, row["ownership_generation"]) + return _binding_from_row(await get_workspace(session_id)) # type: ignore[arg-type] + + +def is_crashed(binding_row: dict) -> bool: + return ( + binding_row["observed_state"] == "unavailable" + and binding_row.get("last_error") is not None + and "CRASHED" in (binding_row.get("last_error") or "").upper() + ) + + +def lifecycle_state(actor_state: ActorState) -> WorkspaceObservedState: + """Project Substrate actor status without treating unknown states as healthy.""" + return LIFECYCLE_STATE[actor_state] + + +def suspend_fence_reason(delivery_states: set[str]) -> str | None: + """Return why parking is unsafe while the durable delivery ledger is open.""" + if "recorded" in delivery_states: + return "DeliveryRecorded" + if delivery_states & {"sending", "delivered"}: + return "TurnInFlight" + if "uncertain" in delivery_states: + return "DeliveryUncertain" + if "queued" in delivery_states: + return "DeliveryQueued" + return None + + +def _suspend_fence_detail(reason: str) -> str: + return { + "DeliveryRecorded": "A recorded delivery must be reconciled before the workspace can be suspended.", + "TurnInFlight": "A native turn is still in flight; wait for it to finish before suspending.", + "DeliveryUncertain": "A delivery outcome is uncertain; reconcile it before suspending.", + "DeliveryQueued": "A queued delivery must be handled before the workspace can be suspended.", + }[reason] + + +def _json_value(value): + if isinstance(value, str): + return json.loads(value) + return value + + +def _condition( + condition_type: str, + status: WorkspaceConditionStatus, + reason: str, + message: str, + at: datetime, +) -> WorkspaceCondition: + return WorkspaceCondition( + type=condition_type, + status=status, + reason=reason, + message=message, + last_transition_time=at, + ) + + +def _conditions_for( + *, + observed: WorkspaceObservedState, + desired: WorkspaceDesiredState, + snapshot_ref: str | None, + reason: str, + message: str, + at: datetime, + previous: tuple[WorkspaceCondition, ...] = (), + operation_status: WorkspaceConditionStatus | None = None, + operation_reason: str | None = None, + operation_message: str | None = None, + suspend_allowed: tuple[bool, str, str] | None = None, +) -> tuple[WorkspaceCondition, ...]: + available_status = { + WorkspaceObservedState.RUNNING: WorkspaceConditionStatus.TRUE, + WorkspaceObservedState.SUSPENDED: WorkspaceConditionStatus.FALSE, + WorkspaceObservedState.FAILED: WorkspaceConditionStatus.FALSE, + }.get(observed, WorkspaceConditionStatus.UNKNOWN) + parked_status = ( + WorkspaceConditionStatus.TRUE + if observed == WorkspaceObservedState.SUSPENDED and snapshot_ref + else ( + WorkspaceConditionStatus.FALSE + if observed + in (WorkspaceObservedState.RUNNING, WorkspaceObservedState.FAILED) + or observed == WorkspaceObservedState.SUSPENDED + else WorkspaceConditionStatus.UNKNOWN + ) + ) + desired_status = ( + WorkspaceConditionStatus.TRUE + if observed.value == desired.value + else ( + WorkspaceConditionStatus.FALSE + if observed + in (WorkspaceObservedState.RUNNING, WorkspaceObservedState.SUSPENDED) + else WorkspaceConditionStatus.UNKNOWN + ) + ) + specs = [ + ("Available", available_status, reason, message), + ( + "Parked", + parked_status, + ( + "SnapshotConfirmed" + if parked_status == WorkspaceConditionStatus.TRUE + else ( + "SnapshotReferenceMissing" + if observed == WorkspaceObservedState.SUSPENDED + else reason + ) + ), + ( + "The suspended workspace has a recorded snapshot." + if parked_status == WorkspaceConditionStatus.TRUE + else ( + "Substrate reported suspension without a snapshot reference." + if observed == WorkspaceObservedState.SUSPENDED + else message + ) + ), + ), + ( + "DesiredState", + desired_status, + ( + "DesiredStateObserved" + if desired_status == WorkspaceConditionStatus.TRUE + else reason + ), + f"Desired {desired.value}; observed {observed.value}.", + ), + ( + "ControlOperation", + operation_status + or ( + WorkspaceConditionStatus.TRUE + if observed + in (WorkspaceObservedState.RUNNING, WorkspaceObservedState.SUSPENDED) + and desired_status == WorkspaceConditionStatus.TRUE + else WorkspaceConditionStatus.UNKNOWN + ), + operation_reason or reason, + operation_message or message, + ), + ] + if suspend_allowed is not None: + allowed, allowed_reason, allowed_message = suspend_allowed + specs.append( + ( + "SuspendAllowed", + ( + WorkspaceConditionStatus.TRUE + if allowed + else WorkspaceConditionStatus.FALSE + ), + allowed_reason, + allowed_message, + ) + ) + + old = {item.type: item for item in previous} + result = [] + for condition_type, status, item_reason, item_message in specs: + prior = old.get(condition_type) + changed = ( + prior is None + or prior.status != status + or prior.reason != item_reason + or prior.message != item_message + ) + result.append( + _condition( + condition_type, + status, + item_reason, + item_message, + at if changed else prior.last_transition_time, + ) + ) + return tuple(result) + + +def _transition( + previous: WorkspaceLifecycle | None, + state: WorkspaceObservedState, + reason: str, + at: datetime, +) -> WorkspaceTransition | None: + if previous is not None and previous.observed_state == state: + return previous.last_transition + return WorkspaceTransition( + from_state=previous.observed_state if previous else None, + to_state=state, + reason=reason, + occurred_at=at, + ) + + +def _actor_reason(state: ActorState) -> tuple[str, str]: + messages = { + ActorState.RUNNING: ( + "ActorRunning", + "Substrate reports the workspace actor running.", + ), + ActorState.RESUMING: ( + "ActorResuming", + "Substrate is restoring the workspace actor.", + ), + ActorState.SUSPENDING: ( + "ActorSuspending", + "Substrate is suspending the workspace actor.", + ), + ActorState.SUSPENDED: ( + "ActorSuspended", + "Substrate reports the workspace actor suspended.", + ), + ActorState.CRASHED: ( + "ActorCrashed", + "Substrate reports the workspace actor crashed.", + ), + ActorState.DELETING: ( + "ActorDeleting", + "Substrate reports the workspace actor deleting.", + ), + ActorState.PAUSED: ( + "UnexpectedPaused", + "Substrate reported PAUSED; it is not confirmed parked.", + ), + ActorState.PAUSING: ( + "UnexpectedPausing", + "Substrate reported PAUSING; lifecycle is not confirmed.", + ), + ActorState.REVERTING: ( + "ActorReverting", + "Substrate is reverting the workspace actor.", + ), + ActorState.UNSPECIFIED: ( + "ActorStateUnknown", + "Substrate did not report a recognized actor state.", + ), + } + return messages[state] + + +def _lifecycle_from_row(row: dict) -> WorkspaceLifecycle: + payload = { + "workspace_id": row["workspace_id"], + "session_id": row["workspace_id"], + "desired_state": row["desired_state"], + "observed_state": row["observed_state"], + "manifest": _json_value(row["manifest"]), + "conditions": _json_value(row["conditions"]), + "last_transition": ( + _json_value(row["last_transition"]) if row.get("last_transition") else None + ), + "operation_id": row.get("operation_id"), + "snapshot_ref": row.get("snapshot_ref"), + "last_activity_at": row.get("last_activity_at"), + "ownership_generation": row["ownership_generation"], + "updated_at": row["updated_at"], + } + return WorkspaceLifecycle.model_validate_json( + json.dumps(payload, default=lambda value: value.isoformat()) + ) + + +def _manifest_from_session(row: dict) -> WorkspaceManifest: + raw_kind = row.get("agent_kind") + agent_kinds = ( + (WorkspaceAgentKind(raw_kind),) + if raw_kind in {kind.value for kind in WorkspaceAgentKind} + else () + ) + return WorkspaceManifest( + repo_url=row.get("repo_url"), + branch=row.get("branch_name") or row.get("base_branch") or "main", + agent_kinds=agent_kinds, + skills=(), + mcp_servers=(), + egress_allowlist=(), + resource_class="default", + dev=None, + ) + + +async def _get_lifecycle_row(workspace_id: str) -> dict | None: + async with db.connection() as conn: + row = await conn.fetchrow( + """SELECT l.*, b.ownership_generation, b.external_snapshot_uri + FROM workspace_lifecycles l + JOIN workspace_bindings b USING (workspace_id) + WHERE l.workspace_id=$1""", + workspace_id, + ) + return dict(row) if row else None + + +async def get_workspace_lifecycle(workspace_id: str) -> WorkspaceLifecycle | None: + row = await _get_lifecycle_row(workspace_id) + return _lifecycle_from_row(row) if row else None + + +async def ensure_workspace_lifecycle(workspace_id: str) -> WorkspaceLifecycle | None: + """Backfill a declarative manifest and lifecycle record for an existing actor binding.""" + async with db.connection() as conn: + row = await conn.fetchrow( + """SELECT b.*, s.repo_url, s.branch_name, s.base_branch, n.kind AS agent_kind + FROM workspace_bindings b + JOIN sessions s ON s.id=b.workspace_id + LEFT JOIN native_bindings n ON n.session_id=b.workspace_id + WHERE b.workspace_id=$1""", + workspace_id, + ) + if row is None: + return None + binding = dict(row) + existing = await get_workspace_lifecycle(workspace_id) + if existing: + return existing + + at = datetime.now(UTC) + initial_state = ( + WorkspaceObservedState.RUNNING + if binding["observed_state"] == "ready" + else WorkspaceObservedState.UNKNOWN + ) + reason = ( + "LifecycleInitialized" + if initial_state == WorkspaceObservedState.UNKNOWN + else "ActorRunning" + ) + message = ( + "Lifecycle tracking was initialized; refresh status to inspect Substrate." + if initial_state == WorkspaceObservedState.UNKNOWN + else "Substrate previously reported the workspace ready." + ) + manifest = _manifest_from_session(binding) + conditions = _conditions_for( + observed=initial_state, + desired=WorkspaceDesiredState.RUNNING, + snapshot_ref=binding.get("external_snapshot_uri"), + reason=reason, + message=message, + at=at, + ) + async with db.connection() as conn: + await conn.execute( + """INSERT INTO workspace_lifecycles + (workspace_id, desired_state, observed_state, manifest, conditions, snapshot_ref, + updated_at) + VALUES ($1,$2,$3,$4::jsonb,$5::jsonb,$6,$7) + ON CONFLICT (workspace_id) DO NOTHING""", + workspace_id, + WorkspaceDesiredState.RUNNING.value, + initial_state.value, + json.dumps(manifest.model_dump(mode="json")), + json.dumps([condition.model_dump(mode="json") for condition in conditions]), + binding.get("external_snapshot_uri"), + at, + ) + return await get_workspace_lifecycle(workspace_id) + + +async def list_workspace_lifecycles(user_id: str) -> list[WorkspaceLifecycle]: + async with db.connection() as conn: + rows = await conn.fetch( + """SELECT b.workspace_id FROM workspace_bindings b + JOIN sessions s ON s.id=b.workspace_id + WHERE s.user_id=$1 ORDER BY s.created_at DESC""", + user_id, + ) + for row in rows: + await ensure_workspace_lifecycle(row["workspace_id"]) + async with db.connection() as conn: + lifecycle_rows = await conn.fetch( + """SELECT l.*, b.ownership_generation, b.external_snapshot_uri + FROM workspace_lifecycles l + JOIN workspace_bindings b USING (workspace_id) + JOIN sessions s ON s.id=b.workspace_id + WHERE s.user_id=$1 ORDER BY s.created_at DESC""", + user_id, + ) + return [_lifecycle_from_row(dict(row)) for row in lifecycle_rows] + + +async def _save_lifecycle(conn, lifecycle: WorkspaceLifecycle) -> None: + await conn.execute( + """UPDATE workspace_lifecycles + SET desired_state=$2, observed_state=$3, conditions=$4::jsonb, + last_transition=$5::jsonb, operation_id=$6, snapshot_ref=$7, updated_at=$8 + WHERE workspace_id=$1""", + lifecycle.workspace_id, + lifecycle.desired_state.value, + lifecycle.observed_state.value, + json.dumps([item.model_dump(mode="json") for item in lifecycle.conditions]), + ( + json.dumps(lifecycle.last_transition.model_dump(mode="json")) + if lifecycle.last_transition + else None + ), + lifecycle.operation_id, + lifecycle.snapshot_ref, + lifecycle.updated_at, + ) + + +async def _record_observation( + workspace_id: str, + *, + actor: ActorRecord | None = None, + failure: tuple[WorkspaceObservedState, str, str] | None = None, + binding_error: str | None = None, + desired_state: WorkspaceDesiredState | None = None, + operation_status: WorkspaceConditionStatus | None = None, + operation_reason: str | None = None, + operation_message: str | None = None, + suspend_allowed: tuple[bool, str, str] | None = None, +) -> WorkspaceLifecycle: + previous = await ensure_workspace_lifecycle(workspace_id) + if previous is None: + raise ContractError(f"no workspace binding for {workspace_id}") + at = datetime.now(UTC) + if actor is not None: + state = lifecycle_state(actor.state) + reason, message = _actor_reason(actor.state) + snapshot_ref = ( + actor.external_snapshot_uri + if state == WorkspaceObservedState.SUSPENDED + else actor.external_snapshot_uri or previous.snapshot_ref + ) + observed_state_text = OBSERVED_STATE[actor.state] + last_error = ( + binding_error + if binding_error is not None + else CRASHED_NOTE if actor.state == ActorState.CRASHED else None + ) + if actor.state in (ActorState.PAUSED, ActorState.DELETING): + last_error = reason + async with db.connection() as conn: + await conn.execute( + """UPDATE workspace_bindings + SET observed_state=$2, observed_at=$3, external_snapshot_uri=$4, + last_error=$5, updated_at=NOW() WHERE workspace_id=$1""", + workspace_id, + observed_state_text, + at, + actor.external_snapshot_uri, + last_error, + ) + else: + if failure is None: + raise ValueError("actor or failure observation is required") + state, reason, message = failure + snapshot_ref = previous.snapshot_ref + + desired = desired_state or previous.desired_state + stable = state in ( + WorkspaceObservedState.RUNNING, + WorkspaceObservedState.SUSPENDED, + WorkspaceObservedState.FAILED, + ) + operation_id = None if stable else previous.operation_id + conditions = _conditions_for( + observed=state, + desired=desired, + snapshot_ref=snapshot_ref, + reason=reason, + message=message, + at=at, + previous=previous.conditions, + operation_status=operation_status, + operation_reason=operation_reason, + operation_message=operation_message, + suspend_allowed=suspend_allowed, + ) + updated = WorkspaceLifecycle( + workspace_id=workspace_id, + session_id=workspace_id, + desired_state=desired, + observed_state=state, + manifest=previous.manifest, + conditions=conditions, + last_transition=_transition(previous, state, reason, at), + operation_id=operation_id, + snapshot_ref=snapshot_ref, + ownership_generation=previous.ownership_generation, + updated_at=at, + ) + async with db.connection() as conn: + await _save_lifecycle(conn, updated) + return await get_workspace_lifecycle(workspace_id) or updated + + +async def refresh_workspace_lifecycle( + workspace_id: str, *, control: SubstrateControl | None = None +) -> WorkspaceLifecycle: + """Observe the actor once; transport errors remain unknown until a later refresh.""" + control = control or _control() + async with _lock(workspace_id): + row = await get_workspace(workspace_id) + if row is None: + raise ContractError(f"no workspace binding for {workspace_id}") + await ensure_workspace_lifecycle(workspace_id) + try: + actor = await control.get_actor(row["atespace"], row["actor_name"]) + except Exception: + return await _record_observation( + workspace_id, + failure=( + WorkspaceObservedState.UNKNOWN, + "ObservationUncertain", + "Substrate could not confirm workspace status. Refresh again before retrying an operation.", + ), + ) + if actor is None: + return await _record_observation( + workspace_id, + failure=( + WorkspaceObservedState.FAILED, + "ActorMissing", + "The workspace binding exists but Substrate returned no actor.", + ), + ) + return await _record_observation(workspace_id, actor=actor) + + +async def _record_operation_failure( + workspace_id: str, + *, + reason: str, + message: str, + uncertain: bool, +) -> WorkspaceLifecycle: + previous = await ensure_workspace_lifecycle(workspace_id) + if previous is None: + raise ContractError(f"no workspace binding for {workspace_id}") + at = datetime.now(UTC) + state = WorkspaceObservedState.UNKNOWN if uncertain else previous.observed_state + conditions = _conditions_for( + observed=state, + desired=previous.desired_state, + snapshot_ref=previous.snapshot_ref, + reason=reason, + message=message, + at=at, + previous=previous.conditions, + operation_status=( + WorkspaceConditionStatus.UNKNOWN + if uncertain + else WorkspaceConditionStatus.FALSE + ), + operation_reason=reason, + operation_message=message, + ) + updated = WorkspaceLifecycle( + workspace_id=previous.workspace_id, + session_id=previous.session_id, + desired_state=previous.desired_state, + observed_state=state, + manifest=previous.manifest, + conditions=conditions, + last_transition=_transition(previous, state, reason, at), + operation_id=previous.operation_id if uncertain else None, + snapshot_ref=previous.snapshot_ref, + ownership_generation=previous.ownership_generation, + updated_at=at, + ) + async with db.connection() as conn: + await _save_lifecycle(conn, updated) + return await get_workspace_lifecycle(workspace_id) or updated + + +async def _delivery_states(workspace_id: str, *, conn=None) -> set[str]: + if conn is None: + async with db.connection() as connection: + rows = await connection.fetch( + """SELECT state FROM native_deliveries + WHERE session_id=$1 AND state = ANY($2)""", + workspace_id, + list(BLOCKING_DELIVERY_STATES), + ) + else: + rows = await conn.fetch( + """SELECT state FROM native_deliveries + WHERE session_id=$1 AND state = ANY($2)""", + workspace_id, + list(BLOCKING_DELIVERY_STATES), + ) + return {row["state"] for row in rows} + + +async def _record_suspend_fence( + workspace_id: str, + reason: str, + message: str, + *, + previous: WorkspaceLifecycle | None = None, + conn=None, +) -> WorkspaceLifecycle: + previous = previous or await ensure_workspace_lifecycle(workspace_id) + if previous is None: + raise ContractError(f"no workspace binding for {workspace_id}") + at = datetime.now(UTC) + conditions = _conditions_for( + observed=previous.observed_state, + desired=previous.desired_state, + snapshot_ref=previous.snapshot_ref, + reason=reason, + message=message, + at=at, + previous=previous.conditions, + operation_status=WorkspaceConditionStatus.FALSE, + operation_reason=reason, + operation_message=message, + suspend_allowed=(False, reason, message), + ) + updated = WorkspaceLifecycle( + **{ + **previous.model_dump(), + "conditions": conditions, + "updated_at": at, + } + ) + if conn is None: + async with db.connection() as connection: + await _save_lifecycle(connection, updated) + else: + await _save_lifecycle(conn, updated) + if conn is not None: + return updated + return await get_workspace_lifecycle(workspace_id) or updated + + +async def _reserve_operation( + previous: WorkspaceLifecycle, + desired_state: WorkspaceDesiredState, + *, + only_if_idle: bool = False, +) -> WorkspaceLifecycle | None: + at = datetime.now(UTC) + transitional = ( + WorkspaceObservedState.SUSPENDING + if desired_state == WorkspaceDesiredState.SUSPENDED + else WorkspaceObservedState.RESUMING + ) + operation_id = str(uuid.uuid4()) + reason = ( + "SuspendRequested" + if desired_state == WorkspaceDesiredState.SUSPENDED + else "ResumeRequested" + ) + message = ( + f"Mainloop recorded a request to make the workspace {desired_state.value}." + ) + conditions = _conditions_for( + observed=transitional, + desired=desired_state, + snapshot_ref=previous.snapshot_ref, + reason=reason, + message=message, + at=at, + previous=previous.conditions, + operation_status=WorkspaceConditionStatus.UNKNOWN, + operation_reason=reason, + operation_message=message, + suspend_allowed=( + ( + True, + "NoOpenDelivery", + "No open or uncertain delivery blocks suspension.", + ) + if desired_state == WorkspaceDesiredState.SUSPENDED + else None + ), + ) + updated = WorkspaceLifecycle( + workspace_id=previous.workspace_id, + session_id=previous.session_id, + desired_state=desired_state, + observed_state=transitional, + manifest=previous.manifest, + conditions=conditions, + last_transition=_transition(previous, transitional, reason, at), + operation_id=operation_id, + snapshot_ref=previous.snapshot_ref, + ownership_generation=previous.ownership_generation, + updated_at=at, + ) + fence_detail: str | None = None + async with db.connection() as conn: + async with conn.transaction(): + binding = await conn.fetchrow( + """SELECT ownership_generation,desired_state FROM workspace_bindings + WHERE workspace_id=$1 FOR UPDATE""", + previous.workspace_id, + ) + if binding is None: + raise ContractError(f"no workspace binding for {previous.workspace_id}") + if binding.get("desired_state") == "deleting": + raise ContractError("The workspace is being deleted.") + if binding["ownership_generation"] != previous.ownership_generation: + raise StaleOwnership( + f"workspace {previous.workspace_id} changed during lifecycle request" + ) + if only_if_idle: + if desired_state != WorkspaceDesiredState.SUSPENDED: + raise ValueError("only_if_idle applies to suspension") + dev = previous.manifest.dev + if dev is None: + return None + activity = await conn.fetchrow( + """SELECT last_activity_at, + (SELECT MAX(created_at) FROM native_deliveries + WHERE session_id=$1) AS last_delivery_at + FROM workspace_lifecycles WHERE workspace_id=$1 FOR UPDATE""", + previous.workspace_id, + ) + if activity is None: + raise ContractError( + f"no workspace lifecycle for {previous.workspace_id}" + ) + last_active = max( + value + for value in ( + activity["last_activity_at"], + activity["last_delivery_at"], + ) + if value is not None + ) + if last_active > at - timedelta(minutes=dev.idle_timeout_minutes): + return None + if desired_state == WorkspaceDesiredState.SUSPENDED: + fence_reason = suspend_fence_reason( + await _delivery_states(previous.workspace_id, conn=conn) + ) + if fence_reason: + fence_detail = _suspend_fence_detail(fence_reason) + await _record_suspend_fence( + previous.workspace_id, + fence_reason, + fence_detail, + previous=previous, + conn=conn, + ) + if fence_detail is None: + tag = await conn.execute( + """UPDATE workspace_bindings + SET ownership_generation=ownership_generation+1, updated_at=NOW() + WHERE workspace_id=$1 AND ownership_generation=$2""", + previous.workspace_id, + previous.ownership_generation, + ) + if tag.endswith(" 0"): + raise StaleOwnership( + f"workspace {previous.workspace_id} changed during lifecycle request" + ) + await _save_lifecycle(conn, updated) + if fence_detail is not None: + raise ContractError(fence_detail) + return await get_workspace_lifecycle(previous.workspace_id) or updated + + +async def _request_workspace_state( + workspace_id: str, + desired_state: WorkspaceDesiredState, + *, + control: SubstrateControl | None = None, + only_if_idle: bool = False, +) -> WorkspaceLifecycle | None: + control = control or _control() + async with _lock(workspace_id): + previous = await ensure_workspace_lifecycle(workspace_id) + row = await get_workspace(workspace_id) + if previous is None or row is None: + raise ContractError(f"no workspace binding for {workspace_id}") + if ( + previous.desired_state == desired_state + and previous.observed_state.value == desired_state.value + and previous.operation_id is None + ): + return previous + + # A pending operation means a previous control call may have timed out. Its original + # intent is already durable; inspect that actor before deciding whether a new operation + # can be recorded. First attempts persist intent before the first Substrate call. + had_pending_operation = previous.operation_id is not None + if not had_pending_operation: + reserved = ( + await _reserve_operation(previous, desired_state, only_if_idle=True) + if only_if_idle + else await _reserve_operation(previous, desired_state) + ) + if reserved is None: + return None + previous = reserved + + try: + actor = await control.get_actor(row["atespace"], row["actor_name"]) + except Exception: + return await _record_operation_failure( + workspace_id, + reason="ObservationUncertain", + message="Substrate status is unknown. Refresh status before retrying the operation.", + uncertain=True, + ) + if actor is None: + return await _record_observation( + workspace_id, + failure=( + WorkspaceObservedState.FAILED, + "ActorMissing", + "The workspace binding exists but Substrate returned no actor.", + ), + desired_state=desired_state, + ) + + observed = lifecycle_state(actor.state) + if observed.value == desired_state.value: + if had_pending_operation and previous.desired_state != desired_state: + # Finish reconciling the old intent, then persist the new request. The observed + # actor already matches it, so no second control mutation is needed. + previous = await _record_observation( + workspace_id, actor=actor, desired_state=previous.desired_state + ) + await _reserve_operation(previous, desired_state) + return await _record_observation( + workspace_id, actor=actor, desired_state=desired_state + ) + if observed in ( + WorkspaceObservedState.SUSPENDING, + WorkspaceObservedState.RESUMING, + WorkspaceObservedState.UNKNOWN, + WorkspaceObservedState.FAILED, + ): + reason, message = _actor_reason(actor.state) + return await _record_observation( + workspace_id, + actor=actor, + operation_status=WorkspaceConditionStatus.UNKNOWN, + operation_reason=reason, + operation_message=message, + desired_state=( + previous.desired_state + if had_pending_operation and previous.desired_state != desired_state + else desired_state + ), + ) + + if had_pending_operation: + # The inspected actor is stable in the opposite state. Retire the uncertain + # operation record before persisting a fresh attempt; this is the required + # inspect-before-retry fence. + previous = await _record_observation( + workspace_id, actor=actor, desired_state=previous.desired_state + ) + previous = await _reserve_operation(previous, desired_state) + + if desired_state == WorkspaceDesiredState.SUSPENDED: + fence_reason = suspend_fence_reason(await _delivery_states(workspace_id)) + if fence_reason: + detail = _suspend_fence_detail(fence_reason) + await _record_observation( + workspace_id, + actor=actor, + desired_state=desired_state, + operation_status=WorkspaceConditionStatus.FALSE, + operation_reason=fence_reason, + operation_message=detail, + suspend_allowed=(False, fence_reason, detail), + ) + raise ContractError(detail) + + try: + if desired_state == WorkspaceDesiredState.SUSPENDED: + actor = await control.suspend_actor(row["atespace"], row["actor_name"]) + else: + actor = await control.resume_actor(row["atespace"], row["actor_name"]) + except TransportError: + return await _record_operation_failure( + workspace_id, + reason="OperationOutcomeUnknown", + message="Substrate did not confirm the operation. Refresh status before retrying; Mainloop will not replay it automatically.", + uncertain=True, + ) + except RuntimeError: + return await _record_operation_failure( + workspace_id, + reason="OperationRejected", + message="Substrate rejected the lifecycle request. Refresh status before retrying.", + uncertain=False, + ) + except Exception: + return await _record_operation_failure( + workspace_id, + reason="OperationOutcomeUnknown", + message="Substrate did not confirm the operation. Refresh status before retrying; Mainloop will not replay it automatically.", + uncertain=True, + ) + observed = lifecycle_state(actor.state) + if observed.value == desired_state.value and ( + desired_state != WorkspaceDesiredState.SUSPENDED + or actor.external_snapshot_uri is not None + ): + operation_status = WorkspaceConditionStatus.TRUE + operation_reason = "OperationConfirmed" + operation_message = ( + f"Substrate confirmed the workspace {desired_state.value}." + ) + elif observed in ( + WorkspaceObservedState.RUNNING, + WorkspaceObservedState.SUSPENDED, + WorkspaceObservedState.FAILED, + ): + operation_status = WorkspaceConditionStatus.FALSE + operation_reason = "DesiredStateNotReached" + operation_message = f"Substrate observed {observed.value}; desired {desired_state.value} was not confirmed." + else: + operation_status = WorkspaceConditionStatus.UNKNOWN + operation_reason = "ActorTransitionInProgress" + operation_message = ( + "Substrate has not reached a stable state; refresh status to reconcile." + ) + return await _record_observation( + workspace_id, + actor=actor, + desired_state=desired_state, + operation_status=operation_status, + operation_reason=operation_reason, + operation_message=operation_message, + ) diff --git a/backend/src/mainloop/runtime/workspace_api.py b/backend/src/mainloop/runtime/workspace_api.py new file mode 100644 index 0000000..165a559 --- /dev/null +++ b/backend/src/mainloop/runtime/workspace_api.py @@ -0,0 +1,466 @@ +"""Workspace lifecycle endpoints for branch workspace actors.""" + +import json +import uuid +from datetime import UTC, datetime +from typing import Annotated, Literal + +from fastapi import APIRouter, Header, HTTPException, Response +from fastapi.responses import JSONResponse +from mainloop.config import settings +from mainloop.db import db +from mainloop.runtime import workspace_adapter +from mainloop.runtime.actor_provisioner import get_actor_provisioner +from mainloop.runtime.contracts import ContractError +from mainloop.runtime.credential_broker import CredentialBroker, CredentialBrokerError +from mainloop.runtime.credential_reauth import ( + CredentialReauthRunner, + KubernetesCredentialReauthRunner, +) +from mainloop.runtime.preview_proxy import workspace_preview_ports +from mainloop.sse import notify_workspace_updated +from pydantic import BaseModel, ConfigDict, Field, StrictStr, field_validator + +from models import ( + WorkspaceAgentKind, + WorkspaceDev, + WorkspaceLifecycle, + WorkspaceManifest, + WorkspaceObservedState, +) + +router = APIRouter(prefix="/workspaces", tags=["workspaces"]) +_reauth_runner: CredentialReauthRunner = KubernetesCredentialReauthRunner() +_reauth_owners: dict[str, tuple[str, str]] = {} + + +class CreateWorkspaceRequest(BaseModel): + project_id: Annotated[StrictStr, Field(min_length=1)] + branch: Annotated[StrictStr, Field(min_length=1)] + dev: WorkspaceDev + agent_kind: Literal["claude", "codex"] = "claude" + + model_config = ConfigDict(extra="forbid", strict=True) + + @field_validator("branch") + @classmethod + def validate_branch_name(cls, value: str) -> str: + WorkspaceManifest(branch=value, resource_class="default") + return value + + +def _user_id(value: str | None) -> str: + return value or "local-dev-user" + + +async def _require_owned_workspace(workspace_id: str, user_id: str) -> None: + async with db.connection() as conn: + exists = await conn.fetchval( + """SELECT EXISTS ( + SELECT 1 FROM workspace_bindings b + JOIN sessions s ON s.id=b.workspace_id + WHERE b.workspace_id=$1 AND s.user_id=$2 + )""", + workspace_id, + user_id, + ) + if not exists: + raise HTTPException(status_code=404, detail="Workspace not found") + + +async def _publish(user_id: str, lifecycle: WorkspaceLifecycle) -> None: + await notify_workspace_updated(user_id, lifecycle.model_dump(mode="json")) + + +@router.post("", response_model=WorkspaceLifecycle) +async def create_workspace( + request: CreateWorkspaceRequest, + user_id: str | None = Header(default=None, alias="X-User-ID"), +): + """Create a branch workspace and its independent Substrate actor.""" + owner = _user_id(user_id) + workspace_id = str(uuid.uuid4()) + actor_name = f"ml-{workspace_id[:16]}" + atespace = settings.substrate_atespace + shim_token_secret_name = settings.shim_token_secret_name(atespace, actor_name) + template = request.dev.actor_template or settings.substrate_actor_template + + async with db.connection() as conn: + project = await conn.fetchrow( + "SELECT id, html_url FROM projects WHERE id=$1 AND user_id=$2", + request.project_id, + owner, + ) + if project is None: + raise HTTPException(status_code=404, detail="Project not found") + + manifest = WorkspaceManifest( + repo_url=project["html_url"], + branch=request.branch, + agent_kinds=(WorkspaceAgentKind(request.agent_kind),), + resource_class="default", + dev=request.dev, + ) + conversation_id = str(uuid.uuid4()) + now = datetime.now(UTC) + async with conn.transaction(): + thread = await conn.fetchrow( + "SELECT id FROM main_threads WHERE user_id=$1 ORDER BY created_at LIMIT 1", + owner, + ) + thread_id = thread["id"] if thread else str(uuid.uuid4()) + if thread is None: + await conn.execute( + "INSERT INTO main_threads (id,user_id) VALUES ($1,$2)", + thread_id, + owner, + ) + await conn.execute( + "INSERT INTO conversations (id,user_id,title) VALUES ($1,$2,$3)", + conversation_id, + owner, + f"{project['id']} · {request.branch}", + ) + await conn.execute( + """INSERT INTO sessions + (id,user_id,main_thread_id,title,description,prompt,conversation_id, + status,created_at,repo_url,project_id,branch_name,base_branch) + VALUES ($1,$2,$3,$4,$5,$6,$7,'active',$8,$9,$10,$11,$12)""", + workspace_id, + owner, + thread_id, + f"{project['id']} · {request.branch}", + "Branch development workspace", + "Development workspace", + conversation_id, + now, + project["html_url"], + request.project_id, + request.branch, + request.branch, + ) + await conn.execute( + """INSERT INTO workspace_bindings + (workspace_id,atespace,actor_name,actor_template, + shim_token_secret_name,observed_state,desired_state,created_at,updated_at) + VALUES ($1,$2,$3,$4,$5,'unknown','active',$6,$6)""", + workspace_id, + atespace, + actor_name, + template, + shim_token_secret_name, + now, + ) + await conn.execute( + """INSERT INTO workspace_lifecycles + (workspace_id,desired_state,observed_state,manifest,conditions, + last_activity_at,updated_at) + VALUES ($1,'running','unknown',$2::jsonb,'[]'::jsonb,$3,$3)""", + workspace_id, + json.dumps(manifest.model_dump(mode="json")), + now, + ) + from mainloop.runtime import native_sessions + + await native_sessions.create_binding( + workspace_id, request.agent_kind, conn=conn + ) + + try: + provisioner = get_actor_provisioner() + provisioned = await provisioner.create( + atespace=atespace, + actor_name=actor_name, + template=template, + shim_token_secret_name=shim_token_secret_name, + ) + lifecycle = await workspace_adapter._record_observation( + workspace_id, actor=provisioned.actor + ) + except Exception: + # The row and actor identity are durable before the external call. Keep them so a + # refresh can reconcile an outcome that timed out instead of creating a second actor. + lifecycle = await workspace_adapter._record_observation( + workspace_id, + failure=( + WorkspaceObservedState.UNKNOWN, + "ProvisioningUncertain", + "Actor provisioning did not return a confirmed result. Refresh status before retrying.", + ), + ) + await _publish(owner, lifecycle) + return JSONResponse(status_code=202, content=lifecycle.model_dump(mode="json")) + await _publish(owner, lifecycle) + return lifecycle + + +@router.get("", response_model=list[WorkspaceLifecycle]) +async def list_workspaces( + user_id: str | None = Header(default=None, alias="X-User-ID"), +): + return await workspace_adapter.list_workspace_lifecycles(_user_id(user_id)) + + +@router.get("/{workspace_id}", response_model=WorkspaceLifecycle) +async def get_workspace( + workspace_id: str, + user_id: str | None = Header(default=None, alias="X-User-ID"), +): + owner = _user_id(user_id) + await _require_owned_workspace(workspace_id, owner) + lifecycle = await workspace_adapter.ensure_workspace_lifecycle(workspace_id) + if lifecycle is None: + raise HTTPException(status_code=404, detail="Workspace not found") + return lifecycle + + +async def _run_operation( + workspace_id: str, + owner: str, + operation, +) -> WorkspaceLifecycle: + await _require_owned_workspace(workspace_id, owner) + try: + lifecycle = await operation(workspace_id) + except ContractError as exc: + current = await workspace_adapter.get_workspace_lifecycle(workspace_id) + if current is not None: + await _publish(owner, current) + raise HTTPException(status_code=409, detail=str(exc)) from exc + await _publish(owner, lifecycle) + return lifecycle + + +@router.post("/{workspace_id}/suspend", response_model=WorkspaceLifecycle) +async def suspend_workspace( + workspace_id: str, + user_id: str | None = Header(default=None, alias="X-User-ID"), +): + return await _run_operation( + workspace_id, _user_id(user_id), workspace_adapter.suspend_workspace + ) + + +@router.post("/{workspace_id}/resume", response_model=WorkspaceLifecycle) +async def resume_workspace( + workspace_id: str, + user_id: str | None = Header(default=None, alias="X-User-ID"), +): + return await _run_operation( + workspace_id, _user_id(user_id), workspace_adapter.resume_workspace + ) + + +@router.post("/{workspace_id}/refresh", response_model=WorkspaceLifecycle) +async def refresh_workspace( + workspace_id: str, + user_id: str | None = Header(default=None, alias="X-User-ID"), +): + return await _run_operation( + workspace_id, _user_id(user_id), workspace_adapter.refresh_workspace_lifecycle + ) + + +@router.post("/{workspace_id}/touch", response_model=WorkspaceLifecycle) +async def touch_workspace( + workspace_id: str, + reason: str, + user_id: str | None = Header(default=None, alias="X-User-ID"), +): + owner = _user_id(user_id) + await _require_owned_workspace(workspace_id, owner) + try: + lifecycle = await workspace_adapter.touch_workspace(workspace_id, reason=reason) + except (ContractError, ValueError) as exc: + raise HTTPException(status_code=400, detail=str(exc)) from exc + await _publish(owner, lifecycle) + return lifecycle + + +@router.delete("/{workspace_id}", status_code=204) +async def delete_workspace( + workspace_id: str, + user_id: str | None = Header(default=None, alias="X-User-ID"), +): + owner = _user_id(user_id) + await _require_owned_workspace(workspace_id, owner) + async with workspace_adapter._lock(workspace_id): + async with db.connection() as conn: + async with conn.transaction(): + row = await conn.fetchrow( + """SELECT b.atespace,b.actor_name,b.shim_token_secret_name, + b.desired_state,s.conversation_id + FROM workspace_bindings b JOIN sessions s ON s.id=b.workspace_id + WHERE b.workspace_id=$1 FOR UPDATE OF b""", + workspace_id, + ) + if row is None: + raise HTTPException(status_code=404, detail="Workspace not found") + open_deliveries = await workspace_adapter._delivery_states( + workspace_id, conn=conn + ) + if open_deliveries: + raise HTTPException( + status_code=409, + detail="An open delivery must be reconciled before deleting this workspace.", + ) + await conn.execute( + "UPDATE workspace_bindings SET desired_state='deleting',updated_at=NOW() WHERE workspace_id=$1", + workspace_id, + ) + try: + await get_actor_provisioner().delete( + atespace=row["atespace"], + actor_name=row["actor_name"], + shim_token_secret_name=row["shim_token_secret_name"], + ) + except Exception as exc: + async with db.connection() as conn: + await conn.execute( + """UPDATE workspace_bindings SET desired_state=$2,updated_at=NOW() + WHERE workspace_id=$1 AND desired_state='deleting'""", + workspace_id, + row["desired_state"], + ) + raise HTTPException( + status_code=502, + detail="Substrate did not confirm workspace deletion; refresh before retrying.", + ) from exc + async with db.connection() as conn: + async with conn.transaction(): + await conn.execute( + "DELETE FROM native_deliveries WHERE session_id=$1", workspace_id + ) + await conn.execute( + "DELETE FROM native_events WHERE session_id=$1", workspace_id + ) + await conn.execute( + "DELETE FROM native_lineage WHERE session_id=$1", workspace_id + ) + await conn.execute( + "DELETE FROM native_bindings WHERE session_id=$1", workspace_id + ) + await conn.execute( + "DELETE FROM workspace_lifecycles WHERE workspace_id=$1", + workspace_id, + ) + await conn.execute( + "DELETE FROM workspace_bindings WHERE workspace_id=$1", workspace_id + ) + await conn.execute("DELETE FROM sessions WHERE id=$1", workspace_id) + await conn.execute( + """DELETE FROM messages + WHERE conversation_id=$1 + AND NOT EXISTS ( + SELECT 1 FROM sessions s WHERE s.anchor_message_id=messages.id + )""", + row["conversation_id"], + ) + await conn.execute( + """DELETE FROM conversations c WHERE c.id=$1 + AND NOT EXISTS ( + SELECT 1 FROM messages m WHERE m.conversation_id=c.id + )""", + row["conversation_id"], + ) + return Response(status_code=204) + + +@router.get("/{workspace_id}/ports") +async def list_workspace_preview_ports( + workspace_id: str, + user_id: str | None = Header(default=None, alias="X-User-ID"), +): + ports = await workspace_preview_ports(workspace_id, _user_id(user_id)) + if ports is None: + raise HTTPException(status_code=404, detail="Workspace not found") + return {"ports": ports} + + +@router.get("/{workspace_id}/credentials") +async def list_workspace_credentials( + workspace_id: str, + user_id: str | None = Header(default=None, alias="X-User-ID"), +): + owner = _user_id(user_id) + await _require_owned_workspace(workspace_id, owner) + _require_credential_owner(owner) + broker = CredentialBroker() + states = [] + for provider in ("codex", "claude"): + try: + status = await broker.status(provider) + except CredentialBrokerError as exc: + raise HTTPException( + status_code=503, detail="Credential status is unavailable" + ) from exc + states.append( + { + "provider": provider, + "available": status.available if status else False, + "needs_signin": status.needs_signin if status else True, + "expires_at": ( + status.expires_at.isoformat() + if status and status.expires_at + else None + ), + } + ) + return {"credentials": states} + + +@router.post("/{workspace_id}/credentials/{provider}/reauth") +async def start_workspace_credential_reauth( + workspace_id: str, + provider: str, + user_id: str | None = Header(default=None, alias="X-User-ID"), +): + owner = _user_id(user_id) + await _require_owned_workspace(workspace_id, owner) + _require_credential_owner(owner) + if provider not in {"codex", "claude"}: + raise HTTPException(status_code=404, detail="Credential provider not found") + try: + job = await _reauth_runner.start(provider, owner=owner) + except Exception as exc: + raise HTTPException( + status_code=503, detail="Sign-in could not be started" + ) from exc + _reauth_owners[job.id] = (workspace_id, owner) + return _reauth_status_payload(job) + + +@router.get("/{workspace_id}/credentials/reauth/{job_id}") +async def workspace_credential_reauth_status( + workspace_id: str, + job_id: str, + user_id: str | None = Header(default=None, alias="X-User-ID"), +): + owner = _user_id(user_id) + await _require_owned_workspace(workspace_id, owner) + if _reauth_owners.get(job_id) != (workspace_id, owner): + raise HTTPException(status_code=404, detail="Sign-in job not found") + job = await _reauth_runner.status(job_id) + if job is None: + raise HTTPException(status_code=404, detail="Sign-in job not found") + if job.state in {"completed", "failed"}: + _reauth_owners.pop(job_id, None) + return _reauth_status_payload(job) + + +def _reauth_status_payload(job) -> dict: + return { + "id": job.id, + "provider": job.provider, + "state": job.state, + "challenge": ( + {"url": job.challenge.url, "code": job.challenge.code} + if job.challenge + else None + ), + } + + +def _require_credential_owner(user_id: str) -> None: + if user_id != settings.substrate_credential_owner_user_id: + raise HTTPException(status_code=404, detail="Workspace not found") diff --git a/backend/src/mainloop/services/chat_handler.py b/backend/src/mainloop/services/chat_handler.py deleted file mode 100644 index 74d6c1b..0000000 --- a/backend/src/mainloop/services/chat_handler.py +++ /dev/null @@ -1,535 +0,0 @@ -"""Synchronous chat handler - processes messages and returns immediate responses.""" - -import logging -from dataclasses import dataclass -from typing import Any, AsyncIterator - -from claude_agent_sdk import ( - AssistantMessage, - ClaudeAgentOptions, - ResultMessage, - SystemMessage, - TextBlock, - create_sdk_mcp_server, - query, - tool, -) -from dbos import SetWorkflowID -from mainloop.config import settings -from mainloop.db import db -from mainloop.workflows.dbos_config import worker_queue - -from models import ( - Message, - QueueItem, - Session, - SessionStatus, -) - -logger = logging.getLogger(__name__) - - -def build_chat_system_prompt(recent_repos: list[str] | None = None) -> str: - """Build the system prompt for chat, including recent repos if available.""" - base_prompt = """You are a helpful AI assistant that can spawn background sessions to work on tasks independently. - -## spawn_session -Use spawn_session when the user requests work that should run in the background. - -When to use spawn_session WITH repo_url (for code work): -- Creating, modifying, or deleting code files -- Making commits or pull requests -- Running builds, tests, or deployments -- Any work that requires access to a codebase - -When to use spawn_session WITHOUT repo_url (for other background work): -- Research tasks that take time -- Analysis or investigation work -- Planning or brainstorming that needs multiple steps -- Any work that can run in the background - -IMPORTANT - When spawning a session: -1. IMMEDIATELY acknowledge and spawn - don't ask for confirmation -2. Respond with a brief acknowledgment like "On it - spawning a session to [task summary]" -3. Then call spawn_session with the title (and repo_url for code work) - -Do NOT use spawn_session for: -- Answering simple questions -- Explaining concepts or providing information -- General conversation you can handle directly""" - - if recent_repos: - repos_list = "\n".join(f" - {repo}" for repo in recent_repos) - base_prompt += f""" - -The user has recently worked with these repositories: -{repos_list} - -If the request involves code work and includes a repo URL, use it. Otherwise use the most relevant recent repo. -""" - else: - base_prompt += """ - -If the request involves code work but no repo is provided, ask for the GitHub repo URL. -""" - - return base_prompt - - -def create_spawn_session_callable( - user_id: str, - main_thread_id: str, - conversation_id: str, - spawned_session_ids: list[str], # Mutable list to track spawned sessions -): - """Create a raw spawn_session callable for Claude to use. - - Sessions are unified background work - they can be simple Claude conversations - or code work with GitHub integration. - """ - - async def spawn_session_impl(args: dict[str, Any]) -> dict[str, Any]: - """Spawn a background session to work on a task independently.""" - print(f"[SESSION] spawn_session_impl called with args: {args}") - title = args.get("title", "") - repo_url = args.get("repo_url") # Optional - if provided, this is code work - request_message_id = args.get( - "request_message_id" - ) # ID of the user's original request - - # Fetch the original request message from DB - anchor_message_id = None - prompt = "" - try: - if request_message_id: - # Claude told us which message contains the request - fetch it - request_msg = await db.get_message(request_message_id) - if request_msg and request_msg.role == "user": - anchor_message_id = request_msg.id - prompt = request_msg.content - print( - f"[SESSION] Using specified message {request_message_id}: {prompt[:100]}..." - ) - - # Fallback: use last user message if no ID provided or not found - if not prompt: - conv_messages = await db.get_messages(conversation_id) - if conv_messages: - for msg in reversed(conv_messages): - if msg.role == "user": - anchor_message_id = msg.id - prompt = msg.content - break - print(f"[SESSION] Fallback to last user message: {prompt[:100]}...") - except Exception as e: - print(f"[SESSION] Warning: Could not get message: {e}") - - if not title: - return { - "content": [{"type": "text", "text": "Error: title is required"}], - "is_error": True, - } - - if not prompt: - return { - "content": [ - { - "type": "text", - "text": "Error: Could not find a user message in the conversation to use as the session prompt.", - } - ], - "is_error": True, - } - - # Use title as description - description = title - - # Validate repo URL format if provided - if repo_url and not repo_url.startswith("https://github.com/"): - return { - "content": [ - { - "type": "text", - "text": f"Error: Invalid repo URL format. Expected https://github.com/owner/repo, got: {repo_url}", - } - ], - "is_error": True, - } - - try: - is_code_work = repo_url is not None - print(f"[SESSION] Creating session: {title} (code_work={is_code_work})") - - # Create conversation for this session - conv = await db.create_conversation(user_id, title=title) - print(f"[SESSION] Created conversation: {conv.id}") - - # Create project from repo URL if provided (so it shows in sidebar) - project_id = None - if repo_url: - print(f"[SESSION] Creating project for repo: {repo_url}") - project = await db.get_or_create_project_from_url(user_id, repo_url) - project_id = project.id - print( - f"[SESSION] Project created/found: {project.id} - {project.full_name}" - ) - # Record this repo as recently used - await db.add_recent_repo(main_thread_id, repo_url) - - # Create session anchored to user's message - from mainloop.api import _get_next_session_color - - color = await _get_next_session_color(user_id) - session = Session( - user_id=user_id, - main_thread_id=main_thread_id, - title=title, - description=description or title, - prompt=prompt, - conversation_id=conv.id, - status=SessionStatus.PENDING, - anchor_message_id=anchor_message_id, - color=color, - # Code work fields (optional) - repo_url=repo_url, - project_id=project_id, - ) - session = await db.create_session(session) - print(f"[SESSION] Session saved to DB: {session.id}") - - # Track this session for anchor update after assistant message is saved - spawned_session_ids.append(session.id) - - # Start the session workflow - from mainloop.workflows.session_worker import session_worker_workflow - - print(f"[SESSION] Enqueueing workflow for session {session.id}") - with SetWorkflowID(session.id): - handle = worker_queue.enqueue(session_worker_workflow, session.id) - print(f"[SESSION] Workflow enqueued, handle: {handle}") - - logger.info( - f"Spawned session via tool: {session.id} (code_work={is_code_work})" - ) - - response_text = ( - f"Session started successfully!\n" - f"Session ID: {session.id[:8]}\n" - f"Title: {title}\n" - ) - if repo_url: - response_text += f"Repository: {repo_url}\n" - response_text += ( - "\nThe session is now running in the background. " - "It will appear in the user's Sessions panel." - ) - - return { - "content": [ - { - "type": "text", - "text": response_text, - } - ] - } - except Exception as e: - import traceback - - print(f"[SESSION] ERROR: {e}") - print(f"[SESSION] Traceback: {traceback.format_exc()}") - logger.error(f"Failed to spawn session: {e}") - return { - "content": [ - {"type": "text", "text": f"Error spawning session: {str(e)}"} - ], - "is_error": True, - } - - return spawn_session_impl - - -def create_spawn_session_tool( - user_id: str, - main_thread_id: str, - conversation_id: str, - spawned_session_ids: list[str], # Mutable list to track spawned sessions -): - """Create a spawn_session tool with context baked in. - - This factory creates a tool that has access to the current user/conversation context. - Returns an SdkMcpTool for use with Claude Agent SDK. - """ - # Get the raw callable - spawn_session_impl = create_spawn_session_callable( - user_id, main_thread_id, conversation_id, spawned_session_ids - ) - - # Wrap it with the @tool decorator for Claude - @tool( - "spawn_session", - "Spawn a background session to work on the user's request. " - "Use this for: (1) code work - provide repo_url for GitHub integration, " - "(2) research/analysis - omit repo_url for general background work. " - "IMPORTANT: Pass the request_message_id from the conversation history [ID: ...] " - "that contains the user's actual request (not a confirmation like 'yes').", - { - "title": str, # Short title for the session (e.g., "Add quickstart to README") - "request_message_id": str, # ID from conversation [ID: ...] with the user's request - "repo_url": str, # Optional - if provided, enables code work with GitHub - }, - ) - async def spawn_session(args: dict[str, Any]) -> dict[str, Any]: - return await spawn_session_impl(args) - - return spawn_session - - -def format_conversation_history(messages: list[Message]) -> str: - """Format conversation history for inclusion in prompt. - - Includes message IDs so Claude can reference them when spawning sessions. - """ - if not messages: - return "" - - lines = [] - for msg in messages: - role = "User" if msg.role == "user" else "Assistant" - lines.append(f"[ID: {msg.id}] {role}: {msg.content}") - - return "\n\n".join(lines) - - -def build_context_prompt( - summary: str | None, - recent_messages: list[Message], - new_message: str, -) -> str: - """Build the full prompt with summary and recent messages. - - Structure: - 1. Summary of earlier conversation (if exists) - 2. Recent messages (unsummarized) - 3. New user message - """ - parts = [] - - if summary: - parts.append(f"[Summary of earlier conversation]\n{summary}") - - if recent_messages: - history = format_conversation_history(recent_messages) - parts.append(f"[Recent conversation]\n{history}") - - parts.append(f"User: {new_message}") - - if parts: - context = "\n\n".join(parts) - return f"""Continue this conversation naturally, taking into account the full context above. - -{context} - -Respond to the user's latest message.""" - else: - return new_message - - -@dataclass -class ChatResult: - """Result of processing a chat message.""" - - response: str - task_id: str | None = None - needs_inbox_action: bool = False - queue_item: QueueItem | None = None - spawned_session_ids: list[str] | None = None # Sessions created during this turn - suppress_response: bool = False # Don't save assistant message to main thread - - -@dataclass -class ClaudeResponse: - """Response from Claude.""" - - text: str - compacted: bool = False - compaction_count: int = 0 - spawned_session_ids: list[str] | None = None # Sessions created during this turn - - -def _create_message_generator(prompt_text: str) -> AsyncIterator[dict]: - """Create an async generator that yields the user message. - - This is required when using MCP servers with Claude Agent SDK. - The SDK requires an async iterable for streaming input when MCP tools are configured. - """ - - async def generator(): - yield { - "type": "user", - "message": { - "role": "user", - "content": prompt_text, - }, - } - - return generator() - - -async def get_claude_response( - message: str, - summary: str | None = None, - recent_messages: list[Message] | None = None, - model: str = "sonnet", - user_id: str | None = None, - main_thread_id: str | None = None, - conversation_id: str | None = None, -) -> ClaudeResponse: - """Get a response from Claude with conversation context and spawn_task tool. - - Context is provided via: - - summary: Compacted summary of older messages (from PostgreSQL) - - recent_messages: Recent unsummarized messages (from PostgreSQL) - - If user_id, main_thread_id, and conversation_id are provided, Claude - will have access to the spawn_task tool to spawn autonomous worker agents. - - This ensures continuity across sessions, pod restarts, and deployments. - """ - try: - # Build prompt with summary and recent messages - prompt_text = build_context_prompt(summary, recent_messages or [], message) - - # Create MCP server with spawn_task tool if context is provided - mcp_servers = {} - allowed_tools = [] - system_prompt = None - spawned_session_ids: list[str] = [] # Track sessions created during this turn - - if user_id and main_thread_id and conversation_id: - spawn_session_tool = create_spawn_session_tool( - user_id, main_thread_id, conversation_id, spawned_session_ids - ) - mcp_server = create_sdk_mcp_server( - name="mainloop", - version="1.0.0", - tools=[spawn_session_tool], - ) - mcp_servers["mainloop"] = mcp_server - allowed_tools.append("mcp__mainloop__spawn_session") - - # Fetch recent repos for system prompt - recent_repos = await db.get_recent_repos(main_thread_id) - system_prompt = build_chat_system_prompt(recent_repos) - - options = ClaudeAgentOptions( - model=model, - permission_mode="bypassPermissions", - system_prompt=system_prompt, - mcp_servers=mcp_servers if mcp_servers else None, - allowed_tools=allowed_tools if allowed_tools else None, - ) - - print( - f"[CLAUDE] query - model={model}, mcp_servers={list(mcp_servers.keys()) if mcp_servers else None}, " - f"allowed_tools={allowed_tools}" - ) - - # CRITICAL: When using MCP servers, must use async generator for prompt. - # This is a Claude Agent SDK requirement - string prompts fail with - # "ProcessTransport is not ready for writing" error. - if mcp_servers: - prompt = _create_message_generator(prompt_text) - else: - prompt = prompt_text - - collected_text = [] - compaction_count: int = 0 - - async for msg in query(prompt=prompt, options=options): - if isinstance(msg, AssistantMessage): - for block in msg.content: - if isinstance(block, TextBlock): - collected_text.append(block.text) - elif isinstance(msg, ResultMessage): - if msg.is_error: - return ClaudeResponse( - text=f"Sorry, I encountered an error: {msg.result or 'Unknown error'}", - compacted=compaction_count > 0, - compaction_count=compaction_count, - spawned_session_ids=( - spawned_session_ids if spawned_session_ids else None - ), - ) - elif isinstance(msg, SystemMessage): - # Track compaction events (context was automatically summarized) - if msg.subtype == "compact_boundary": - compaction_count += 1 - data = msg.data or {} - pre_tokens = data.get("pre_tokens", 0) - trigger = data.get("trigger", "unknown") - logger.info( - f"Context compacted ({trigger}): {pre_tokens} tokens summarized" - ) - - return ClaudeResponse( - text=( - "\n".join(collected_text) - if collected_text - else "No response generated." - ), - compacted=compaction_count > 0, - compaction_count=compaction_count, - spawned_session_ids=spawned_session_ids if spawned_session_ids else None, - ) - except Exception as e: - logger.error(f"Claude Agent SDK error: {e}") - return ClaudeResponse( - text=f"Sorry, I encountered an error: {str(e)}", - spawned_session_ids=spawned_session_ids if spawned_session_ids else None, - ) - - -async def process_message( - user_id: str, - message: str, - conversation_id: str, - main_thread_id: str, - summary: str | None = None, - recent_messages: list[Message] | None = None, -) -> ChatResult: - """Process a user message and return an immediate response. - - Claude has access to spawn_session tool to create background sessions - when the user wants to start tasks. - - Args: - user_id: The user's unique identifier. - message: The user's message text. - conversation_id: The conversation's unique identifier. - main_thread_id: The main thread workflow ID. - summary: Compacted summary of older messages. - recent_messages: Recent unsummarized messages for context. - - """ - # Get Claude response with spawn_session tool available - # Claude will naturally decide when to ask for confirmation and spawn sessions - model = settings.claude_model # Uses haiku by default - claude_response = await get_claude_response( - message, - summary=summary, - recent_messages=recent_messages, - model=model, - user_id=user_id, - main_thread_id=main_thread_id, - conversation_id=conversation_id, - ) - - # If sessions were spawned, suppress the main thread response - # The user interacts with the session directly - suppress = bool(claude_response.spawned_session_ids) - - return ChatResult( - response=claude_response.text, - spawned_session_ids=claude_response.spawned_session_ids, - suppress_response=suppress, - ) diff --git a/backend/src/mainloop/services/claude_agent.py b/backend/src/mainloop/services/claude_agent.py deleted file mode 100644 index 588036c..0000000 --- a/backend/src/mainloop/services/claude_agent.py +++ /dev/null @@ -1,134 +0,0 @@ -"""Client for the Claude Agent container HTTP API.""" - -import logging -from typing import AsyncGenerator - -import httpx -from mainloop.config import settings -from pydantic import BaseModel - -logger = logging.getLogger(__name__) - - -class ExecuteResponse(BaseModel): - """Response from Claude Agent execution.""" - - output: str - session_id: str | None = None - cost_usd: float | None = None - error: str | None = None - - -class ClaudeAgentClient: - """HTTP client for the Claude Agent container.""" - - def __init__(self, base_url: str | None = None): - self.base_url = ( - base_url or settings.claude_agent_url or "http://claude-agent:8001" - ) - - async def execute( - self, - prompt: str, - model: str = "sonnet", - timeout: float = 300.0, - ) -> ExecuteResponse: - """ - Execute a prompt using Claude Agent SDK. - - Claude-agent manages its own isolated workspace internally. - - Args: - prompt: The prompt to execute - model: Model to use (haiku, sonnet, opus) - timeout: Request timeout in seconds - - Returns: - ExecuteResponse with output or error - - """ - async with httpx.AsyncClient(timeout=timeout) as client: - try: - response = await client.post( - f"{self.base_url}/execute", - json={ - "prompt": prompt, - "model": model, - }, - ) - response.raise_for_status() - return ExecuteResponse(**response.json()) - except httpx.HTTPStatusError as e: - logger.error(f"Claude agent HTTP error: {e}") - return ExecuteResponse( - output="", - error=f"HTTP {e.response.status_code}: {e.response.text}", - ) - except httpx.RequestError as e: - logger.error(f"Claude agent request error: {e}") - return ExecuteResponse( - output="", - error=f"Request failed: {str(e)}", - ) - - async def execute_stream( - self, - prompt: str, - model: str = "sonnet", - timeout: float = 300.0, - ) -> AsyncGenerator[dict, None]: - """ - Stream execution results from Claude Agent SDK. - - Yields: - Dict events with type: 'text', 'result', or 'error' - - """ - import json - - async with httpx.AsyncClient(timeout=timeout) as client: - try: - async with client.stream( - "POST", - f"{self.base_url}/execute/stream", - json={ - "prompt": prompt, - "model": model, - }, - ) as response: - response.raise_for_status() - async for line in response.aiter_lines(): - if line.startswith("data: "): - data = line[6:] - if data == "[DONE]": - break - try: - yield json.loads(data) - except json.JSONDecodeError: - continue - except httpx.HTTPStatusError as e: - yield {"type": "error", "error": f"HTTP {e.response.status_code}"} - except httpx.RequestError as e: - yield {"type": "error", "error": str(e)} - - async def health_check(self) -> dict: - """Check if the Claude Agent service is healthy.""" - async with httpx.AsyncClient(timeout=5.0) as client: - try: - response = await client.get(f"{self.base_url}/health") - response.raise_for_status() - return response.json() - except Exception as e: - return {"status": "error", "error": str(e)} - - -# Singleton client instance -_client: ClaudeAgentClient | None = None - - -def get_claude_agent_client() -> ClaudeAgentClient: - """Get the singleton Claude Agent client.""" - global _client - if _client is None: - _client = ClaudeAgentClient() - return _client diff --git a/backend/src/mainloop/services/compaction.py b/backend/src/mainloop/services/compaction.py deleted file mode 100644 index a2d6942..0000000 --- a/backend/src/mainloop/services/compaction.py +++ /dev/null @@ -1,163 +0,0 @@ -"""Async conversation compaction service. - -Summarizes older messages to manage context window while preserving conversation history. -Runs asynchronously to avoid blocking the main chat flow. -""" - -import asyncio -import logging - -from claude_agent_sdk import ( - AssistantMessage, - ClaudeAgentOptions, - ResultMessage, - TextBlock, - query, -) -from mainloop.config import settings -from mainloop.db import db - -from models import Message - -logger = logging.getLogger(__name__) - -# Compaction thresholds -COMPACTION_THRESHOLD = 40 # Trigger compaction when message_count exceeds this -MESSAGES_TO_SUMMARIZE = 30 # Number of oldest messages to summarize -RECENT_MESSAGES_TO_KEEP = 10 # Keep this many recent messages unsummarized - - -async def summarize_messages(messages: list[Message]) -> str: - """Use Claude to summarize a list of messages.""" - if not messages: - return "" - - # Format messages for summarization - formatted = [] - for msg in messages: - role = "User" if msg.role == "user" else "Assistant" - formatted.append(f"{role}: {msg.content}") - - conversation_text = "\n\n".join(formatted) - - prompt = f"""Summarize this conversation concisely, preserving key information: -- Important facts mentioned (names, preferences, decisions) -- Key topics discussed -- Any commitments or action items -- Context needed to continue the conversation naturally - -Conversation to summarize: -{conversation_text} - -Write a concise summary (2-4 paragraphs) that captures the essential context.""" - - try: - options = ClaudeAgentOptions( - model=settings.claude_model, # Use same model as main thread - permission_mode="bypassPermissions", - ) - - collected_text: list[str] = [] - async for msg in query(prompt=prompt, options=options): - if isinstance(msg, AssistantMessage): - for block in msg.content: - if isinstance(block, TextBlock): - collected_text.append(block.text) - elif isinstance(msg, ResultMessage): - if msg.is_error: - logger.error(f"Summarization error: {msg.result}") - return "" - - return "\n".join(collected_text) - except Exception as e: - logger.error(f"Failed to summarize messages: {e}") - return "" - - -async def compact_conversation(conversation_id: str) -> None: - """Compact a conversation by summarizing older messages. - - This runs asynchronously and updates the conversation's summary field. - """ - try: - conversation = await db.get_conversation(conversation_id) - if not conversation: - logger.warning(f"Conversation {conversation_id} not found for compaction") - return - - # Check if compaction is needed - if conversation.message_count < COMPACTION_THRESHOLD: - return - - logger.info( - f"Starting compaction for conversation {conversation_id} " - f"(message_count={conversation.message_count})" - ) - - # Get messages to summarize (oldest ones) - messages_to_summarize = await db.get_messages_for_compaction( - conversation_id, MESSAGES_TO_SUMMARIZE - ) - - if not messages_to_summarize: - return - - # Include existing summary in new summary if present - existing_summary = conversation.summary or "" - - if existing_summary: - # Append new messages to existing summary context - summary_prompt_messages = messages_to_summarize - existing_context = ( - f"Previous summary:\n{existing_summary}\n\nNew messages to incorporate:" - ) - else: - existing_context = "" - summary_prompt_messages = messages_to_summarize - - # Generate summary - new_summary = await summarize_messages(summary_prompt_messages) - - if not new_summary: - logger.warning( - f"Failed to generate summary for conversation {conversation_id}" - ) - return - - # Combine with existing summary if present - if existing_context: - final_summary = f"{existing_summary}\n\n{new_summary}" - else: - final_summary = new_summary - - # Get the ID of the last summarized message - last_summarized = messages_to_summarize[-1] - - # Update conversation with new summary - await db.update_conversation_summary( - conversation_id=conversation_id, - summary=final_summary, - summarized_through_id=last_summarized.id, - ) - - logger.info( - f"Compaction complete for conversation {conversation_id}: " - f"summarized {len(messages_to_summarize)} messages" - ) - - except Exception as e: - logger.error(f"Compaction failed for conversation {conversation_id}: {e}") - - -def trigger_compaction(conversation_id: str, message_count: int) -> None: - """Fire-and-forget trigger for compaction. - - Checks if compaction is needed and schedules it asynchronously. - Does not block the calling code. - """ - if message_count < COMPACTION_THRESHOLD: - return - - # Schedule compaction as a background task - asyncio.create_task(compact_conversation(conversation_id)) - logger.debug(f"Scheduled compaction for conversation {conversation_id}") diff --git a/backend/src/mainloop/services/k8s_jobs.py b/backend/src/mainloop/services/k8s_jobs.py deleted file mode 100644 index b97d433..0000000 --- a/backend/src/mainloop/services/k8s_jobs.py +++ /dev/null @@ -1,280 +0,0 @@ -"""Kubernetes Job management for session tasks.""" - -import logging - -from kubernetes import client -from kubernetes.client.rest import ApiException -from mainloop.config import settings -from mainloop.services.k8s_namespace import WORKER_SERVICE_ACCOUNT, get_k8s_client - -logger = logging.getLogger(__name__) - -# Job configuration -JOB_TTL_SECONDS = 3600 # Keep completed jobs for 1 hour - - -async def create_session_job( - session_id: str, - namespace: str, - prompt: str, - callback_url: str, - model: str | None = None, - iteration: int = 0, -) -> str: - """Create a session Job in the session namespace. - - Args: - session_id: The session ID - namespace: Target namespace for the Job - prompt: The full prompt with conversation context - callback_url: URL to POST results to - model: Claude model to use (defaults to settings.claude_model) - iteration: Iteration number for jobs (ensures unique names) - - Returns: - The Job name - - """ - _, batch_v1 = get_k8s_client() - - # Include iteration in job name to ensure uniqueness across rounds - if iteration > 0: - job_name = f"session-{session_id[:8]}-{iteration}" - else: - job_name = f"session-{session_id[:8]}" - - # Check if job already exists and is completed - delete it to allow retry - try: - existing_job = batch_v1.read_namespaced_job(name=job_name, namespace=namespace) - status = existing_job.status - if (status.succeeded and status.succeeded > 0) or ( - status.failed and status.failed > 0 - ): - # Job completed, delete it to allow re-creation - logger.info(f"Deleting completed job {job_name} for retry") - batch_v1.delete_namespaced_job( - name=job_name, - namespace=namespace, - body=client.V1DeleteOptions(propagation_policy="Background"), - ) - # Brief wait for deletion to propagate - import asyncio - - await asyncio.sleep(1) - except ApiException as e: - if e.status != 404: - raise - # Job doesn't exist, continue with creation - - model = model or settings.claude_model - - # Environment variables for the job - env_vars = [ - client.V1EnvVar(name="SESSION_ID", value=session_id), - client.V1EnvVar(name="TASK_PROMPT", value=prompt), - client.V1EnvVar(name="CALLBACK_URL", value=callback_url), - client.V1EnvVar(name="CLAUDE_MODEL", value=model), - # Claude credentials from secret - client.V1EnvVar( - name="CLAUDE_CODE_OAUTH_TOKEN", - value_from=client.V1EnvVarSource( - secret_key_ref=client.V1SecretKeySelector( - name="mainloop-secrets", - key="claude-secret-token", - ) - ), - ), - # GitHub token from secret (optional, for gh CLI access) - client.V1EnvVar( - name="GH_TOKEN", - value_from=client.V1EnvVarSource( - secret_key_ref=client.V1SecretKeySelector( - name="mainloop-secrets", - key="github-token", - optional=True, - ) - ), - ), - ] - - # Job spec - job = client.V1Job( - metadata=client.V1ObjectMeta( - name=job_name, - namespace=namespace, - labels={ - "app.kubernetes.io/managed-by": "mainloop", - "mainloop.dev/session-id": session_id, - }, - ), - spec=client.V1JobSpec( - ttl_seconds_after_finished=JOB_TTL_SECONDS, - backoff_limit=0, # Don't retry failed jobs - template=client.V1PodTemplateSpec( - metadata=client.V1ObjectMeta( - labels={ - "app.kubernetes.io/managed-by": "mainloop", - "mainloop.dev/session-id": session_id, - }, - ), - spec=client.V1PodSpec( - restart_policy="Never", - service_account_name=WORKER_SERVICE_ACCOUNT, - # Only use image pull secrets for remote registries - image_pull_secrets=( - [client.V1LocalObjectReference(name="ghcr-secret")] - if settings.worker_image.startswith("ghcr.io/") - else None - ), - containers=[ - client.V1Container( - name="claude-agent", - image=settings.worker_image, - image_pull_policy=settings.worker_image_pull_policy, - command=["/app/.venv/bin/python", "/app/job_runner.py"], - env=env_vars, - resources=client.V1ResourceRequirements( - requests={"memory": "512Mi", "cpu": "500m"}, - limits={"memory": "2Gi", "cpu": "2"}, - ), - volume_mounts=[ - client.V1VolumeMount( - name="workspace", - mount_path="/workspace", - ), - ], - ), - ], - volumes=[ - client.V1Volume( - name="workspace", - empty_dir=client.V1EmptyDirVolumeSource(), - ), - ], - ), - ), - ), - ) - - try: - batch_v1.create_namespaced_job(namespace=namespace, body=job) - logger.info(f"Created job {job_name} in namespace {namespace}") - except ApiException as e: - if e.status == 409: - logger.info(f"Job {job_name} already exists in {namespace}") - else: - raise - - return job_name - - -async def get_job_status(session_id: str, namespace: str) -> dict | None: - """Get the status of a session Job. - - Args: - session_id: The session ID - namespace: Namespace where the Job is running - - Returns: - Job status dict or None if not found - - """ - _, batch_v1 = get_k8s_client() - - try: - jobs = batch_v1.list_namespaced_job( - namespace=namespace, - label_selector=f"mainloop.dev/session-id={session_id}", - ) - - if not jobs.items: - return None - - job = jobs.items[0] - status = job.status - - return { - "name": job.metadata.name, - "active": status.active or 0, - "succeeded": status.succeeded or 0, - "failed": status.failed or 0, - "start_time": status.start_time.isoformat() if status.start_time else None, - "completion_time": ( - status.completion_time.isoformat() if status.completion_time else None - ), - } - - except ApiException as e: - if e.status == 404: - return None - raise - - -async def delete_job(session_id: str, namespace: str) -> None: - """Delete a session Job. - - Args: - session_id: The session ID - namespace: Namespace where the Job is running - - """ - _, batch_v1 = get_k8s_client() - - try: - jobs = batch_v1.list_namespaced_job( - namespace=namespace, - label_selector=f"mainloop.dev/session-id={session_id}", - ) - - for job in jobs.items: - batch_v1.delete_namespaced_job( - name=job.metadata.name, - namespace=namespace, - body=client.V1DeleteOptions( - propagation_policy="Background", - ), - ) - logger.info(f"Deleted job {job.metadata.name} from namespace {namespace}") - - except ApiException as e: - if e.status == 404: - logger.info(f"No jobs found for session {session_id} in {namespace}") - else: - raise - - -async def get_job_logs(session_id: str, namespace: str) -> str | None: - """Get logs from a session Job's pod. - - Args: - session_id: The session ID - namespace: Namespace where the Job is running - - Returns: - Pod logs as string or None if not found - - """ - core_v1, _ = get_k8s_client() - - try: - pods = core_v1.list_namespaced_pod( - namespace=namespace, - label_selector=f"mainloop.dev/session-id={session_id}", - ) - - if not pods.items: - return None - - pod = pods.items[0] - logs = core_v1.read_namespaced_pod_log( - name=pod.metadata.name, - namespace=namespace, - container="claude-agent", - ) - - return logs - - except ApiException as e: - if e.status == 404: - return None - raise diff --git a/backend/src/mainloop/services/k8s_namespace.py b/backend/src/mainloop/services/k8s_namespace.py deleted file mode 100644 index 990f80b..0000000 --- a/backend/src/mainloop/services/k8s_namespace.py +++ /dev/null @@ -1,436 +0,0 @@ -"""Kubernetes namespace management for task isolation.""" - -import logging - -from kubernetes import client, config -from kubernetes.client.rest import ApiException - -logger = logging.getLogger(__name__) - -# Namespace prefix for session namespaces -SESSION_NAMESPACE_PREFIX = "mainloop-session-" - -# Secrets to copy from mainloop namespace to task namespaces -DEFAULT_SECRETS_TO_COPY = [ - "mainloop-secrets", - "ghcr-secret", # Image pull secret for ghcr.io -] - -# Source namespace for secrets -SOURCE_NAMESPACE = "mainloop" - -# Worker service account name -WORKER_SERVICE_ACCOUNT = "worker" - -# ClusterRole to bind to worker service account -WORKER_CLUSTER_ROLE = "mainloop-worker-role" - - -def get_k8s_client() -> tuple[client.CoreV1Api, client.BatchV1Api]: - """Get Kubernetes API clients. - - Loads in-cluster config when running in K8s, falls back to kubeconfig for local dev. - """ - try: - config.load_incluster_config() - logger.info("Loaded in-cluster Kubernetes config") - except config.ConfigException: - config.load_kube_config() - logger.info("Loaded kubeconfig for local development") - - return client.CoreV1Api(), client.BatchV1Api() - - -def get_rbac_client() -> client.RbacAuthorizationV1Api: - """Get Kubernetes RBAC API client.""" - try: - config.load_incluster_config() - except config.ConfigException: - config.load_kube_config() - - return client.RbacAuthorizationV1Api() - - -def get_networking_client() -> client.NetworkingV1Api: - """Get Kubernetes Networking API client.""" - try: - config.load_incluster_config() - except config.ConfigException: - config.load_kube_config() - - return client.NetworkingV1Api() - - -async def create_session_namespace(session_id: str) -> str: - """Create an isolated namespace for a session. - - Args: - session_id: The session ID (will be used in namespace name) - - Returns: - The namespace name that was created - - """ - core_v1, _ = get_k8s_client() - namespace_name = f"{SESSION_NAMESPACE_PREFIX}{session_id[:8]}" - - namespace = client.V1Namespace( - metadata=client.V1ObjectMeta( - name=namespace_name, - labels={ - "app.kubernetes.io/managed-by": "mainloop", - "mainloop.dev/session-id": session_id, - }, - ) - ) - - try: - core_v1.create_namespace(body=namespace) - logger.info(f"Created namespace: {namespace_name}") - except ApiException as e: - if e.status == 409: - logger.info(f"Namespace {namespace_name} already exists") - else: - raise - - return namespace_name - - -async def copy_secrets_to_namespace( - session_id: str, - namespace: str, - secrets: list[str] | None = None, -) -> None: - """Copy secrets from mainloop namespace to session namespace. - - Args: - session_id: The session ID - namespace: Target namespace to copy secrets to - secrets: List of secret names to copy (defaults to DEFAULT_SECRETS_TO_COPY) - - """ - core_v1, _ = get_k8s_client() - secrets_to_copy = secrets or DEFAULT_SECRETS_TO_COPY - - for secret_name in secrets_to_copy: - try: - # Read secret from source namespace - source_secret = core_v1.read_namespaced_secret( - name=secret_name, - namespace=SOURCE_NAMESPACE, - ) - - # Create new secret in target namespace - new_secret = client.V1Secret( - metadata=client.V1ObjectMeta( - name=secret_name, - namespace=namespace, - labels={ - "app.kubernetes.io/managed-by": "mainloop", - "mainloop.dev/session-id": session_id, - "mainloop.dev/copied-from": SOURCE_NAMESPACE, - }, - ), - type=source_secret.type, - data=source_secret.data, - ) - - try: - core_v1.create_namespaced_secret(namespace=namespace, body=new_secret) - logger.info(f"Copied secret {secret_name} to namespace {namespace}") - except ApiException as e: - if e.status == 409: - logger.info(f"Secret {secret_name} already exists in {namespace}") - else: - raise - - except ApiException as e: - if e.status == 404: - logger.warning( - f"Secret {secret_name} not found in {SOURCE_NAMESPACE}, skipping" - ) - else: - raise - - -async def setup_session_rbac(session_id: str, namespace: str) -> None: - """Create ServiceAccount and RoleBinding for worker in session namespace. - - This gives the worker pod permissions to deploy resources within its namespace. - - Args: - session_id: The session ID - namespace: Target namespace - - """ - core_v1, _ = get_k8s_client() - rbac_v1 = get_rbac_client() - - # Create worker ServiceAccount - service_account = client.V1ServiceAccount( - metadata=client.V1ObjectMeta( - name=WORKER_SERVICE_ACCOUNT, - namespace=namespace, - labels={ - "app.kubernetes.io/managed-by": "mainloop", - "mainloop.dev/session-id": session_id, - }, - ), - ) - - try: - core_v1.create_namespaced_service_account( - namespace=namespace, body=service_account - ) - logger.info(f"Created ServiceAccount {WORKER_SERVICE_ACCOUNT} in {namespace}") - except ApiException as e: - if e.status == 409: - logger.info( - f"ServiceAccount {WORKER_SERVICE_ACCOUNT} already exists in {namespace}" - ) - else: - raise - - # Create RoleBinding to bind ClusterRole to ServiceAccount - role_binding = client.V1RoleBinding( - metadata=client.V1ObjectMeta( - name="worker-role-binding", - namespace=namespace, - labels={ - "app.kubernetes.io/managed-by": "mainloop", - "mainloop.dev/session-id": session_id, - }, - ), - subjects=[ - client.RbacV1Subject( - kind="ServiceAccount", - name=WORKER_SERVICE_ACCOUNT, - namespace=namespace, - ), - ], - role_ref=client.V1RoleRef( - kind="ClusterRole", - name=WORKER_CLUSTER_ROLE, - api_group="rbac.authorization.k8s.io", - ), - ) - - try: - rbac_v1.create_namespaced_role_binding(namespace=namespace, body=role_binding) - logger.info(f"Created RoleBinding for {WORKER_SERVICE_ACCOUNT} in {namespace}") - except ApiException as e: - if e.status == 409: - logger.info(f"RoleBinding already exists in {namespace}") - else: - raise - - -async def apply_session_namespace_network_policies( - session_id: str, namespace: str -) -> None: - """Apply network policies to session namespace for security isolation. - - Applies strict network policies: - - Default deny-all ingress and egress - - Allow DNS (required for internet access) - - Allow egress to internet ONLY (blocks all cluster internal communication) - - This ensures session agents can only communicate with external services, - not with other cluster resources or each other. - - Args: - session_id: The session ID - namespace: Target namespace - - """ - networking_v1 = get_networking_client() - - # Default deny-all policy - deny_all_policy = client.V1NetworkPolicy( - metadata=client.V1ObjectMeta( - name="default-deny-all", - namespace=namespace, - labels={ - "app.kubernetes.io/managed-by": "mainloop", - "mainloop.dev/session-id": session_id, - }, - ), - spec=client.V1NetworkPolicySpec( - pod_selector=client.V1LabelSelector(), - policy_types=["Ingress", "Egress"], - ), - ) - - # Allow DNS policy - allow_dns_policy = client.V1NetworkPolicy( - metadata=client.V1ObjectMeta( - name="allow-dns", - namespace=namespace, - labels={ - "app.kubernetes.io/managed-by": "mainloop", - "mainloop.dev/session-id": session_id, - }, - ), - spec=client.V1NetworkPolicySpec( - pod_selector=client.V1LabelSelector(), - policy_types=["Egress"], - egress=[ - client.V1NetworkPolicyEgressRule( - to=[ - client.V1NetworkPolicyPeer( - namespace_selector=client.V1LabelSelector( - match_labels={ - "kubernetes.io/metadata.name": "kube-system" - } - ) - ) - ], - ports=[client.V1NetworkPolicyPort(protocol="UDP", port=53)], - ) - ], - ), - ) - - # Allow internet-only egress (block cluster internal) - allow_internet_policy = client.V1NetworkPolicy( - metadata=client.V1ObjectMeta( - name="allow-internet-only", - namespace=namespace, - labels={ - "app.kubernetes.io/managed-by": "mainloop", - "mainloop.dev/session-id": session_id, - }, - ), - spec=client.V1NetworkPolicySpec( - pod_selector=client.V1LabelSelector(), - policy_types=["Egress"], - egress=[ - client.V1NetworkPolicyEgressRule( - to=[ - client.V1NetworkPolicyPeer( - ip_block=client.V1IPBlock( - cidr="0.0.0.0/0", - _except=[ - "10.0.0.0/8", # Private class A - "172.16.0.0/12", # Private class B - "192.168.0.0/16", # Private class C - "169.254.0.0/16", # Link-local - "127.0.0.0/8", # Loopback - ], - ) - ) - ], - ) - ], - ), - ) - - # Allow egress to mainloop namespace (for callback to backend) - allow_mainloop_policy = client.V1NetworkPolicy( - metadata=client.V1ObjectMeta( - name="allow-mainloop-callback", - namespace=namespace, - labels={ - "app.kubernetes.io/managed-by": "mainloop", - "mainloop.dev/session-id": session_id, - }, - ), - spec=client.V1NetworkPolicySpec( - pod_selector=client.V1LabelSelector(), - policy_types=["Egress"], - egress=[ - client.V1NetworkPolicyEgressRule( - to=[ - client.V1NetworkPolicyPeer( - namespace_selector=client.V1LabelSelector( - match_labels={"kubernetes.io/metadata.name": "mainloop"} - ) - ) - ], - ) - ], - ), - ) - - # Apply policies - for policy in [ - deny_all_policy, - allow_dns_policy, - allow_internet_policy, - allow_mainloop_policy, - ]: - try: - networking_v1.create_namespaced_network_policy( - namespace=namespace, body=policy - ) - logger.info(f"Applied NetworkPolicy {policy.metadata.name} to {namespace}") - except ApiException as e: - if e.status == 409: - logger.info( - f"NetworkPolicy {policy.metadata.name} already exists in {namespace}" - ) - else: - raise - - -async def delete_session_namespace(session_id: str) -> None: - """Delete a session namespace and all its resources. - - Args: - session_id: The session ID (used to construct namespace name) - - """ - core_v1, _ = get_k8s_client() - namespace_name = f"{SESSION_NAMESPACE_PREFIX}{session_id[:8]}" - - try: - core_v1.delete_namespace( - name=namespace_name, - body=client.V1DeleteOptions( - propagation_policy="Foreground", - ), - ) - logger.info(f"Deleted namespace: {namespace_name}") - except ApiException as e: - if e.status == 404: - logger.info(f"Namespace {namespace_name} already deleted") - else: - raise - - -async def session_namespace_exists(session_id: str) -> bool: - """Check if a session namespace exists. - - Args: - session_id: The session ID - - Returns: - True if namespace exists, False otherwise - - """ - core_v1, _ = get_k8s_client() - namespace_name = f"{SESSION_NAMESPACE_PREFIX}{session_id[:8]}" - - try: - core_v1.read_namespace(name=namespace_name) - return True - except ApiException as e: - if e.status == 404: - return False - raise - - -async def list_session_namespaces() -> list[str]: - """List all session namespaces managed by mainloop. - - Returns: - List of namespace names - - """ - core_v1, _ = get_k8s_client() - - namespaces = core_v1.list_namespace( - label_selector="app.kubernetes.io/managed-by=mainloop" - ) - - return [ns.metadata.name for ns in namespaces.items] diff --git a/backend/src/mainloop/sse.py b/backend/src/mainloop/sse.py index 4098773..7571a97 100644 --- a/backend/src/mainloop/sse.py +++ b/backend/src/mainloop/sse.py @@ -23,6 +23,7 @@ class EventType(str, Enum): SESSION_UPDATED = "session:updated" SESSION_NEEDS_INPUT = "session:needs_input" SESSION_MESSAGE = "session:message" + WORKSPACE_UPDATED = "workspace:updated" HEARTBEAT = "heartbeat" @@ -212,3 +213,14 @@ async def notify_session_message( }, ), ) + + +async def notify_workspace_updated(user_id: str, lifecycle: dict[str, Any]) -> None: + """Publish the current durable workspace lifecycle projection.""" + await event_bus.publish_to_user( + user_id, + SSEEvent( + event=EventType.WORKSPACE_UPDATED, + data={"workspace": lifecycle}, + ), + ) diff --git a/backend/src/mainloop/workflows/__init__.py b/backend/src/mainloop/workflows/__init__.py index f64f98f..528cd6e 100644 --- a/backend/src/mainloop/workflows/__init__.py +++ b/backend/src/mainloop/workflows/__init__.py @@ -1,6 +1,5 @@ """Durable workflow orchestration using DBOS.""" -from mainloop.workflows.dbos_config import dbos_config, worker_queue -from mainloop.workflows.session_worker import session_worker_workflow +from mainloop.workflows.dbos_config import dbos_config -__all__ = ["dbos_config", "worker_queue", "session_worker_workflow"] +__all__ = ["dbos_config"] diff --git a/backend/src/mainloop/workflows/dbos_config.py b/backend/src/mainloop/workflows/dbos_config.py index 9ac7848..b086731 100644 --- a/backend/src/mainloop/workflows/dbos_config.py +++ b/backend/src/mainloop/workflows/dbos_config.py @@ -2,13 +2,15 @@ import os -from dbos import DBOS, DBOSConfig, Queue +from dbos import DBOS, DBOSConfig from mainloop.config import settings # DBOS configuration # application_version prevents recovery of old workflows after code changes # Bump this when workflow step order/logic changes to avoid DBOSUnexpectedStepError -WORKFLOW_VERSION = "11" # v11: Sessions use K8s job isolation, removed worker tasks +WORKFLOW_VERSION = ( + "12" # v12: Native Substrate sessions replace the removed session workers +) dbos_config: DBOSConfig = { "name": "mainloop", @@ -20,17 +22,3 @@ # Initialize DBOS - must be done before defining workflows DBOS(config=dbos_config) - -# Queue for session workflows with concurrency limit -# This ensures we don't overwhelm resources with too many concurrent sessions -worker_queue = Queue( - "worker_tasks", # Queue name kept for backwards compatibility - concurrency=3, # Max 3 sessions running at once globally -) - -# Queue for user main threads - one at a time per partition (user) -main_thread_queue = Queue( - "main_threads", - partition_queue=True, # Partition by user_id - concurrency=1, # One active main thread per user at a time -) diff --git a/backend/src/mainloop/workflows/main_thread.py b/backend/src/mainloop/workflows/main_thread.py index 20f2542..f3ea653 100644 --- a/backend/src/mainloop/workflows/main_thread.py +++ b/backend/src/mainloop/workflows/main_thread.py @@ -29,7 +29,7 @@ async def main_thread_workflow(user_id: str) -> None: """Run the main thread workflow for a user. This workflow runs as long as needed, processing queue responses. - Chat messages are handled directly by chat_handler with Claude Agent SDK. + Chat messages are ledgered and delivered to the user's native Substrate session. The workflow is started per-user and identified by user_id. """ diff --git a/backend/src/mainloop/workflows/session_worker.py b/backend/src/mainloop/workflows/session_worker.py deleted file mode 100644 index 3783336..0000000 --- a/backend/src/mainloop/workflows/session_worker.py +++ /dev/null @@ -1,276 +0,0 @@ -"""Session worker workflow - runs Claude in isolated K8s Jobs. - -Sessions run in their own K8s namespace with full isolation: -1. User sends message -2. K8s Job spawned with prompt -3. Job POSTs result back via callback -4. Result added to conversation -5. Wait for next user message -6. Repeat -""" - -import logging -from datetime import datetime, timezone -from typing import Any - -from dbos import DBOS -from mainloop.config import settings -from mainloop.services.k8s_jobs import create_session_job -from mainloop.services.k8s_namespace import ( - apply_session_namespace_network_policies, - copy_secrets_to_namespace, - create_session_namespace, - delete_session_namespace, - setup_session_rbac, -) -from mainloop.sse import notify_session_updated -from mainloop.workflows.transactions import ( - add_message_to_conversation, - load_session, - update_session_status, -) - -from models import SessionStatus - -logger = logging.getLogger(__name__) - -# Topics for DBOS messaging -TOPIC_USER_MESSAGE = "user_message" -TOPIC_JOB_RESULT = "job_result" - -# Timeouts -USER_INPUT_TIMEOUT = 86400 # 24 hours -JOB_TIMEOUT = 3600 # 1 hour per job - -SESSION_SYSTEM_PROMPT = """You are an AI assistant working in a background session. Respond directly to the user's request.""" - - -@DBOS.step() -async def setup_namespace(session_id: str) -> str: - """Create namespace, copy secrets, set up RBAC, and apply network policies.""" - namespace = await create_session_namespace(session_id) - await copy_secrets_to_namespace(session_id, namespace) - await setup_session_rbac(session_id, namespace) - await apply_session_namespace_network_policies(session_id, namespace) - return namespace - - -@DBOS.step() -async def cleanup_namespace(session_id: str) -> None: - """Delete the session namespace.""" - await delete_session_namespace(session_id) - - -@DBOS.step() -async def spawn_session_job( - session_id: str, - namespace: str, - prompt: str, - model: str | None = None, - iteration: int = 0, -) -> str: - """Spawn a K8s Job to run Claude with the given prompt.""" - callback_url = ( - f"{settings.backend_internal_url}/internal/sessions/{session_id}/complete" - ) - - job_name = await create_session_job( - session_id=session_id, - namespace=namespace, - prompt=prompt, - callback_url=callback_url, - model=model, - iteration=iteration, - ) - - return job_name - - -def build_conversation_prompt( - messages: list, - repo_url: str | None = None, -) -> str: - """Build prompt from conversation history.""" - history_parts = [] - for msg in messages: - role = "User" if msg.role == "user" else "Assistant" - history_parts.append(f"{role}: {msg.content}") - - context = "\n\n".join(history_parts) - repo_context = f"\n\nRepository: {repo_url}" if repo_url else "" - - return f"""{SESSION_SYSTEM_PROMPT} - -Continue this conversation:{repo_context} - -{context} - -Respond to the user's latest message.""" - - -async def notify_status(user_id: str, session_id: str, status: str): - """Send SSE notification about session status change.""" - await notify_session_updated(user_id, session_id, status) - - -@DBOS.workflow() -async def session_worker_workflow(session_id: str) -> dict[str, Any]: - """Run session workflow with Claude running in isolated K8s Jobs. - - 1. Set up isolated K8s namespace - 2. Spawn job for initial prompt - 3. Wait for job result (via callback) - 4. Add response to conversation - 5. Wait for user message - 6. Spawn job for response - 7. Repeat steps 3-6 - 8. Clean up namespace on completion - """ - logger.info(f"Starting session workflow: {session_id}") - - session = load_session(session_id) - if not session: - return {"status": "failed", "error": "Session not found"} - - namespace = None - iteration = 0 - - try: - # Set up isolated namespace - logger.info(f"Setting up namespace for session: {session_id}") - namespace = await setup_namespace(session_id) - - # Mark session as active - update_session_status( - session_id, - SessionStatus.ACTIVE, - started_at=datetime.now(timezone.utc), - ) - await notify_status(session.user_id, session_id, "active") - - # Add initial prompt as user message - add_message_to_conversation( - session.conversation_id, - "user", - session.prompt, - ) - - # Build initial prompt and spawn job - from mainloop.db import db - - messages = await db.get_messages(session.conversation_id) - prompt = build_conversation_prompt(messages, session.repo_url) - - logger.info(f"Spawning initial job for session: {session_id}") - await spawn_session_job( - session_id, - namespace, - prompt, - model=session.model, - iteration=iteration, - ) - - # Wait for job result - result = await DBOS.recv_async( - topic=TOPIC_JOB_RESULT, - timeout_seconds=JOB_TIMEOUT, - ) - - if result is None: - raise RuntimeError("Job timed out waiting for response") - - if result.get("status") == "failed": - raise RuntimeError(result.get("error", "Job failed")) - - # Add response to conversation - response = result.get("result", {}).get("output", "No response generated.") - add_message_to_conversation( - session.conversation_id, - "assistant", - response, - ) - - # Now wait for user messages in a loop - while True: - iteration += 1 - - # Wait for user input - update_session_status(session_id, SessionStatus.WAITING_ON_USER) - await notify_status(session.user_id, session_id, "waiting_on_user") - - message_response = await DBOS.recv_async( - topic=TOPIC_USER_MESSAGE, - timeout_seconds=USER_INPUT_TIMEOUT, - ) - - if message_response is None: - # Timeout - complete session - update_session_status( - session_id, - SessionStatus.COMPLETED, - completed_at=datetime.now(timezone.utc), - ) - await notify_status(session.user_id, session_id, "completed") - return {"status": "completed", "reason": "timeout"} - - # Got notification that user sent a message (already saved by API) - # Mark as active and spawn job for response - update_session_status(session_id, SessionStatus.ACTIVE) - await notify_status(session.user_id, session_id, "active") - - # Get updated conversation and spawn job - messages = await db.get_messages(session.conversation_id) - prompt = build_conversation_prompt(messages, session.repo_url) - - logger.info( - f"Spawning job for session: {session_id} (iteration {iteration})" - ) - await spawn_session_job( - session_id, - namespace, - prompt, - model=session.model, - iteration=iteration, - ) - - # Wait for job result - result = await DBOS.recv_async( - topic=TOPIC_JOB_RESULT, - timeout_seconds=JOB_TIMEOUT, - ) - - if result is None: - raise RuntimeError("Job timed out waiting for response") - - if result.get("status") == "failed": - raise RuntimeError(result.get("error", "Job failed")) - - # Add response to conversation - response = result.get("result", {}).get("output", "No response generated.") - add_message_to_conversation( - session.conversation_id, - "assistant", - response, - ) - - # Loop back to wait for next user message - - except Exception as e: - logger.error(f"Session workflow failed: {e}") - update_session_status( - session_id, - SessionStatus.FAILED, - completed_at=datetime.now(timezone.utc), - error=str(e), - ) - await notify_status(session.user_id, session_id, "failed") - return {"status": "failed", "error": str(e)} - - finally: - # Clean up namespace - if namespace: - logger.info(f"Cleaning up namespace for session: {session_id}") - try: - await cleanup_namespace(session_id) - except Exception as e: - logger.warning(f"Failed to cleanup namespace: {e}") diff --git a/backend/tests/__init__.py b/backend/tests/__init__.py new file mode 100644 index 0000000..c1e1fda --- /dev/null +++ b/backend/tests/__init__.py @@ -0,0 +1 @@ +"""Mainloop backend test package for unittest discovery.""" diff --git a/backend/tests/runtime/__init__.py b/backend/tests/runtime/__init__.py new file mode 100644 index 0000000..22b2146 --- /dev/null +++ b/backend/tests/runtime/__init__.py @@ -0,0 +1 @@ +"""Runtime adapter and API tests.""" diff --git a/backend/tests/runtime/fixtures/codex/session.json b/backend/tests/runtime/fixtures/codex/session.json index 82c9b20..df2c793 100644 --- a/backend/tests/runtime/fixtures/codex/session.json +++ b/backend/tests/runtime/fixtures/codex/session.json @@ -7,8 +7,6 @@ "provider": "codex", "runtime_type": "codex-app-server", "native_session_id": "thread-codex-fixture-001", - "herdr_session_id": "herdr-session-fixture", - "herdr_agent_id": "codex-agent-fixture", "creation_mode": "created", "ownership_generation": 1 } diff --git a/backend/tests/runtime/fixtures/substrate/kubectl-ate-workers-pinned-cdac9ba.json b/backend/tests/runtime/fixtures/substrate/kubectl-ate-workers-pinned-cdac9ba.json new file mode 100644 index 0000000..bce43a0 --- /dev/null +++ b/backend/tests/runtime/fixtures/substrate/kubectl-ate-workers-pinned-cdac9ba.json @@ -0,0 +1,25 @@ +{ + "workers": [ + { + "labels": { + "workload": "live-agent-gate" + }, + "metadata": { + "name": "worker-1" + }, + "sandboxClass": "gvisor", + "status": { + "allocated": { + "actors": 3 + }, + "capacity": { + "actors": 4 + }, + "state": "WORKER_STATE_ACTIVE" + }, + "workerNamespace": "live-agent-gate", + "workerPod": "worker-1-pod", + "workerPool": "live-agent-gate-workers" + } + ] +} diff --git a/backend/tests/runtime/test_claude.py b/backend/tests/runtime/test_claude.py index aec93a6..b66ae03 100644 --- a/backend/tests/runtime/test_claude.py +++ b/backend/tests/runtime/test_claude.py @@ -28,8 +28,6 @@ def adapter(records: list[dict], *, suffix: str = "stream") -> ClaudeSessionNorm records[0], binding_id=f"claude-binding-{suffix}", workspace_id=f"workspace-{suffix}", - herdr_session_id=f"herdr-session-{suffix}", - herdr_agent_id=f"herdr-agent-{suffix}", ) @@ -40,8 +38,6 @@ def test_binding_preserves_native_identity_and_observed_metadata(self): records[0], binding_id="binding", workspace_id="workspace", - herdr_session_id="herdr-session", - herdr_agent_id="herdr-agent", ) self.assertEqual(binding.provider, "claude") diff --git a/backend/tests/runtime/test_context_model.py b/backend/tests/runtime/test_context_model.py index 25eae58..c7c63f2 100644 --- a/backend/tests/runtime/test_context_model.py +++ b/backend/tests/runtime/test_context_model.py @@ -8,7 +8,7 @@ from mainloop.runtime import agent_api, policy from mainloop.runtime.agent_api import AgentService, hash_token from mainloop.runtime.journal import parse_claude -from mainloop.runtime.native_sessions import config_name, rotation_due +from mainloop.runtime.native_sessions import rotation_due from mainloop.runtime.policy import Actor, PolicyError from mainloop.runtime.standing import ( RecentMessage, @@ -97,11 +97,6 @@ def test_turn_budget_and_unknown_usage(self): ) ) - def test_binding_config_names(self): - self.assertEqual(config_name({"role": "main", "kind": "claude"}), "claude-main") - self.assertEqual(config_name({"role": "child", "kind": "codex"}), "codex-child") - self.assertEqual(config_name({"role": "agent", "kind": "claude"}), "claude") - class StandingTests(unittest.TestCase): def test_carry_over_is_small_and_lists_topic_index_pending_and_recent(self): @@ -175,7 +170,7 @@ def test_context_tokens_and_compact_boundary(self): ), ), ] - ev = parse_claude(lines, file_ref="f.jsonl", native_id="n", agent="a") + ev = parse_claude(lines, file_ref="f.jsonl", native_id="n") self.assertEqual([e.context_tokens for e in ev], [None, 20624, None]) self.assertEqual(ev[2].native_type, "claude.system.compact_boundary") diff --git a/backend/tests/runtime/test_contracts.py b/backend/tests/runtime/test_contracts.py index 9789d83..c2ad6ff 100644 --- a/backend/tests/runtime/test_contracts.py +++ b/backend/tests/runtime/test_contracts.py @@ -24,8 +24,6 @@ def binding(): "provider": "fixture", "runtime_type": "native", "native_session_id": "native-session", - "herdr_session_id": "herdr-session", - "herdr_agent_id": "agent", "creation_mode": "created", "ownership_generation": 1, } @@ -221,6 +219,86 @@ def test_takeover_can_retire_unsent_attempt_with_evidence(self): self.store.create_attempt(attempt("retry", generation=2), 2) self.assertEqual(len(self.store.attempts), 2) + def test_backend_restart_reconciles_persisted_recorded_attempt_before_retry(self): + # A restart leaves the durable attempt row intact. Ownership takeover is + # the in-memory contract's fake-backed model of loading that row under a + # new generation; no transport or database is involved. + recorded = self.store.create_attempt(attempt(), 1) + self.assertEqual(recorded.state, DeliveryState.RECORDED) + self.store.take_ownership(1) + + historical = self.store.attempts[0] + self.assertEqual(historical.state, DeliveryState.RECORDED) + self.assertEqual(historical.ownership_generation, 1) + with self.assertRaises(ContractError): + self.store.create_attempt(attempt("retry", generation=2), 2) + with self.assertRaises(ContractError): + self.store.reconcile(self.evidence("delivered"), 2) + + retired = self.store.reconcile(self.evidence("not_delivered"), 2) + self.assertEqual(retired.state, DeliveryState.FAILED) + self.assertEqual(retired.result, "not_delivered") + self.store.create_attempt(attempt("retry", generation=2), 2) + self.assertEqual( + [item.state for item in self.store.attempts], + [DeliveryState.FAILED, DeliveryState.RECORDED], + ) + + def test_restart_rehydrates_recorded_prompt_without_replay_or_loss(self): + # Model a backend restart with fake durable rows, then load those rows into + # a fresh ContractStore before taking ownership. ContractStore has no I/O; + # this proves the contract semantics, not a database or transport restart. + before_restart = ContractStore(binding()) + prompt = {**message(), "payload_ref": "fixture://prompts/restart-window"} + before_restart.record_message(prompt, 1) + before_restart.create_attempt(attempt(), 1) + fake_database = { + "binding": before_restart.binding.model_dump(mode="json"), + "messages": [ + item.model_dump(mode="json") for item in before_restart.messages + ], + "attempts": [ + item.model_dump(mode="json") for item in before_restart.attempts + ], + } + + after_restart = ContractStore(fake_database["binding"]) + for persisted_message in fake_database["messages"]: + after_restart.record_message(persisted_message, 1) + for persisted_attempt in fake_database["attempts"]: + after_restart.create_attempt(persisted_attempt, 1) + + binding_after_takeover = after_restart.take_ownership(1) + self.assertEqual(binding_after_takeover.ownership_generation, 2) + self.assertEqual(after_restart.messages[0].payload_ref, prompt["payload_ref"]) + pending = after_restart.checkpoint(2).pending_delivery + self.assertEqual(len(pending), 1) + self.assertEqual(pending[0].state, DeliveryState.RECORDED) + with self.assertRaises(ContractError): + after_restart.create_attempt(attempt("replay", generation=2), 2) + + retired = after_restart.reconcile( + { + "attempt_id": "attempt", + "binding_id": "binding", + "evidence_ref": "fixture://journal/no-prompt-receipt", + "observed_at": NOW, + "outcome": "not_delivered", + }, + 2, + ) + self.assertEqual(retired.state, DeliveryState.FAILED) + self.assertEqual(after_restart.messages[0].payload_ref, prompt["payload_ref"]) + retry = after_restart.create_attempt(attempt("retry", generation=2), 2) + self.assertEqual(retry.logical_message_id, "message") + self.assertEqual( + [ + (item.attempt_id, item.state) + for item in after_restart.checkpoint(2).pending_delivery + ], + [("retry", DeliveryState.RECORDED)], + ) + def test_takeover_reconnect_deduplicates_source_event(self): original = self.store.ingest(attention(), 1) self.store.take_ownership(1) diff --git a/backend/tests/runtime/test_credential_broker.py b/backend/tests/runtime/test_credential_broker.py new file mode 100644 index 0000000..617bad5 --- /dev/null +++ b/backend/tests/runtime/test_credential_broker.py @@ -0,0 +1,331 @@ +"""Credential broker tests use only synthetic fixture values and an in-memory Secret store.""" + +from __future__ import annotations + +import asyncio +import base64 +import json +import tempfile +import unittest +from datetime import UTC, datetime, timedelta +from pathlib import Path +from unittest.mock import patch + +from mainloop.runtime.credential_broker import ( + CredentialBroker, + CredentialNeedsSignin, + CredentialSecretMissing, +) + + +async def _run_sync_in_test(function, *args, **kwargs): + """Keep fake Secret and temporary-file operations deterministic in tests.""" + await asyncio.sleep(0) + return function(*args, **kwargs) + + +def jwt(expiry: int) -> str: + payload = ( + base64.urlsafe_b64encode( + json.dumps({"exp": expiry}, separators=(",", ":")).encode() + ) + .decode() + .rstrip("=") + ) + return f"eyJhbGciOiJub25lIn0.{payload}.fixture" + + +class MemorySecretStore: + def __init__(self): + self.values: dict[tuple[str, str], dict[str, str]] = {} + self.publish_count = 0 + + def precreate(self, namespace: str, name: str): + self.values[(namespace, name)] = {} + + def read(self, namespace: str, name: str): + value = self.values.get((namespace, name)) + return dict(value) if value is not None else None + + def publish(self, namespace: str, name: str, values): + if (namespace, name) not in self.values: + raise AssertionError("credential Secret must be pre-created") + self.values[(namespace, name)] = dict(values) + self.publish_count += 1 + + +class CredentialBrokerTests(unittest.TestCase): + def setUp(self): + self._to_thread_patch = patch.object( + asyncio, "to_thread", new=_run_sync_in_test + ) + self._to_thread_patch.start() + + def tearDown(self): + self._to_thread_patch.stop() + + def test_codex_seed_publishes_access_token_and_delivers_only_synthetic_auth(self): + async def exercise(root: Path): + store = MemorySecretStore() + source_access = jwt( + int((datetime.now(UTC) + timedelta(hours=1)).timestamp()) + ) + source_id = jwt(int((datetime.now(UTC) + timedelta(hours=1)).timestamp())) + source = { + "auth_mode": "chatgpt", + "tokens": { + "id_token": source_id, + "access_token": source_access, + "refresh_token": "synthetic-refresh-fixture", + "account_id": "fixture-account-42", + }, + "last_refresh": datetime.now(UTC).isoformat(), + } + auth_path = root / "auth.json" + auth_path.write_text(json.dumps(source), encoding="utf-8") + broker = CredentialBroker( + store=store, + codex_auth_path=str(auth_path), + namespace="test-control", + account="fixture-owner", + ) + store.precreate("test-control", broker.secret_name("codex")) + + statuses = await asyncio.gather( + broker.status("codex"), broker.status("codex") + ) + status = statuses[0] + self.assertTrue(status.available) + self.assertFalse(status.needs_signin) + self.assertEqual(status.expires_at.tzinfo, UTC) + self.assertEqual(status.state, "available") + self.assertEqual(store.publish_count, 1) + + secret = store.read("test-control", broker.secret_name("codex")) + self.assertEqual(secret["injection-value"], source_access) + placeholder = json.loads(await broker.codex_placeholder_auth()) + self.assertEqual(placeholder["tokens"]["account_id"], "fixture-account-42") + self.assertEqual(placeholder["tokens"]["refresh_token"], "") + self.assertNotEqual(placeholder["tokens"]["access_token"], source_access) + self.assertNotEqual(placeholder["tokens"]["id_token"], source_id) + self.assertNotIn(source_access, json.dumps(placeholder)) + self.assertNotIn("synthetic-refresh-fixture", json.dumps(placeholder)) + self.assertEqual(len(placeholder["tokens"]["access_token"].split(".")), 3) + + with tempfile.TemporaryDirectory() as directory: + asyncio.run(exercise(Path(directory))) + + def test_expired_codex_token_is_not_published_and_requires_signin(self): + async def exercise(root: Path): + store = MemorySecretStore() + source = { + "tokens": { + "id_token": jwt(2_000_000_000), + "access_token": jwt(1), + "refresh_token": "synthetic-refresh-fixture", + "account_id": "fixture-account-expired", + }, + "last_refresh": datetime.now(UTC).isoformat(), + } + path = root / "auth.json" + path.write_text(json.dumps(source), encoding="utf-8") + broker = CredentialBroker(store=store, codex_auth_path=str(path)) + store.precreate("mainloop-control", broker.secret_name("codex")) + + status = await broker.seed_configured("codex") + self.assertFalse(status.available) + self.assertTrue(status.needs_signin) + self.assertEqual(status.state, "expired") + values = store.read("mainloop-control", broker.secret_name("codex")) + self.assertEqual(values["injection-value"], "") + + valid_expiry = int((datetime.now(UTC) + timedelta(hours=1)).timestamp()) + path.write_text( + json.dumps( + { + "tokens": { + "id_token": jwt(valid_expiry), + "access_token": jwt(valid_expiry), + "refresh_token": "new-synthetic-refresh", + "account_id": "fixture-account-expired", + }, + "last_refresh": datetime.now(UTC).isoformat(), + } + ), + encoding="utf-8", + ) + status = await broker.status("codex") + self.assertEqual(status.state, "expired") + self.assertEqual(store.publish_count, 1) + with self.assertRaises(CredentialNeedsSignin) as raised: + await broker.codex_placeholder_auth() + self.assertEqual(str(raised.exception), "Codex needs sign-in") + self.assertNotIn(str(path), str(raised.exception)) + + with tempfile.TemporaryDirectory() as directory: + asyncio.run(exercise(Path(directory))) + + def test_expiring_stored_token_is_removed_from_injection_before_signin(self): + async def exercise(root: Path): + store = MemorySecretStore() + expiry = int((datetime.now(UTC) + timedelta(hours=1)).timestamp()) + source = { + "tokens": { + "id_token": jwt(expiry), + "access_token": jwt(expiry), + "refresh_token": "synthetic-refresh-fixture", + "account_id": "fixture-account-near-expiry", + }, + "last_refresh": datetime.now(UTC).isoformat(), + } + path = root / "auth.json" + path.write_text(json.dumps(source), encoding="utf-8") + broker = CredentialBroker(store=store, codex_auth_path=str(path)) + store.precreate("mainloop-control", broker.secret_name("codex")) + await broker.seed_configured("codex") + name = broker.secret_name("codex") + values = store.read("mainloop-control", name) + values["expires-at"] = str( + int((datetime.now(UTC) + timedelta(seconds=30)).timestamp()) + ) + store.publish("mainloop-control", name, values) + + status = await broker.status("codex") + self.assertTrue(status.needs_signin) + values = store.read("mainloop-control", name) + self.assertEqual(values["injection-value"], "") + self.assertEqual(values["needs-signin"], "true") + with self.assertRaises(CredentialNeedsSignin): + await broker.codex_placeholder_auth() + + with tempfile.TemporaryDirectory() as directory: + asyncio.run(exercise(Path(directory))) + + def test_claude_file_is_seeded_by_path_and_missing_source_is_safe(self): + async def exercise(root: Path): + store = MemorySecretStore() + path = root / "claude-token" + path.write_text("synthetic-claude-token\n", encoding="utf-8") + broker = CredentialBroker(store=store, claude_token_path=str(path)) + store.precreate("mainloop-control", broker.secret_name("claude")) + status = await broker.status("claude") + self.assertTrue(status.available) + self.assertFalse(status.needs_signin) + values = store.read("mainloop-control", broker.secret_name("claude")) + self.assertEqual(values["injection-value"], "synthetic-claude-token") + + missing_store = MemorySecretStore() + missing = CredentialBroker( + store=missing_store, claude_token_path=str(root / "missing-token") + ) + missing_store.precreate("mainloop-control", missing.secret_name("claude")) + with self.assertRaises(CredentialNeedsSignin) as raised: + await missing.seed_configured("claude") + self.assertEqual(str(raised.exception), "Claude needs sign-in") + + with tempfile.TemporaryDirectory() as directory: + asyncio.run(exercise(Path(directory))) + + def test_reauth_result_methods_publish_only_valid_synthetic_fixture_values(self): + async def exercise(): + store = MemorySecretStore() + broker = CredentialBroker(store=store) + store.precreate("mainloop-control", broker.secret_name("codex")) + store.precreate("mainloop-control", broker.secret_name("claude")) + access = jwt(int((datetime.now(UTC) + timedelta(hours=1)).timestamp())) + document = { + "auth_mode": "chatgpt", + "tokens": { + "id_token": access, + "access_token": access, + "refresh_token": "synthetic-refresh-fixture", + "account_id": "fixture-account-re-auth", + }, + "last_refresh": datetime.now(UTC).isoformat(), + } + status = await broker.store_codex_auth_document( + json.dumps(document).encode("utf-8") + ) + self.assertTrue(status.available) + self.assertEqual( + store.read("mainloop-control", broker.secret_name("codex"))[ + "injection-value" + ], + access, + ) + claude = await broker.store_claude_token("synthetic-claude-token") + self.assertTrue(claude.available) + self.assertEqual( + store.read("mainloop-control", broker.secret_name("claude"))[ + "injection-value" + ], + "synthetic-claude-token", + ) + placeholder = await broker.claude_placeholder_token() + self.assertNotEqual(placeholder, "synthetic-claude-token") + self.assertIn("egress-placeholder", placeholder) + + asyncio.run(exercise()) + + def test_missing_secret_does_not_attempt_file_seeding(self): + async def exercise(root: Path): + store = MemorySecretStore() + path = root / "auth.json" + expiry = int((datetime.now(UTC) + timedelta(hours=1)).timestamp()) + path.write_text( + json.dumps( + { + "tokens": { + "id_token": jwt(expiry), + "access_token": jwt(expiry), + "refresh_token": "synthetic-refresh-fixture", + "account_id": "fixture-account-missing-secret", + }, + "last_refresh": datetime.now(UTC).isoformat(), + } + ), + encoding="utf-8", + ) + broker = CredentialBroker(store=store, codex_auth_path=str(path)) + + status = await broker.status("codex") + self.assertEqual(status.state, "missing") + self.assertFalse(status.available) + self.assertEqual(store.values, {}) + self.assertEqual(store.publish_count, 0) + + with self.assertRaises(CredentialSecretMissing): + await broker.seed_configured("codex") + self.assertEqual(store.publish_count, 0) + + with tempfile.TemporaryDirectory() as directory: + asyncio.run(exercise(Path(directory))) + + def test_rejected_secret_is_not_replaced_from_a_configured_file(self): + async def exercise(root: Path): + store = MemorySecretStore() + path = root / "claude-token" + path.write_text("synthetic-new-claude-token", encoding="utf-8") + broker = CredentialBroker(store=store, claude_token_path=str(path)) + secret_name = broker.secret_name("claude") + store.values[("mainloop-control", secret_name)] = { + "oauth-token": "synthetic-rejected-token", + "injection-value": "", + "needs-signin": "true", + } + + status = await broker.status("claude") + self.assertEqual(status.state, "rejected") + self.assertFalse(status.available) + self.assertEqual( + store.read("mainloop-control", secret_name)["oauth-token"], + "synthetic-rejected-token", + ) + self.assertEqual(store.publish_count, 0) + + with tempfile.TemporaryDirectory() as directory: + asyncio.run(exercise(Path(directory))) + + +if __name__ == "__main__": + unittest.main() diff --git a/backend/tests/runtime/test_credential_reauth.py b/backend/tests/runtime/test_credential_reauth.py new file mode 100644 index 0000000..436e276 --- /dev/null +++ b/backend/tests/runtime/test_credential_reauth.py @@ -0,0 +1,164 @@ +"""Credential re-auth runner tests use fake state and sanitized challenge logs only.""" + +from __future__ import annotations + +import asyncio +import json +import unittest +from unittest.mock import patch + +from mainloop.config import settings +from mainloop.runtime import credential_reauth_api +from mainloop.runtime.credential_reauth import ( + FakeCredentialReauthRunner, + KubernetesCredentialReauthRunner, + _challenge_from_logs, +) +from starlette.requests import Request + + +async def _run_sync_in_test(function, *args, **kwargs): + """Keep fake Kubernetes API calls deterministic in tests.""" + await asyncio.sleep(0) + return function(*args, **kwargs) + + +class CredentialReauthTests(unittest.TestCase): + def setUp(self): + self._to_thread_patch = patch.object( + asyncio, "to_thread", new=_run_sync_in_test + ) + self._to_thread_patch.start() + + def tearDown(self): + self._to_thread_patch.stop() + + def test_fake_runner_keeps_callback_token_private_and_completes(self): + async def exercise(): + runner = FakeCredentialReauthRunner() + job = await runner.start("codex", owner="fixture-user") + self.assertEqual(job.provider, "codex") + self.assertEqual(job.state, "running") + self.assertIsNone(await runner.callback_provider(job.id, "wrong-token")) + + token = runner._jobs[job.id][1] + self.assertEqual(await runner.callback_provider(job.id, token), "codex") + await runner.complete(job.id, token) + completed = await runner.status(job.id) + self.assertEqual(completed.state, "completed") + self.assertNotIn(token, repr(completed)) + + asyncio.run(exercise()) + + def test_kubernetes_runner_builds_a_bounded_non_root_job(self): + class BatchApi: + job = None + + def create_namespaced_job(self, namespace, body): + self.job = body + + async def exercise(): + batch = BatchApi() + runner = KubernetesCredentialReauthRunner( + batch_api=batch, core_api=object() + ) + with patch.object(settings, "substrate_reauth_job_image", "fixture-image"): + job = await runner.start("codex") + spec = batch.job.spec + container = spec.template.spec.containers[0] + self.assertEqual(job.state, "running") + self.assertEqual(spec.backoff_limit, 0) + self.assertGreater(spec.active_deadline_seconds, 0) + self.assertTrue(spec.template.spec.automount_service_account_token is False) + self.assertTrue(container.security_context.run_as_non_root) + self.assertEqual( + container.command, ["node", "/usr/local/bin/mainloop-reauth"] + ) + token = next( + env.value + for env in container.env + if env.name == "MAINLOOP_REAUTH_CALLBACK_TOKEN" + ) + self.assertNotIn(token, repr(job)) + self.assertEqual(await runner.callback_provider(job.id, token), "codex") + await runner.complete(job.id, token) + self.assertIsNone(await runner.callback_provider(job.id, token)) + + asyncio.run(exercise()) + + def test_callback_authenticates_job_before_storing_credential(self): + class MemoryBroker: + stored = None + + async def store_codex_auth_document(self, raw): + self.stored = raw + + async def store_claude_token(self, token): + raise AssertionError("unexpected provider") + + async def exercise(): + runner = FakeCredentialReauthRunner() + job = await runner.start("codex") + token = runner._jobs[job.id][1] + broker = MemoryBroker() + payload = {"provider": "codex", "auth": {"fixture": "synthetic-auth"}} + raw = json.dumps(payload).encode() + delivered = False + + async def receive(): + nonlocal delivered + if delivered: + return {"type": "http.request", "body": b"", "more_body": False} + delivered = True + return {"type": "http.request", "body": raw, "more_body": False} + + request = Request( + {"type": "http", "method": "POST", "headers": []}, receive + ) + with ( + patch.object(credential_reauth_api, "_reauth_runner", runner), + patch.object( + credential_reauth_api, "CredentialBroker", return_value=broker + ), + ): + response = await credential_reauth_api.receive_reauth_result( + job.id, request, token + ) + self.assertEqual(response.status_code, 204) + self.assertEqual( + broker.stored, + json.dumps(payload["auth"], separators=(",", ":")).encode(), + ) + self.assertEqual((await runner.status(job.id)).state, "completed") + + asyncio.run(exercise()) + + def test_only_https_challenges_from_allowed_provider_hosts_are_returned(self): + good = _challenge_from_logs( + "noise\nMAINLOOP_REAUTH_CHALLENGE " + '{"url":"https://auth.openai.com/codex/device","code":"ABCD-EFGH"}\n' + ) + self.assertEqual(good.url, "https://auth.openai.com/codex/device") + self.assertEqual(good.code, "ABCD-EFGH") + self.assertIsNone( + _challenge_from_logs( + "MAINLOOP_REAUTH_CHALLENGE " + '{"url":"http://auth.openai.com/","code":"ABCD-EFGH"}' + ) + ) + self.assertIsNone( + _challenge_from_logs( + "MAINLOOP_REAUTH_CHALLENGE " + '{"url":"https://attacker.example/","code":"ABCD-EFGH"}' + ) + ) + self.assertIsNone( + _challenge_from_logs( + "MAINLOOP_REAUTH_CHALLENGE " + '{"url":"https://auth.openai.com/","code":"secret-token"}' + ) + ) + + +if __name__ == "__main__": + unittest.main() diff --git a/backend/tests/runtime/test_delivery_suspend_fence.py b/backend/tests/runtime/test_delivery_suspend_fence.py new file mode 100644 index 0000000..11fb8c0 --- /dev/null +++ b/backend/tests/runtime/test_delivery_suspend_fence.py @@ -0,0 +1,137 @@ +"""The delivery write shares the workspace row lock used by suspend reservation.""" + +from __future__ import annotations + +import unittest +from contextlib import asynccontextmanager +from types import SimpleNamespace +from unittest.mock import AsyncMock, patch + +from mainloop.db import db +from mainloop.runtime.native_sessions import _record_delivery_message + + +class FakeConnection: + def __init__(self, workspace): + self.workspace = workspace + self.events: list[str] = [] + + @asynccontextmanager + async def transaction(self): + self.events.append("begin") + try: + yield self + except Exception: + self.events.append("rollback") + raise + else: + self.events.append("commit") + + async def fetchrow(self, query, *_args): + if "FROM workspace_bindings" in query: + self.events.append("workspace-lock") + if "FOR UPDATE" not in query: + raise AssertionError( + "delivery recording must lock the workspace binding row" + ) + return {"workspace_id": "session-1"} + elif "FROM workspace_lifecycles" in query: + self.events.append("lifecycle-read") + else: + raise AssertionError(f"unexpected fetchrow query: {query}") + return self.workspace + + async def execute(self, query, *_args): + if "INSERT INTO native_deliveries" in query: + self.events.append("delivery-insert") + elif "UPDATE workspace_lifecycles" in query: + self.events.append("activity-touch") + + +def fake_connection(connection): + @asynccontextmanager + async def acquire(): + yield connection + + return acquire() + + +class DeliverySuspendFenceTests(unittest.IsolatedAsyncioTestCase): + async def test_message_and_delivery_are_recorded_after_the_workspace_lock(self): + connection = FakeConnection( + {"desired_state": "active", "observed_state": "running"} + ) + + async def create_message(**_kwargs): + connection.events.append("message-insert") + return SimpleNamespace(id="message-1") + + with ( + patch.object( + db, + "connection", + return_value=fake_connection(connection), + ), + patch.object( + db, + "create_message", + new=AsyncMock(side_effect=create_message), + ) as create, + ): + message_id = await _record_delivery_message( + session_id="session-1", + conversation_id="conversation-1", + text="hello", + state="recorded", + source="user", + ) + + self.assertEqual(message_id, "message-1") + self.assertEqual( + connection.events, + [ + "begin", + "workspace-lock", + "lifecycle-read", + "message-insert", + "delivery-insert", + "activity-touch", + "commit", + ], + ) + self.assertEqual(create.await_args.kwargs["conn"], connection) + + async def test_suspending_workspace_rejects_the_delivery_before_recording(self): + connection = FakeConnection( + {"desired_state": "suspended", "observed_state": "suspending"} + ) + with ( + patch.object( + db, + "connection", + return_value=fake_connection(connection), + ), + patch.object( + db, + "create_message", + new=AsyncMock(), + ) as create, + ): + with self.assertRaisesRegex(ValueError, "resume it before sending"): + await _record_delivery_message( + session_id="session-1", + conversation_id="conversation-1", + text="hello", + state="recorded", + source="user", + ) + + self.assertEqual( + connection.events, + ["begin", "workspace-lock", "lifecycle-read", "rollback"], + ) + create.assert_not_awaited() + + +if __name__ == "__main__": + unittest.main() diff --git a/backend/tests/runtime/test_gate5_setup.py b/backend/tests/runtime/test_gate5_setup.py new file mode 100644 index 0000000..b067561 --- /dev/null +++ b/backend/tests/runtime/test_gate5_setup.py @@ -0,0 +1,1306 @@ +"""Credential-free regressions for the gate-5 setup script's build and rerun identity.""" + +import json +import os +import subprocess # nosec B404 - fixed shell calls exercise cleanup with fake commands. +import tempfile +import unittest +from contextlib import redirect_stdout +from dataclasses import replace +from io import StringIO +from pathlib import Path +from types import SimpleNamespace +from unittest.mock import patch +from urllib.error import HTTPError + +from mainloop.runtime.substrate import ( + ActorFailedToStart, + ActorRecord, + ActorState, + IdentityConflict, +) + +from scripts import gate5_setup + +FIXTURE_KUBECONFIG = "/fixture/kubeconfig" +FIXTURE_KO = "/fixture/substrate/bin/ko" + + +def completed(argv, returncode=0, stdout="", stderr=""): + return SimpleNamespace( + args=argv, returncode=returncode, stdout=stdout, stderr=stderr + ) + + +class Gate5SourceAndBuildTests(unittest.TestCase): + def parse_cli_args(self, manifest: Path, *extra: str): + argv = [ + "gate5_setup.py", + "--context", + "kind-substrate-preview", + "--kubeconfig", + FIXTURE_KUBECONFIG, + "--substrate-src", + "/fixture/substrate", + "--atespace", + "nonroot-check", + "--template-version", + "v3", + "--image", + "localhost:5001/live-agent-gate@sha256:" + "a" * 64, + "--manifest", + str(manifest), + "--state-file", + "gate5-test-state.json", + "--egress-deny-all", + *extra, + ] + with patch("sys.argv", argv): + return gate5_setup.parse_args() + + def test_worker_pool_option_drives_product_template_selector_and_labels(self): + manifest = ( + Path(__file__).resolve().parents[3] + / "spikes/substrate-workspace-adapter/k8s/actor-template.yaml.tmpl" + ) + args = self.parse_cli_args(manifest, "--worker-pool", "isolated-pool") + worker_pool, actor_template = gate5_setup.render_gate_manifest(args) + + self.assertEqual(args.atespace, "nonroot-check") + self.assertEqual(args.worker_pool, "isolated-pool") + self.assertIn("name: isolated-pool", worker_pool) + self.assertIn("workload: isolated-pool", worker_pool) + self.assertIn("name: live-agent-gate-v3", actor_template) + self.assertIn("workload: isolated-pool", actor_template) + self.assertIn( + "name: MAINLOOP_API, value: " + "http://mainloop-backend.mainloop-control.svc.cluster.local:8000", + actor_template, + ) + self.assertIn("mountPath: /work", actor_template) + self.assertIn("value: /work/repo", actor_template) + self.assertIn("durableDir: {}", actor_template) + self.assertIn("path: /healthz, port: 8090", actor_template) + self.assertNotIn("/workspace", actor_template) + + def test_worker_pool_defaults_to_the_atespace_name(self): + manifest = ( + Path(__file__).resolve().parents[3] + / "spikes/substrate-workspace-adapter/k8s/actor-template.yaml.tmpl" + ) + + args = self.parse_cli_args(manifest) + + self.assertEqual(args.worker_pool, "nonroot-check") + + def test_existing_worker_image_is_pinned_and_skips_ko_reference(self): + manifest = ( + Path(__file__).resolve().parents[3] + / "spikes/substrate-workspace-adapter/k8s/actor-template.yaml.tmpl" + ) + worker_image = "localhost:5001/ateom-gvisor@sha256:" + "b" * 64 + args = self.parse_cli_args(manifest, "--worker-image", worker_image) + + worker_pool, _ = gate5_setup.render_gate_manifest(args) + + self.assertIn(f"workerImage: {worker_image}", worker_pool) + self.assertNotIn("ko://", worker_pool) + + def test_existing_worker_image_rejects_tags_and_short_digests(self): + manifest = ( + Path(__file__).resolve().parents[3] + / "spikes/substrate-workspace-adapter/k8s/actor-template.yaml.tmpl" + ) + for image in ( + "localhost:5001/ateom-gvisor:latest", + "localhost:5001/ateom-gvisor@sha256:bad", + ): + with ( + self.subTest(image=image), + self.assertRaisesRegex(RuntimeError, "full sha256 digest"), + ): + args = self.parse_cli_args(manifest, "--worker-image", image) + gate5_setup.render_gate_manifest(args) + + def test_image_manifest_preflight_checks_registry_endpoint_and_accept_types(self): + calls = [] + + class Response: + status = 200 + + def __enter__(self): + return self + + def __exit__(self, *_args): + return False + + def opener(request, *, timeout): + calls.append((request, timeout)) + return Response() + + image = "localhost:5001/live-agent-gate@sha256:" + "a" * 64 + gate5_setup.verify_image_manifest(image, opener=opener) + + request, timeout = calls[0] + self.assertEqual( + request.full_url, + f"http://localhost:5001/v2/live-agent-gate/manifests/sha256:{'a' * 64}", + ) + self.assertEqual(request.get_method(), "HEAD") + self.assertEqual( + request.get_header("Accept"), gate5_setup.IMAGE_MANIFEST_ACCEPT + ) + self.assertEqual(timeout, 10) + + def test_image_manifest_preflight_fails_before_template_on_missing_manifest(self): + image = "localhost:5001/live-agent-gate@sha256:" + "b" * 64 + + def missing(request, *, timeout): + error = HTTPError(request.full_url, 404, "MANIFEST_UNKNOWN", {}, None) + error.close() + raise error + + with self.assertRaisesRegex(RuntimeError, "registry returned HTTP 404"): + gate5_setup.verify_image_manifest(image, opener=missing) + + def test_image_manifest_preflight_rejects_tag_or_malformed_digest(self): + calls = [] + + def opener(*_args, **_kwargs): + calls.append(True) + + for image in ( + "localhost:5001/live-agent-gate:latest", + "localhost:5001/live-agent-gate@sha256:bad", + ): + with ( + self.subTest(image=image), + self.assertRaisesRegex(RuntimeError, "full sha256 digest"), + ): + gate5_setup.verify_image_manifest(image, opener=opener) + self.assertEqual(calls, []) + + def make_source(self, root: Path) -> Path: + (root / ".git").mkdir(parents=True) + (root / "go.mod").write_text("module fixture\n") + (root / ".ko.yaml").write_text("defaultBaseImage: scratch\n") + return root + + def test_source_requires_pinned_checkout_and_required_files(self): + with tempfile.TemporaryDirectory() as temp_dir: + source = self.make_source(Path(temp_dir) / "substrate") + calls = [] + + def runner(argv, **kwargs): + calls.append(argv) + return completed(argv, stdout=gate5_setup.SUBSTRATE_FORK_COMMIT + "\n") + + self.assertEqual( + gate5_setup.verify_substrate_source(str(source), runner=runner), + str(source), + ) + self.assertEqual(calls[0][:4], ["git", "-C", str(source), "rev-parse"]) + + def test_source_rejects_wrong_commit(self): + with tempfile.TemporaryDirectory() as temp_dir: + source = self.make_source(Path(temp_dir) / "substrate") + + def runner(argv, **kwargs): + return completed(argv, stdout="a" * 40) + + with self.assertRaisesRegex(RuntimeError, "must be pinned"): + gate5_setup.verify_substrate_source(str(source), runner=runner) + + def test_source_rejects_missing_go_module_files(self): + with tempfile.TemporaryDirectory() as temp_dir: + source = Path(temp_dir) / "substrate" + source.mkdir() + (source / ".git").mkdir() + with self.assertRaisesRegex(RuntimeError, "missing go.mod"): + gate5_setup.verify_substrate_source(str(source)) + + def test_ko_resolve_uses_pinned_source_cwd_and_explicit_kube_target(self): + with tempfile.TemporaryDirectory() as temp_dir: + source = self.make_source(Path(temp_dir) / "substrate") + calls = [] + + def runner(argv, **kwargs): + calls.append((argv, kwargs)) + if argv[0] == "git": + return completed( + argv, stdout=gate5_setup.SUBSTRATE_FORK_COMMIT + "\n" + ) + if argv[1:3] == ["resolve", "-f"]: + return completed(argv, stdout="resolved-yaml") + return completed(argv) + + with patch.dict("os.environ", {"KO_DOCKER_REPO": "localhost:5001"}): + gate5_setup.apply_worker_pool( + ( + "apiVersion: v1\n" + "workerImage: ko://github.com/agent-substrate/substrate/cmd/ateom-gvisor\n" + ), + kubeconfig=FIXTURE_KUBECONFIG, + context="kind-substrate-preview", + ko=FIXTURE_KO, + substrate_src=str(source), + runner=runner, + ) + + ko_argv, ko_kwargs = calls[1] + self.assertEqual(ko_argv[:3], [FIXTURE_KO, "resolve", "-f"]) + self.assertEqual(ko_kwargs["cwd"], str(source)) + apply_argv, apply_kwargs = calls[2] + self.assertEqual( + apply_argv[:5], + [ + "kubectl", + "--context", + "kind-substrate-preview", + "--kubeconfig", + FIXTURE_KUBECONFIG, + ], + ) + self.assertEqual(apply_kwargs["input"], "resolved-yaml") + + def test_existing_worker_image_applies_without_ko_or_source_checkout(self): + calls = [] + + def runner(argv, **kwargs): + calls.append((argv, kwargs)) + return completed(argv) + + gate5_setup.apply_worker_pool( + "apiVersion: v1\nkind: Namespace\nmetadata:\n name: lane-a\n", + kubeconfig=FIXTURE_KUBECONFIG, + context="kind-substrate-preview", + ko=FIXTURE_KO, + substrate_src=None, + runner=runner, + ) + + self.assertEqual(len(calls), 1) + apply_argv, apply_kwargs = calls[0] + self.assertEqual( + apply_argv, + [ + "kubectl", + "--context", + "kind-substrate-preview", + "--kubeconfig", + FIXTURE_KUBECONFIG, + "apply", + "-f", + "-", + ], + ) + self.assertIn("kind: Namespace", apply_kwargs["input"]) + + def test_cluster_identity_uses_explicit_context_and_namespace_uid(self): + calls = [] + results = [ + json.dumps( + { + "contexts": [{"name": "kind-substrate-preview"}], + "clusters": [{"cluster": {"server": "https://127.0.0.1:45147"}}], + } + ), + json.dumps({"metadata": {"uid": "kube-system-uid"}}), + ] + + def runner(argv, **kwargs): + calls.append(argv) + return completed(argv, stdout=results.pop(0)) + + identity = gate5_setup.get_cluster_identity( + context="kind-substrate-preview", + kubeconfig=FIXTURE_KUBECONFIG, + runner=runner, + ) + self.assertEqual( + identity, + { + "api_server_url": "https://127.0.0.1:45147", + "kube_system_namespace_uid": "kube-system-uid", + }, + ) + self.assertTrue( + all("--context" in call and "--kubeconfig" in call for call in calls) + ) + + def test_actor_health_uses_actor_route_and_shim_port(self): + class FakeConnection: + def __init__(self, _host, _port, timeout): + self.timeout = timeout + self.tunnel = None + self.requested = None + + def set_tunnel(self, target, *, headers): + self.tunnel = (target, headers) + + def request(self, method, path): + self.requested = (method, path) + + def getresponse(self): + return SimpleNamespace(status=200, read=lambda: b"ok") + + def close(self): + pass + + connections = [] + + def make_connection(*args, **kwargs): + connection = FakeConnection(*args, **kwargs) + connections.append(connection) + return connection + + with patch.object(gate5_setup.http.client, "HTTPConnection", make_connection): + self.assertTrue( + gate5_setup.actor_health_check( + port=18091, + atespace="live-agent-gate", + actor_name="claude-gate5", + ) + ) + self.assertEqual( + connections[0].tunnel, + ( + "actor-upstream:8090", + {"ate-target-actor": "live-agent-gate/claude-gate5"}, + ), + ) + self.assertEqual(connections[0].requested, ("GET", "/healthz")) + + def test_actor_router_tunnel_forwards_to_connect_listener(self): + class FakeSocket: + def __enter__(self): + return self + + def __exit__(self, *_exc): + return False + + class FakeProcess: + def __init__(self): + self.terminated = False + self.stdout = None + + def poll(self): + return None + + def terminate(self): + self.terminated = True + + def wait(self, timeout): + self.wait_timeout = timeout + + args = SimpleNamespace( + context="kind-substrate-preview", + kubeconfig=FIXTURE_KUBECONFIG, + router_port=18081, + ) + process = FakeProcess() + with ( + patch.object( + gate5_setup.subprocess, "Popen", return_value=process + ) as popen, + patch.object( + gate5_setup.socket, "create_connection", return_value=FakeSocket() + ), + gate5_setup.actor_router_tunnel(args) as route_port, + ): + self.assertEqual(route_port, 18081) + + command = popen.call_args.args[0] + self.assertIn("18081:8081", command) + self.assertTrue(process.terminated) + + def egress_args(self, **mode): + base = dict( + ate_cli="/fixture/kubectl-ate", + kubeconfig=FIXTURE_KUBECONFIG, + context="kind-substrate-preview", + atespace="live-agent-gate", + actor_name="claude-gate5", + egress_deny_all=False, + egress_allow_all=False, + egress_hostnames=None, + egress_cidr=None, + ) + base.update(mode) + return SimpleNamespace(**base) + + def test_egress_policy_manifest_has_one_rule_per_mode(self): + cases = [ + ({"egress_deny_all": True}, []), + ({"egress_allow_all": True}, [{"all": {}}]), + ( + {"egress_hostnames": ["api.anthropic.com", "api.openai.com"]}, + [{"hostnames": {"patterns": ["api.anthropic.com", "api.openai.com"]}}], + ), + ({"egress_cidr": "192.0.2.1/32"}, [{"cidrs": {"cidrs": ["192.0.2.1/32"]}}]), + ] + for mode, rules in cases: + with self.subTest(mode=mode): + manifest = json.loads( + gate5_setup.egress_policy_manifest(self.egress_args(**mode)) + ) + self.assertEqual(manifest, {"rules": rules}) + + def test_egress_policy_manifest_refuses_no_mode(self): + with self.assertRaises(ValueError): + gate5_setup.egress_policy_manifest(self.egress_args()) + + def expected_egress_argv(self, verb): + return [ + "/fixture/kubectl-ate", + "--kubeconfig", + FIXTURE_KUBECONFIG, + "--context", + "kind-substrate-preview", + verb, + "egress-policy", + "claude-gate5", + "--atespace", + "live-agent-gate", + "--filename", + "-", + ] + + def expected_get_egress_argv(self): + return [ + "/fixture/kubectl-ate", + "--kubeconfig", + FIXTURE_KUBECONFIG, + "--context", + "kind-substrate-preview", + "get", + "egress-policy", + "claude-gate5", + "--atespace", + "live-agent-gate", + "-o", + "json", + ] + + def test_apply_egress_policy_creates_through_explicit_kube_target(self): + args = self.egress_args(egress_deny_all=True) + with patch.object( + gate5_setup.subprocess, + "run", + side_effect=lambda argv, **_: completed(argv), + ) as run: + gate5_setup.apply_egress_policy(args) + self.assertEqual(run.call_count, 1) + self.assertEqual(run.call_args.args[0], self.expected_egress_argv("create")) + self.assertEqual(run.call_args.kwargs["input"], '{"rules": []}') + + def test_apply_egress_policy_updates_an_existing_policy(self): + args = self.egress_args(egress_hostnames=["api.openai.com"]) + results = iter( + [ + completed( + self.expected_egress_argv("create"), + returncode=1, + stderr="rpc error: code = AlreadyExists desc = EgressPolicy already exists", + ), + completed( + self.expected_get_egress_argv(), + stdout=json.dumps( + {"metadata": {"uid": "policy-uid-1", "version": "12"}} + ), + ), + completed(self.expected_egress_argv("update")), + ] + ) + + def fake_run(argv, **_): + result = next(results) + self.assertEqual(argv, result.args) + return result + + with patch.object(gate5_setup.subprocess, "run", side_effect=fake_run) as run: + gate5_setup.apply_egress_policy(args) + self.assertEqual( + [call.args[0] for call in run.call_args_list], + [ + self.expected_egress_argv("create"), + self.expected_get_egress_argv(), + self.expected_egress_argv("update"), + ], + ) + self.assertEqual( + json.loads(run.call_args_list[0].kwargs["input"]), + {"rules": [{"hostnames": {"patterns": ["api.openai.com"]}}]}, + ) + self.assertIsNone(run.call_args_list[1].kwargs.get("input")) + self.assertEqual( + json.loads(run.call_args_list[2].kwargs["input"]), + { + "metadata": {"uid": "policy-uid-1", "version": "12"}, + "rules": [{"hostnames": {"patterns": ["api.openai.com"]}}], + }, + ) + + def test_apply_egress_policy_refuses_update_without_valid_preconditions(self): + args = self.egress_args(egress_deny_all=True) + results = iter( + [ + completed( + self.expected_egress_argv("create"), + returncode=1, + stderr="rpc error: code = AlreadyExists desc = EgressPolicy already exists", + ), + completed( + self.expected_get_egress_argv(), + stdout=json.dumps( + {"metadata": {"uid": "policy-uid-1", "version": "0"}} + ), + ), + ] + ) + + def fake_run(argv, **_): + result = next(results) + self.assertEqual(argv, result.args) + return result + + with ( + patch.object(gate5_setup.subprocess, "run", side_effect=fake_run) as run, + self.assertRaisesRegex( + RuntimeError, "valid metadata.uid and metadata.version" + ), + ): + gate5_setup.apply_egress_policy(args) + self.assertEqual(run.call_count, 2) + + def test_apply_egress_policy_does_not_update_after_other_create_failures(self): + args = self.egress_args(egress_deny_all=True) + with ( + patch.object( + gate5_setup.subprocess, + "run", + side_effect=lambda argv, **_: completed( + argv, + returncode=1, + stderr="rpc error: code = FailedPrecondition desc = parent Actor does not exist", + ), + ) as run, + self.assertRaisesRegex(RuntimeError, "create egress-policy failed"), + ): + gate5_setup.apply_egress_policy(args) + self.assertEqual(run.call_count, 1) + + def args(self, state_file: str, **overrides): + values = { + "context": "kind-substrate-preview", + "kubeconfig": FIXTURE_KUBECONFIG, + "atespace": "live-agent-gate", + "worker_pool": "live-agent-gate", + "template_version": "v1", + "image": "localhost:5001/live-agent-gate@sha256:" + "a" * 64, + "actor_name": "claude-gate5", + "state_file": state_file, + } + values.update(overrides) + return SimpleNamespace(**values) + + def cluster(self, *, uid="cluster-uid"): + return { + "api_server_url": "https://127.0.0.1:45147", + "kube_system_namespace_uid": uid, + } + + def test_persists_run_intent_before_actor_or_template_uids_exist(self): + with tempfile.TemporaryDirectory() as temp_dir: + path = str(Path(temp_dir) / "state.json") + args = self.args(path) + state = gate5_setup.prepare_run_state(args, self.cluster()) + stored = json.loads(Path(path).read_text()) + self.assertEqual(stored["run_id"], state["run_id"]) + self.assertEqual(stored["template_name"], "live-agent-gate-v1") + self.assertEqual(stored["worker_pool"], "live-agent-gate") + self.assertEqual(stored["actor_name"], "claude-gate5") + self.assertIsNone(stored["namespace_uid"]) + self.assertIsNone(stored["template_uid"]) + self.assertIsNone(stored["actor_uid"]) + self.assertEqual(stored["cluster_identity"], self.cluster()) + + def test_creates_namespace_and_persists_returned_uid(self): + with tempfile.TemporaryDirectory() as temp_dir: + path = str(Path(temp_dir) / "state.json") + args = self.args(path) + state = gate5_setup.prepare_run_state(args, self.cluster()) + calls = [] + + def runner(command, **kwargs): + calls.append((command, kwargs)) + return completed( + command, stdout='{"metadata":{"uid":"lane-namespace-uid"}}' + ) + + namespace_doc = "apiVersion: v1\nkind: Namespace\nmetadata:\n name: lane\n" + uid = gate5_setup.create_namespace_and_record_uid( + args, state, namespace_doc, runner=runner + ) + + self.assertEqual(uid, "lane-namespace-uid") + self.assertEqual(json.loads(Path(path).read_text())["namespace_uid"], uid) + self.assertEqual(Path(path).stat().st_mode & 0o777, 0o600) + self.assertEqual(calls[0][0][-5:], ["create", "-f", "-", "-o", "json"]) + self.assertEqual(calls[0][1]["input"], namespace_doc) + + def test_resume_refuses_state_without_namespace_uid(self): + with tempfile.TemporaryDirectory() as temp_dir: + path = str(Path(temp_dir) / "state.json") + args = self.args(path) + state = gate5_setup.prepare_run_state(args, self.cluster()) + state.pop("namespace_uid") + gate5_setup.save_state(path, state) + + with self.assertRaisesRegex(RuntimeError, "namespace_uid"): + gate5_setup.prepare_run_state(args, self.cluster()) + + def test_resume_refuses_empty_namespace_uid(self): + with tempfile.TemporaryDirectory() as temp_dir: + path = str(Path(temp_dir) / "state.json") + args = self.args(path) + state = gate5_setup.prepare_run_state(args, self.cluster()) + gate5_setup.save_state(path, state) + + with self.assertRaisesRegex(RuntimeError, "namespace_uid"): + gate5_setup.prepare_run_state(args, self.cluster()) + + def test_namespace_uid_rerun_refuses_to_adopt_replacement(self): + with tempfile.TemporaryDirectory() as temp_dir: + path = str(Path(temp_dir) / "state.json") + args = self.args(path) + state = gate5_setup.prepare_run_state(args, self.cluster()) + state["namespace_uid"] = "original-namespace-uid" + gate5_setup.save_state(path, state) + + def runner(argv, **_): + return completed( + argv, stdout='{"metadata":{"uid":"replacement-namespace-uid"}}' + ) + + with self.assertRaisesRegex(RuntimeError, "refusing to adopt"): + gate5_setup.verify_namespace_uid(args, state, runner=runner) + + self.assertEqual( + json.loads(Path(path).read_text())["namespace_uid"], + "original-namespace-uid", + ) + + def test_rerun_refuses_changed_cluster_identity(self): + with tempfile.TemporaryDirectory() as temp_dir: + path = str(Path(temp_dir) / "state.json") + args = self.args(path) + gate5_setup.prepare_run_state(args, self.cluster()) + with self.assertRaisesRegex(RuntimeError, "requested identity differs"): + gate5_setup.prepare_run_state(args, self.cluster(uid="other-cluster")) + + def test_rerun_refuses_changed_template_request(self): + with tempfile.TemporaryDirectory() as temp_dir: + path = str(Path(temp_dir) / "state.json") + gate5_setup.prepare_run_state(self.args(path), self.cluster()) + with self.assertRaisesRegex(RuntimeError, "template_name"): + gate5_setup.prepare_run_state( + self.args(path, template_version="v2"), self.cluster() + ) + + def test_rerun_refuses_changed_worker_pool(self): + with tempfile.TemporaryDirectory() as temp_dir: + path = str(Path(temp_dir) / "state.json") + gate5_setup.prepare_run_state(self.args(path), self.cluster()) + with self.assertRaisesRegex(RuntimeError, "worker_pool"): + gate5_setup.prepare_run_state( + self.args(path, worker_pool="isolated-pool"), self.cluster() + ) + + +class SetupControl: + def __init__(self, actor: ActorRecord | None): + self.actor = actor + self.created = [] + self.resumed = 0 + self.events = [] + + async def get_actor(self, _atespace, _name): + self.events.append("get_actor") + return self.actor + + async def create_actor(self, atespace, name, *, template): + self.events.append("create_actor") + self.created.append((atespace, name, template)) + self.actor = ActorRecord( + atespace=atespace, + name=name, + uid="actor-new", + state=ActorState.SUSPENDED, + external_snapshot_uri=None, + current_actor_template_uid="template-new", + raw={}, + ) + return self.actor + + async def resume_actor(self, _atespace, _name): + self.events.append("resume_actor") + self.resumed += 1 + self.actor = replace(self.actor, state=ActorState.RUNNING) + return self.actor + + +class Gate5ActorIdentityTests(unittest.TestCase): + def state(self, path, *, actor_uid="actor-1"): + state = { + "run_id": "run-1", + "actor_uid": actor_uid, + "actor_name": "claude-gate5", + "template_name": "live-agent-gate-v2", + "template_uid": "template-new", + } + gate5_setup.save_state(path, state) + return state + + def test_state_file_is_private_for_future_shim_token(self): + with tempfile.TemporaryDirectory() as temp_dir: + path = str(Path(temp_dir) / "state.json") + gate5_setup.save_state(path, {"shim_token": "fixture-secret"}) + self.assertEqual(Path(path).stat().st_mode & 0o777, 0o600) + + def test_shim_token_is_persisted_before_body_only_install_and_verified(self): + with tempfile.TemporaryDirectory() as temp_dir: + path = str(Path(temp_dir) / "state.json") + state = {"run_id": "run-token"} + args = SimpleNamespace( + state_file=path, + atespace="live-agent-gate", + actor_name="claude-gate5", + ) + token = "fixture-shim-" + "v" * 40 + statuses = [201, 401, 401, 404, 409, 200] + calls = [] + + def requester(**kwargs): + calls.append(kwargs) + return statuses.pop(0) + + output = StringIO() + with ( + patch.object(gate5_setup.secrets, "token_urlsafe", return_value=token), + redirect_stdout(output), + ): + gate5_setup.ensure_shim_token( + args=args, state=state, port=18091, requester=requester + ) + + self.assertEqual(json.loads(Path(path).read_text())["shim_token"], token) + self.assertEqual(Path(path).stat().st_mode & 0o777, 0o600) + self.assertEqual(calls[0]["method"], "POST") + self.assertEqual(calls[0]["path"], "/token") + self.assertEqual(calls[0]["body"], {"token": token}) + self.assertNotIn("token", calls[0]) + self.assertEqual( + calls[1]["path"], "/turn/00000000-0000-4000-8000-000000000000" + ) + self.assertEqual(calls[1]["token"], None) + self.assertEqual(calls[2]["token"], f"{token}x") + self.assertEqual(calls[3]["token"], token) + self.assertNotIn(token, output.getvalue()) + + def test_shim_token_rerun_verifies_existing_token_without_rotating_it(self): + with tempfile.TemporaryDirectory() as temp_dir: + path = str(Path(temp_dir) / "state.json") + token = "existing-shim-" + "v" * 40 + state = {"run_id": "run-token", "shim_token": token} + gate5_setup.save_state(path, state) + args = SimpleNamespace( + state_file=path, + atespace="live-agent-gate", + actor_name="claude-gate5", + ) + statuses = [409, 404, 401, 401, 404, 409, 200] + calls = [] + + def requester(**kwargs): + calls.append(kwargs) + return statuses.pop(0) + + with patch.object(gate5_setup.secrets, "token_urlsafe") as generate: + gate5_setup.ensure_shim_token( + args=args, state=state, port=18091, requester=requester + ) + + generate.assert_not_called() + self.assertEqual(calls[1]["token"], token) + self.assertEqual(json.loads(Path(path).read_text())["shim_token"], token) + + def actor( + self, *, uid="actor-1", template_uid="template-new", state=ActorState.RUNNING + ): + return ActorRecord( + atespace="live-agent-gate", + name="claude-gate5", + uid=uid, + state=state, + external_snapshot_uri=None, + current_actor_template_uid=template_uid, + raw={}, + ) + + def args(self, path): + return SimpleNamespace( + state_file=path, + atespace="live-agent-gate", + worker_pool="live-agent-gate", + actor_name="claude-gate5", + actor_timeout=5, + worker_timeout=5, + ) + + def test_exact_old_template_collision_with_new_template_is_refused(self): + with tempfile.TemporaryDirectory() as temp_dir: + path = str(Path(temp_dir) / "state.json") + state = { + "run_id": "run-2", + "actor_uid": None, + "template_uid": "template-new", + } + control = SetupControl(self.actor(template_uid="template-old")) + with self.assertRaisesRegex(IdentityConflict, "no actor uid"): + asyncio_run( + gate5_setup.ensure_actor( + control, + self.args(path), + "live-agent-gate-v2", + "template-new", + state, + ) + ) + + def test_owned_actor_with_template_mismatch_is_refused(self): + with tempfile.TemporaryDirectory() as temp_dir: + path = str(Path(temp_dir) / "state.json") + state = self.state(path) + control = SetupControl(self.actor(template_uid="template-old")) + with self.assertRaisesRegex(IdentityConflict, "references template uid"): + asyncio_run( + gate5_setup.ensure_actor( + control, + self.args(path), + "live-agent-gate-v2", + "template-new", + state, + ) + ) + + def test_crashed_and_deleting_actors_are_refused_before_resume(self): + for actor_state in (ActorState.CRASHED, ActorState.DELETING): + with ( + self.subTest(actor_state=actor_state), + tempfile.TemporaryDirectory() as temp_dir, + ): + path = str(Path(temp_dir) / "state.json") + state = self.state(path) + control = SetupControl(self.actor(state=actor_state)) + with self.assertRaises(ActorFailedToStart): + asyncio_run( + gate5_setup.ensure_actor( + control, + self.args(path), + "live-agent-gate-v2", + "template-new", + state, + ) + ) + self.assertEqual(control.resumed, 0) + + def test_new_actor_is_resumed_and_uid_is_saved(self): + with tempfile.TemporaryDirectory() as temp_dir: + path = str(Path(temp_dir) / "state.json") + state = { + "run_id": "run-3", + "actor_uid": None, + "template_uid": "template-new", + } + control = SetupControl(None) + + async def wait_for_worker(*_args, **_kwargs): + control.events.append("wait_for_eligible_worker") + + with patch.object(gate5_setup, "wait_for_eligible_worker", wait_for_worker): + asyncio_run( + gate5_setup.wait_for_worker_if_actor_is_absent( + control, self.args(path), state + ) + ) + uid = asyncio_run( + gate5_setup.ensure_actor( + control, + self.args(path), + "live-agent-gate-v2", + "template-new", + state, + ) + ) + self.assertEqual(uid, "actor-new") + self.assertEqual(control.resumed, 1) + self.assertLess( + control.events.index("get_actor"), + control.events.index("wait_for_eligible_worker"), + ) + self.assertLess( + control.events.index("wait_for_eligible_worker"), + control.events.index("create_actor"), + ) + self.assertEqual( + json.loads(Path(path).read_text())["actor_uid"], "actor-new" + ) + + def test_worker_wait_uses_the_selected_atespace_namespace(self): + with tempfile.TemporaryDirectory() as temp_dir: + path = str(Path(temp_dir) / "state.json") + args = self.args(path) + args.atespace = "native-codex" + args.worker_pool = "native-codex" + control = SetupControl(None) + observed = {} + + async def wait_for_worker( + _control, namespace, worker_selector, *_args, **_kwargs + ): + observed["namespace"] = namespace + observed["worker_selector"] = worker_selector + + with patch.object(gate5_setup, "wait_for_eligible_worker", wait_for_worker): + asyncio_run( + gate5_setup.wait_for_worker_if_actor_is_absent( + control, + args, + { + "run_id": "run-codex", + "actor_uid": None, + "template_uid": "template-codex", + }, + ) + ) + + self.assertEqual(observed["namespace"], "native-codex") + self.assertEqual(observed["worker_selector"], "workload=native-codex") + + def test_owned_rerun_skips_worker_wait_when_its_actor_occupies_only_worker(self): + with tempfile.TemporaryDirectory() as temp_dir: + path = str(Path(temp_dir) / "state.json") + state = self.state(path) + control = SetupControl(self.actor()) + + async def unexpected_worker_wait(*_args, **_kwargs): + self.fail("owned rerun must not wait for a spare worker") + + with patch.object( + gate5_setup, "wait_for_eligible_worker", unexpected_worker_wait + ): + asyncio_run( + gate5_setup.wait_for_worker_if_actor_is_absent( + control, self.args(path), state + ) + ) + uid = asyncio_run( + gate5_setup.ensure_actor( + control, + self.args(path), + "live-agent-gate-v2", + "template-new", + state, + ) + ) + + self.assertEqual(uid, "actor-1") + self.assertEqual(control.created, []) + self.assertEqual(control.resumed, 0) + + def test_unowned_actor_is_refused_before_worker_wait(self): + with tempfile.TemporaryDirectory() as temp_dir: + path = str(Path(temp_dir) / "state.json") + state = { + "run_id": "run-4", + "actor_uid": None, + "actor_name": "claude-gate5", + "template_name": "live-agent-gate-v2", + "template_uid": "template-new", + } + control = SetupControl(self.actor(template_uid="template-old")) + + async def unexpected_worker_wait(*_args, **_kwargs): + self.fail("unowned actor must be refused before worker discovery") + + with patch.object( + gate5_setup, "wait_for_eligible_worker", unexpected_worker_wait + ): + with self.assertRaisesRegex(IdentityConflict, "no actor uid"): + asyncio_run( + gate5_setup.wait_for_worker_if_actor_is_absent( + control, self.args(path), state + ) + ) + + self.assertEqual(control.events, ["get_actor"]) + + +class LaneAProviderCleanupTests(unittest.TestCase): + def make_cleanup_environment( + self, + temp_dir: str, + *, + unowned_secret: bool = False, + change_secret_uid: bool = False, + ) -> tuple[dict[str, str], Path]: + root = Path(temp_dir) + state_root = root / "state" + state_root.mkdir() + fake_bin = root / "bin" + fake_bin.mkdir() + substrate_bin = root / "substrate" / "bin" + substrate_bin.mkdir(parents=True) + + state_file = state_root / "claude-gate5-state.json" + state_file.write_text( + json.dumps( + { + "context": "kind-substrate-preview", + "atespace": "lane-a-claude-20260924", + "worker_pool": "lane-a-claude-20260924", + "template_name": "live-agent-gate-lane-a-claude-20260924", + "actor_name": "claude-live-proof", + "namespace_uid": "lane-namespace-uid", + } + ) + ) + + kubectl = fake_bin / "kubectl" + kubectl.write_text( + """#!/bin/bash +set -eu +printf '%s\\n' "$*" >> "${FAKE_KUBECTL_LOG}" +args=" $* " +if [[ ${args} == *" get namespace lane-a-claude-20260924 "* ]]; then + printf '%s\\n' '{"metadata":{"uid":"lane-namespace-uid"}}' + exit 0 +fi +if [[ ${args} == *" get workerpool lane-a-claude-20260924 "* ]]; then + printf '%s\\n' 'Error from server (NotFound): workerpool not found' >&2 + exit 1 +fi +if [[ ${args} == *" get secret claude-oauth "* ]]; then + if [[ ${FAKE_UNOWNED_SECRET:-0} == 1 ]]; then + printf '%s\\n' 'secret/claude-oauth' + exit 0 + fi + count_file=${FAKE_KUBECTL_LOG}.secret-count + count=0 + if [[ -f ${count_file} ]]; then read -r count < "${count_file}"; fi + count=$((count + 1)) + printf '%s\\n' "${count}" > "${count_file}" + uid=secret-uid + if [[ ${FAKE_CHANGE_SECRET_UID:-0} == 1 && ${count} -gt 1 ]]; then + uid=replacement-secret-uid + fi + printf '{"metadata":{"uid":"%s","labels":{"proof.mainloop.dev/lane":"lane-a-live-proof"}}}\\n' "${uid}" + exit 0 +fi +if [[ ${args} == *" get service credprovider "* ]]; then + printf '%s\\n' '{"metadata":{"uid":"service-uid","labels":{"proof.mainloop.dev/lane":"lane-a-live-proof"}}}' + exit 0 +fi +if [[ ${args} == *" get deployment round3-claude-provider "* ]]; then + printf '%s\\n' '{"metadata":{"uid":"deployment-uid","labels":{"proof.mainloop.dev/lane":"lane-a-live-proof"}}}' + exit 0 +fi +if [[ ${args} == *" get serviceaccount round3-claude-provider "* ]]; then + printf '%s\\n' '{"metadata":{"uid":"serviceaccount-uid","labels":{"proof.mainloop.dev/lane":"lane-a-live-proof"}}}' + exit 0 +fi +if [[ ${args} == *" get networkpolicy round3-claude-provider "* ]]; then + printf '%s\\n' '{"metadata":{"uid":"networkpolicy-uid","labels":{"proof.mainloop.dev/lane":"lane-a-live-proof"}}}' + exit 0 +fi +if [[ ${args} == *" delete --raw=/api/v1/namespaces/mainloop-control/"* || ${args} == *" delete --raw=/apis/"* ]]; then + printf '%s\\n' "$*" >> "${FAKE_PROVIDER_DELETE_LOG}" + cat >> "${FAKE_PROVIDER_DELETE_PAYLOADS}" + printf '%s\\n' '---' >> "${FAKE_PROVIDER_DELETE_PAYLOADS}" + exit 0 +fi +if [[ ${args} == *" delete --raw="* ]]; then + cat >/dev/null + exit 0 +fi +exit 0 +""" + ) + kubectl.chmod(0o755) + + ate = substrate_bin / "kubectl-ate" + ate.write_text( + """#!/bin/bash +set -eu +args=" $* " +if [[ ${args} == *" get actor "* || ${args} == *" get actor-template "* ]]; then + printf '%s\\n' 'NotFound' >&2 + exit 1 +fi +exit 0 +""" + ) + ate.chmod(0o755) + + env = os.environ.copy() + env.update( + { + "PATH": f"{fake_bin}{os.pathsep}{env['PATH']}", + "LIVE_PROOF_STATE_DIR": str(state_root), + "SUBSTRATE_SRC": str(root / "substrate"), + "FAKE_KUBECTL_LOG": str(root / "kubectl.log"), + "FAKE_PROVIDER_DELETE_LOG": str(root / "provider-deletes.log"), + "FAKE_PROVIDER_DELETE_PAYLOADS": str( + root / "provider-delete-payloads.jsonl" + ), + "FAKE_UNOWNED_SECRET": "1" if unowned_secret else "0", + "FAKE_CHANGE_SECRET_UID": "1" if change_secret_uid else "0", + } + ) + return env, state_file + + def run_cleanup(self, env: dict[str, str]) -> subprocess.CompletedProcess[str]: + script = ( + Path(__file__).resolve().parents[3] + / "spikes/substrate-workspace-adapter/live/cleanup-lane-a.sh" + ) + return subprocess.run( # nosec B603 - fixed script path; kubectl resolves to a temp fake. + ["/bin/bash", str(script), "claude"], + env=env, + capture_output=True, + check=False, + text=True, + ) + + def test_unowned_provider_preflight_then_failure_fallback_skips_deletes(self): + with tempfile.TemporaryDirectory() as temp_dir: + env, _ = self.make_cleanup_environment(temp_dir, unowned_secret=True) + live_dir = ( + Path(__file__).resolve().parents[3] + / "spikes/substrate-workspace-adapter/live" + ) + env["LIVE_DIR"] = str(live_dir) + proof_script = (live_dir / "claude_turn_proof.sh").read_text() + cleanup_start = proof_script.index("cleanup_provider() {") + cleanup_end = proof_script.index("\nfinish() {", cleanup_start) + cleanup_function = proof_script[cleanup_start:cleanup_end] + finish_start = proof_script.index("finish() {") + finish_end = proof_script.index("\ntrap finish EXIT INT TERM", finish_start) + finish_function = proof_script[finish_start:finish_end] + harness = "\n".join( + ( + "set -euo pipefail", + 'source "${LIVE_DIR}/common.sh"', + "SCRIPT_DIR=${LIVE_DIR}", + "LANE_STARTED=1", + "COMPLETE=0", + "PROVIDER_READY=0", + "declare -A PROVIDER_UIDS=()", + "STATE_FILE=${LIVE_PROOF_STATE_DIR}/claude-gate5-state.json", + "stop_router() { :; }", + cleanup_function, + finish_function, + "trap finish EXIT INT TERM", + "preflight_provider_resources_absent", + ) + ) + preflight_and_trap = ( + subprocess.run( # nosec B603 - fixed trap/preflight with fake kubectl. + ["/bin/bash", "-c", harness], + env=env, + capture_output=True, + check=False, + text=True, + ) + ) + self.assertNotEqual(preflight_and_trap.returncode, 0) + self.assertIn( + "secret/claude-oauth already exists", preflight_and_trap.stderr + ) + self.assertIn("CLEANUP=PASS", preflight_and_trap.stdout) + calls = Path(env["FAKE_KUBECTL_LOG"]).read_text().splitlines() + provider_mutations = [ + call + for call in calls + if " delete " in f" {call} " + and any( + token in call + for token in ( + "claude-oauth", + "credprovider", + "round3-claude-provider", + ) + ) + ] + self.assertEqual(provider_mutations, []) + + def test_provider_uid_change_aborts_cleanup_before_any_provider_delete(self): + with tempfile.TemporaryDirectory() as temp_dir: + env, _ = self.make_cleanup_environment(temp_dir, change_secret_uid=True) + cleanup = self.run_cleanup(env) + + self.assertNotEqual(cleanup.returncode, 0) + self.assertIn( + "provider cleanup skipped secret/claude-oauth", cleanup.stderr + ) + self.assertNotIn("CLEANUP=PASS", cleanup.stdout) + calls = Path(env["FAKE_KUBECTL_LOG"]).read_text().splitlines() + provider_deletes = [ + call + for call in calls + if " delete --raw=/api/v1/namespaces/mainloop-control/" in call + or " delete --raw=/apis/" in call + ] + self.assertEqual(provider_deletes, []) + delete_options = Path(env["FAKE_KUBECTL_LOG"] + ".secret-count") + self.assertEqual(delete_options.read_text(), "2\n") + + def test_owned_provider_deletes_use_the_verified_uid_precondition(self): + with tempfile.TemporaryDirectory() as temp_dir: + env, _ = self.make_cleanup_environment(temp_dir) + cleanup = self.run_cleanup(env) + + self.assertEqual(cleanup.returncode, 0, cleanup.stderr) + provider_deletes = ( + Path(env["FAKE_PROVIDER_DELETE_LOG"]).read_text().splitlines() + ) + self.assertEqual(len(provider_deletes), 5) + payloads = [ + json.loads(payload) + for payload in Path(env["FAKE_PROVIDER_DELETE_PAYLOADS"]) + .read_text() + .split("---") + if payload.strip() + ] + self.assertEqual(len(payloads), 5) + self.assertEqual( + {payload["preconditions"]["uid"] for payload in payloads}, + { + "secret-uid", + "service-uid", + "deployment-uid", + "serviceaccount-uid", + "networkpolicy-uid", + }, + ) + + +def asyncio_run(coro): + import asyncio + + return asyncio.run(coro) + + +if __name__ == "__main__": + unittest.main() diff --git a/backend/tests/runtime/test_herdr.py b/backend/tests/runtime/test_herdr.py deleted file mode 100644 index 6e53787..0000000 --- a/backend/tests/runtime/test_herdr.py +++ /dev/null @@ -1,69 +0,0 @@ -"""Herdr adapter over a fake pod-exec transport: no cluster, no agents, no credentials.""" - -import asyncio -import unittest - -from mainloop.runtime.herdr import ExecResult, HerdrWorkspace, TransportError - - -class FakeWorkspace(HerdrWorkspace): - def __init__(self, results): - super().__init__(namespace="ns", pod="pod") - self.results = list(results) - self.calls: list[list[str]] = [] - - async def _exec(self, command, timeout=45): - self.calls.append(command) - result = self.results.pop(0) - if isinstance(result, Exception): - raise result - return result - - -def run(coro): - return asyncio.run(coro) - - -class HerdrAdapterTests(unittest.TestCase): - def test_send_is_one_exec_and_transport_error_is_not_retried(self): - ws = FakeWorkspace([TransportError("boom")]) - with self.assertRaises(TransportError): - run(ws.send("agent", "hi")) - self.assertEqual( - ws.calls, [["agentctl", "send", "agent", "hi"]] - ) # exactly one attempt - - def test_prompt_text_is_argv_not_shell(self): - ws = FakeWorkspace([ExecResult(0, "sent\n", "")]) - run(ws.send("agent", "a; rm -rf / $(x) 'q'")) - self.assertEqual(ws.calls[0][-1], "a; rm -rf / $(x) 'q'") - - def test_journal_parses_header_and_numbered_lines(self): - out = '#file\t/w/.claude/projects/p/s.jsonl\t3\n2\t{"a":1}\n3\t{"b":2}\n' - ws = FakeWorkspace([ExecResult(0, out, "")]) - sl = run(ws.journal("agent", "sid", 1)) - self.assertEqual( - (sl.file, sl.total_lines, sl.lines), - ("/w/.claude/projects/p/s.jsonl", 3, [(2, '{"a":1}'), (3, '{"b":2}')]), - ) - self.assertEqual(ws.calls[0], ["agentctl", "journal", "agent", "sid", "1"]) - - def test_missing_journal(self): - ws = FakeWorkspace([ExecResult(0, "#nofile\n", "")]) - self.assertIsNone(run(ws.journal("agent", "sid", 0)).file) - - def test_start_uses_resume_or_new_id(self): - ident = '{"pane_id":"w1:p1","terminal_id":"t"}\n' - ws = FakeWorkspace([ExecResult(0, ident, ""), ExecResult(0, ident, "")]) - run(ws.start("claude", "n", native_id="sid", resume=False)) - run(ws.start("claude", "n", native_id="sid", resume=True)) - self.assertEqual(ws.calls[0][-2:], ["--new-id", "sid"]) - self.assertEqual(ws.calls[1][-2:], ["--resume", "sid"]) - - def test_status_none_when_agent_not_live(self): - ws = FakeWorkspace([ExecResult(1, "", "no agent")]) - self.assertIsNone(run(ws.agent_status("n"))) - - -if __name__ == "__main__": - unittest.main() diff --git a/backend/tests/runtime/test_journal.py b/backend/tests/runtime/test_journal.py index 8616724..27e000d 100644 --- a/backend/tests/runtime/test_journal.py +++ b/backend/tests/runtime/test_journal.py @@ -99,7 +99,6 @@ def test_claude_turn_prompt_reply_completion_model(self): numbered(CLAUDE_TURN), file_ref="s.jsonl", native_id=CLAUDE_ID, - agent="a", ) kinds = [e.kind for e in events] self.assertEqual( @@ -123,7 +122,6 @@ def test_open_turn_is_not_persisted_and_cursor_stays_before_it(self): numbered(CLAUDE_TURN[:4]), file_ref="s.jsonl", native_id=CLAUDE_ID, - agent="a", ) turns, safe = completed_turns(events) self.assertEqual(turns, []) @@ -135,7 +133,6 @@ def test_codex_turn(self): numbered(CODEX_TURN), file_ref="r.jsonl", native_id=CODEX_ID, - agent="a", ) self.assertEqual(events[3].kind, "prompt") self.assertEqual(events[5].normalized_type, "completed") @@ -151,17 +148,13 @@ def test_malformed_and_unknown_lines_are_ignored(self): (3, "[]"), ] self.assertEqual( - len( - parse_journal( - "claude", lines, file_ref="s", native_id=CLAUDE_ID, agent="a" - ) - ), + len(parse_journal("claude", lines, file_ref="s", native_id=CLAUDE_ID)), 1, ) def test_unknown_kind_is_rejected(self): with self.assertRaises(ValueError): - parse_journal("pi", [], file_ref="s", native_id="x", agent="a") + parse_journal("pi", [], file_ref="s", native_id="x") if __name__ == "__main__": diff --git a/backend/tests/runtime/test_native_session_empty_binding_update.py b/backend/tests/runtime/test_native_session_empty_binding_update.py new file mode 100644 index 0000000..b9c27fb --- /dev/null +++ b/backend/tests/runtime/test_native_session_empty_binding_update.py @@ -0,0 +1,118 @@ +"""Regression coverage for an unchanged native child binding on consecutive turns.""" + +from __future__ import annotations + +import asyncio +import unittest +from unittest.mock import AsyncMock, patch + +from mainloop.runtime import native_sessions +from mainloop.runtime.standing import content_hash + + +class _Connection: + def __init__(self): + self.executions: list[tuple[str, tuple]] = [] + + async def execute(self, query: str, *args): + self.executions.append((query, args)) + + +class _ConnectionContext: + def __init__(self, connection: _Connection): + self.connection = connection + + async def __aenter__(self): + return self.connection + + async def __aexit__(self, *_args): + return None + + +class _RunningWorkspace: + def __init__(self): + self.sent: list[str] = [] + + async def require_ready(self): + return None + + async def agent_status(self, _name: str): + return {"status": "running"} + + def set_resume_history(self, _resume: bool): + return None + + def set_startup_options(self, _options: dict): + return None + + async def prepare_credentials(self): + return None + + async def send(self, _name: str, text: str): + self.sent.append(text) + + +class NativeSessionEmptyBindingUpdateTests(unittest.TestCase): + def test_two_turns_with_unchanged_child_context_use_valid_update_sql(self): + async def exercise(): + standing = "Stable child context" + binding = { + "session_id": "child-session-fixture", + "kind": "claude", + "role": "child", + "agent_name": "ml-claude-child-fixture", + "native_session_id": None, + "journal_ref": "journal-fixture", + "generation": 1, + "standing_hash": content_hash(standing), + "approval_policy": "bypass-permissions", + } + connection = _Connection() + workspace = _RunningWorkspace() + + async def render_child_context(_binding): + return standing + + with ( + patch.object( + native_sessions, + "get_binding", + new=AsyncMock(side_effect=lambda _sid: dict(binding)), + ), + patch.object(native_sessions, "workspace_for", return_value=workspace), + patch.object( + native_sessions, + "token_for", + return_value="ml_" + "a" * 64, + ), + patch( + "mainloop.runtime.delegation.render_for_binding", + new=AsyncMock(side_effect=render_child_context), + ), + patch.object( + native_sessions.db, + "connection", + side_effect=lambda: _ConnectionContext(connection), + ), + patch.object(native_sessions, "_set_delivery", new=AsyncMock()), + patch.object(native_sessions, "sync", new=AsyncMock()), + ): + await native_sessions._deliver( + binding["session_id"], "message-one", "first turn" + ) + await native_sessions._deliver( + binding["session_id"], "message-two", "second turn" + ) + + self.assertEqual(workspace.sent, ["first turn", "second turn"]) + self.assertEqual(len(connection.executions), 2) + for query, args in connection.executions: + self.assertIn("SET updated_at=NOW()", query) + self.assertNotIn("SET ,", query) + self.assertEqual(args, (binding["session_id"],)) + + asyncio.run(exercise()) + + +if __name__ == "__main__": + unittest.main() diff --git a/backend/tests/runtime/test_native_session_reconcile.py b/backend/tests/runtime/test_native_session_reconcile.py new file mode 100644 index 0000000..5215bd2 --- /dev/null +++ b/backend/tests/runtime/test_native_session_reconcile.py @@ -0,0 +1,171 @@ +"""Adapter-to-reconciler coverage for bounded native journal paging.""" + +from __future__ import annotations + +import asyncio +import json +import unittest +from types import SimpleNamespace +from unittest.mock import AsyncMock, patch + +from mainloop.runtime import native_sessions +from mainloop.runtime.substrate_workspace import JournalSlice + +from models import SessionStatus + + +def journal_fixture() -> list[tuple[int, str]]: + lines = [(line, '{"type":"progress"}') for line in range(1, 201)] + lines.extend( + [ + ( + 201, + json.dumps( + { + "type": "user", + "uuid": "fixture-prompt", + "timestamp": "2026-09-24T00:00:00Z", + "message": {"role": "user", "content": "fixture prompt"}, + } + ), + ), + ( + 202, + json.dumps( + { + "type": "assistant", + "uuid": "fixture-reply", + "timestamp": "2026-09-24T00:00:01Z", + "message": { + "role": "assistant", + "content": [ + {"type": "text", "text": "fixture reply past line 200"} + ], + }, + } + ), + ), + ( + 203, + json.dumps( + { + "type": "system", + "subtype": "turn_duration", + "timestamp": "2026-09-24T00:00:02Z", + } + ), + ), + ] + ) + return lines + + +class _Connection: + def __init__(self): + self.executions: list[tuple[str, tuple]] = [] + + async def fetch(self, _query: str, *_args): + return [] + + async def execute(self, query: str, *args): + self.executions.append((query, args)) + return "INSERT 0 1" + + +class _ConnectionContext: + def __init__(self, connection: _Connection): + self.connection = connection + + async def __aenter__(self): + return self.connection + + async def __aexit__(self, *_args): + return None + + +class _PagedWorkspace: + def __init__(self): + self.lines = journal_fixture() + self.calls: list[int] = [] + + async def journal(self, _name: str, _native_id: str, from_line: int): + self.calls.append(from_line) + return JournalSlice( + "/fake/fixture-session.jsonl", + len(self.lines), + [line for line in self.lines if line[0] > from_line][:200], + ) + + async def credential_rejected(self): + return False + + +class NativeSessionReconcileTests(unittest.TestCase): + def test_completion_after_line_200_is_mirrored_once(self): + async def exercise(): + binding = { + "session_id": "fixture-session", + "kind": "claude", + "agent_name": "fixture-agent", + "native_session_id": "fixture-native-id", + "journal_cursor": 0, + "journal_ref": None, + "turns_in_lineage": 0, + "continuations": 0, + "context_tokens": None, + "baseline_tokens": None, + "role": "agent", + "reported_at": None, + "generation": 1, + } + conn = _Connection() + ws = _PagedWorkspace() + session = SimpleNamespace( + conversation_id="fixture-conversation", + status=SessionStatus.WAITING_ON_USER, + ) + + async def update_binding(_session_id: str, **fields): + binding.update(fields) + + with ( + patch.object( + native_sessions, + "get_binding", + new=AsyncMock(side_effect=lambda _sid: dict(binding)), + ), + patch.object(native_sessions, "workspace_for", return_value=ws), + patch.object(native_sessions, "_update_binding", new=update_binding), + patch.object( + native_sessions.db, + "get_session", + new=AsyncMock(return_value=session), + ), + patch.object( + native_sessions.db, + "connection", + side_effect=lambda: _ConnectionContext(conn), + ), + patch.object( + native_sessions, "_open_count", new=AsyncMock(return_value=0) + ), + patch.object(native_sessions.db, "update_session", new=AsyncMock()), + ): + await native_sessions._sync_locked("fixture-session") + await native_sessions._sync_locked("fixture-session") + + mirrored = [ + args + for query, args in conn.executions + if "INSERT INTO messages" in query and "'assistant'" in query + ] + self.assertEqual(len(mirrored), 1) + self.assertEqual(mirrored[0][2], "fixture reply past line 200") + self.assertEqual(binding["journal_cursor"], 203) + self.assertEqual(ws.calls, [0, 200, 203]) + + asyncio.run(exercise()) + + +if __name__ == "__main__": + unittest.main() diff --git a/backend/tests/runtime/test_preview_proxy.py b/backend/tests/runtime/test_preview_proxy.py new file mode 100644 index 0000000..0a98355 --- /dev/null +++ b/backend/tests/runtime/test_preview_proxy.py @@ -0,0 +1,652 @@ +"""Fake-backed host, port policy, and proxy framing tests.""" + +from __future__ import annotations + +import asyncio +import http.client +import io +import os +import socket +import unittest +from unittest.mock import AsyncMock, patch + +from mainloop.config import settings +from mainloop.runtime import preview_proxy +from starlette.requests import Request +from starlette.websockets import WebSocket + +_PREVIEW_HOST = "5173--workspace-1.preview.localhost:8001" + + +def make_request(method: str, headers: dict[str, str], body: bytes = b"") -> Request: + raw_headers = [ + (key.lower().encode(), value.encode()) for key, value in headers.items() + ] + + async def receive(): + return {"type": "http.request", "body": body, "more_body": False} + + return Request( + { + "type": "http", + "http_version": "1.1", + "method": method, + "scheme": "http", + "path": "/", + "raw_path": b"/", + "query_string": b"", + "root_path": "", + "headers": raw_headers, + "server": ("preview.localhost", 8001), + "client": ("127.0.0.1", 12345), + }, + receive, + ) + + +def make_websocket(headers: dict[str, str]) -> tuple[WebSocket, list[dict]]: + raw_headers = [ + (key.lower().encode(), value.encode()) for key, value in headers.items() + ] + sent: list[dict] = [] + + async def receive(): + return {"type": "websocket.disconnect", "code": 1000} + + async def send(message): + sent.append(message) + + return ( + WebSocket( + { + "type": "websocket", + "asgi": {"version": "3.0"}, + "http_version": "1.1", + "scheme": "ws", + "path": "/", + "raw_path": b"/", + "query_string": b"", + "root_path": "", + "headers": raw_headers, + "server": ("preview.localhost", 8001), + "client": ("127.0.0.1", 12345), + "subprotocols": [], + }, + receive, + send, + ), + sent, + ) + + +async def _run_sync_in_test(function, *args, **kwargs): + """Keep fake router operations deterministic without a test executor thread.""" + await asyncio.sleep(0) + return function(*args, **kwargs) + + +class PreviewProxyTests(unittest.TestCase): + def test_preview_host_requires_matching_domain_port_and_safe_labels(self): + base = "http://preview.localhost:8001" + parsed = preview_proxy.parse_preview_host( + "5173--workspace-1.preview.localhost:8001", base + ) + self.assertEqual(parsed, preview_proxy.PreviewHost(5173, "workspace-1")) + for invalid in ( + "5173--workspace-1.example.test:8001", + "5173--workspace-1.preview.localhost:3000", + "65536--workspace-1.preview.localhost:8001", + "5173--bad-.preview.localhost:8001", + "5173--workspace-1.preview.localhost/path:8001", + ): + with self.subTest(invalid=invalid): + self.assertIsNone(preview_proxy.parse_preview_host(invalid, base)) + + def test_manifest_port_formats_are_validated_and_deduplicated(self): + ports = preview_proxy.declared_ports( + { + "forwardPorts": [5173, "invalid", 70000, True], + "dev": { + "ports": [ + {"name": "web", "number": 5173}, + {"name": "api", "port": 8000}, + {"port": 0}, + ] + }, + } + ) + self.assertEqual(ports, {5173: "web", 8000: "api"}) + + def test_preview_url_uses_configured_wildcard_origin(self): + with patch.object( + settings, "substrate_preview_base_url", "http://preview.localhost:8001" + ): + self.assertEqual( + preview_proxy.preview_url("workspace-1", 5173), + "http://5173--workspace-1.preview.localhost:8001", + ) + + def test_dynamic_shim_secret_uses_configured_prefix_convention(self): + with patch.object(settings, "substrate_shim_secret_prefix", "test-shim"): + self.assertEqual( + preview_proxy._secret_name_for_workspace("fixture-space", "actor-1"), + settings.shim_token_secret_name("fixture-space", "actor-1"), + ) + + def test_preview_touch_calls_lifecycle_service(self): + async def exercise(): + with patch.object( + preview_proxy.workspace_adapter, + "touch_workspace", + new=AsyncMock(), + ) as touch: + await preview_proxy._touch_preview("workspace-1") + touch.assert_awaited_once_with("workspace-1", reason="preview") + + asyncio.run(exercise()) + + def test_http_preview_requires_trusted_identity_and_ignores_forged_user_id(self): + async def exercise(): + with patch.object( + preview_proxy, "_resolve_target", new_callable=AsyncMock + ) as resolve: + anonymous = await preview_proxy._preview_http( + make_request("GET", {"host": _PREVIEW_HOST}), + preview_proxy.PreviewHost(5173, "workspace-1"), + ) + forged = await preview_proxy._preview_http( + make_request( + "GET", + {"host": _PREVIEW_HOST, "x-user-id": "local-dev-user"}, + ), + preview_proxy.PreviewHost(5173, "workspace-1"), + ) + + self.assertEqual(anonymous.status_code, 401) + self.assertEqual(forged.status_code, 401) + resolve.assert_not_awaited() + + with patch.dict( + os.environ, + { + "SUBSTRATE_PREVIEW_LOCAL_DEV_MODE": "false", + "SUBSTRATE_PREVIEW_TRUSTED_INGRESS": "false", + }, + ): + asyncio.run(exercise()) + + def test_http_preview_uses_trusted_identity_for_workspace_ownership(self): + async def exercise(): + with patch.object( + preview_proxy, "_resolve_target", new=AsyncMock(return_value=None) + ) as resolve: + response = await preview_proxy._preview_http( + make_request( + "GET", + { + "host": _PREVIEW_HOST, + "x-user-id": "workspace-owner", + "cf-access-authenticated-user-email": "other-owner", + }, + ), + preview_proxy.PreviewHost(5173, "workspace-1"), + ) + + self.assertEqual(response.status_code, 404) + resolve.assert_awaited_once_with("workspace-1", "other-owner") + + with patch.dict( + os.environ, + { + "SUBSTRATE_PREVIEW_LOCAL_DEV_MODE": "false", + "SUBSTRATE_PREVIEW_TRUSTED_INGRESS": "true", + }, + ): + asyncio.run(exercise()) + + def test_websocket_preview_requires_trusted_identity_and_checks_owner(self): + async def exercise_anonymous(): + with patch.object( + preview_proxy, "_resolve_target", new_callable=AsyncMock + ) as resolve: + for headers in ( + {"host": _PREVIEW_HOST}, + {"host": _PREVIEW_HOST, "x-user-id": "local-dev-user"}, + ): + websocket, sent = make_websocket(headers) + await preview_proxy._preview_websocket( + websocket, preview_proxy.PreviewHost(5173, "workspace-1") + ) + self.assertEqual(sent[0]["type"], "websocket.close") + self.assertEqual(sent[0]["code"], 4401) + resolve.assert_not_awaited() + + async def exercise_wrong_owner(): + with patch.object( + preview_proxy, "_resolve_target", new=AsyncMock(return_value=None) + ) as resolve: + websocket, sent = make_websocket( + { + "host": _PREVIEW_HOST, + "x-user-id": "workspace-owner", + "cf-access-authenticated-user-email": "other-owner", + } + ) + await preview_proxy._preview_websocket( + websocket, preview_proxy.PreviewHost(5173, "workspace-1") + ) + self.assertEqual(sent[0]["type"], "websocket.close") + self.assertEqual(sent[0]["code"], 4404) + resolve.assert_awaited_once_with("workspace-1", "other-owner") + + with patch.dict( + os.environ, + { + "SUBSTRATE_PREVIEW_LOCAL_DEV_MODE": "false", + "SUBSTRATE_PREVIEW_TRUSTED_INGRESS": "false", + }, + ): + asyncio.run(exercise_anonymous()) + with patch.dict( + os.environ, + { + "SUBSTRATE_PREVIEW_LOCAL_DEV_MODE": "false", + "SUBSTRATE_PREVIEW_TRUSTED_INGRESS": "true", + }, + ): + asyncio.run(exercise_wrong_owner()) + + def test_post_that_commits_then_disconnects_is_not_replayed(self): + target = preview_proxy.PreviewTarget( + workspace_id="workspace-1", + user_id="local-dev-user", + atespace="fixture-space", + actor="fixture-actor", + agent="claude", + shim_secret_name="shim" + "-fixture", + manifest={"dev": {"ports": [{"name": "web", "number": 5173}]}}, + observed_state="running", + ) + committed: list[bytes] = [] + + class DisconnectingReader: + def __init__(self): + self._lines = iter( + (b"HTTP/1.1 200 Connection Established\r\n", b"\r\n") + ) + self.closed = False + + def readline(self, _size=-1): + try: + return next(self._lines) + except StopIteration as exc: + raise socket.timeout("fake upstream disconnected") from exc + + def close(self): + self.closed = True + + class CommitThenDisconnectSocket: + def __init__(self): + self.reader = DisconnectingReader() + self.sent: list[bytes] = [] + self.closed = False + + def settimeout(self, _timeout): + pass + + def makefile(self, _mode): + return self.reader + + def sendall(self, payload): + self.sent.append(payload) + if payload.startswith(b"POST "): + _headers, body = payload.split(b"\r\n\r\n", 1) + committed.append(body) + + def close(self): + self.closed = True + + fake_upstream = CommitThenDisconnectSocket() + + async def exercise(): + with ( + patch.object(preview_proxy.asyncio, "to_thread", new=_run_sync_in_test), + patch.object( + preview_proxy, "_resolve_target", new=AsyncMock(return_value=target) + ), + patch.object(preview_proxy, "_touch_preview", new=AsyncMock()), + patch.object( + preview_proxy, "_reported_ports", new=AsyncMock(return_value=()) + ), + patch.object( + preview_proxy.socket, + "create_connection", + return_value=fake_upstream, + ), + patch.object( + settings, "substrate_router_address", "http://router.fixture:8081" + ), + ): + response = await preview_proxy._preview_http( + make_request( + "POST", {"host": _PREVIEW_HOST}, b"synthetic mutation" + ), + preview_proxy.PreviewHost(5173, "workspace-1"), + ) + return response + + with patch.dict(os.environ, {"SUBSTRATE_PREVIEW_LOCAL_DEV_MODE": "true"}): + response = asyncio.run(exercise()) + self.assertEqual(response.status_code, 502) + self.assertIn(b"outcome is unknown", response.body) + self.assertEqual(committed, [b"synthetic mutation"]) + self.assertEqual(len(fake_upstream.sent), 2) + self.assertTrue(fake_upstream.reader.closed) + self.assertTrue(fake_upstream.closed) + + def test_router_connect_failure_before_forwarding_is_retried_once(self): + target = preview_proxy.PreviewTarget( + workspace_id="workspace-1", + user_id="local-dev-user", + atespace="fixture-space", + actor="fixture-actor", + agent="claude", + shim_secret_name="shim" + "-fixture", + manifest={"dev": {"ports": [{"name": "web", "number": 5173}]}}, + observed_state="running", + ) + headers = http.client.HTTPMessage() + headers["Content-Length"] = "2" + upstream = preview_proxy._UpstreamHTTP( + sock=type("FakeSocket", (), {"close": lambda _self: None})(), + reader=io.BytesIO(b"ok"), + status=200, + headers=headers, + ) + failures = 0 + + def connect(*_args, **_kwargs): + nonlocal failures + failures += 1 + if failures == 1: + raise preview_proxy._PreviewPreForwardFailure("CONNECT unavailable") + return upstream + + async def exercise(): + with ( + patch.object(preview_proxy.asyncio, "to_thread", new=_run_sync_in_test), + patch.object( + preview_proxy, "_resolve_target", new=AsyncMock(return_value=target) + ), + patch.object(preview_proxy, "_touch_preview", new=AsyncMock()), + patch.object( + preview_proxy, "_reported_ports", new=AsyncMock(return_value=()) + ), + patch.object(preview_proxy, "_connect_router", side_effect=connect), + ): + response = await preview_proxy._preview_http( + make_request("GET", {"host": _PREVIEW_HOST}), + preview_proxy.PreviewHost(5173, "workspace-1"), + ) + body = b"".join([chunk async for chunk in response.body_iterator]) + return response, body + + with patch.dict(os.environ, {"SUBSTRATE_PREVIEW_LOCAL_DEV_MODE": "true"}): + response, body = asyncio.run(exercise()) + + self.assertEqual(response.status_code, 200) + self.assertEqual(body, b"ok") + self.assertEqual(failures, 2) + + def test_activity_lease_refreshes_before_idle_deadline_and_stops_on_close(self): + async def exercise(): + touches: list[float] = [] + + async def touch(_workspace_id: str): + touches.append(asyncio.get_running_loop().time()) + + with ( + patch.object(preview_proxy, "_PREVIEW_TOUCH_INTERVAL_SECONDS", 0.015), + patch.object( + preview_proxy, "_touch_preview", new=AsyncMock(side_effect=touch) + ), + ): + for active_connection in ("websocket", "http-stream"): + async with preview_proxy._preview_activity_lease( + f"workspace-{active_connection}" + ): + await asyncio.sleep(0.07) + self.assertTrue(touches) + now = asyncio.get_running_loop().time() + self.assertLess(now - touches[-1], 0.05) + count_at_close = len(touches) + await asyncio.sleep(0.04) + self.assertEqual(len(touches), count_at_close) + + touches.clear() + async with preview_proxy._preview_activity_lease("shared-workspace"): + async with preview_proxy._preview_activity_lease( + "shared-workspace" + ): + await asyncio.sleep(0.07) + self.assertGreaterEqual(len(touches), 3) + self.assertLessEqual(len(touches), 5) + + asyncio.run(exercise()) + + def test_chunked_response_body_is_decoded_for_streaming_response(self): + import http.client + + headers = http.client.HTTPMessage() + headers["Transfer-Encoding"] = "chunked" + reader = io.BytesIO(b"4\r\nWiki\r\n5\r\npedia\r\n0\r\n\r\n") + self.assertEqual( + list(preview_proxy._response_body(reader, headers, no_body=False)), + [b"Wiki", b"pedia"], + ) + + def test_large_chunk_is_read_in_bounded_blocks(self): + class TrackingReader(io.BytesIO): + max_requested = 0 + + def read(self, size=-1): + self.max_requested = max(self.max_requested, size) + return super().read(size) + + payload = b"x" * (preview_proxy._RESPONSE_READ_BLOCK_BYTES * 3 + 17) + reader = TrackingReader( + f"{len(payload):X}\r\n".encode() + payload + b"\r\n0\r\n\r\n" + ) + headers = http.client.HTTPMessage() + headers["Transfer-Encoding"] = "chunked" + + chunks = list(preview_proxy._response_body(reader, headers, no_body=False)) + + self.assertEqual(b"".join(chunks), payload) + self.assertGreater(len(chunks), 1) + self.assertLessEqual( + reader.max_requested, preview_proxy._RESPONSE_READ_BLOCK_BYTES + ) + + def test_stalled_response_read_closes_upstream(self): + class StalledReader: + closed = False + + def read(self, _size=-1): + raise socket.timeout("fixture stalled") + + def close(self): + self.closed = True + + class FakeSocket: + closed = False + + def close(self): + self.closed = True + + async def exercise(): + reader = StalledReader() + sock = FakeSocket() + headers = http.client.HTTPMessage() + headers["Content-Length"] = "1" + upstream = preview_proxy._UpstreamHTTP(sock, reader, 200, headers) + body = preview_proxy._stream_response_body(upstream, "workspace-1", False) + with patch.object( + preview_proxy.asyncio, "to_thread", new=_run_sync_in_test + ): + with self.assertRaises(socket.timeout): + await anext(body) + self.assertTrue(reader.closed) + self.assertTrue(sock.closed) + + asyncio.run(exercise()) + + def test_router_socket_read_timeout_remains_set_for_stream_body(self): + class FakeSocket: + def __init__(self): + self.reader = io.BytesIO( + b"HTTP/1.1 200 Connection Established\r\n\r\n" + b"HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n" + ) + self.timeout = None + self.sent: list[bytes] = [] + self.closed = False + + def settimeout(self, value): + self.timeout = value + + def makefile(self, _mode): + return self.reader + + def sendall(self, payload): + self.sent.append(payload) + + def close(self): + self.closed = True + + target = preview_proxy.PreviewTarget( + workspace_id="workspace-1", + user_id="owner", + atespace="fixture-space", + actor="fixture-actor", + agent="claude", + shim_secret_name="shim" + "-fixture", + manifest={}, + ) + upstream_socket = FakeSocket() + with ( + patch.object( + settings, "substrate_router_address", "http://router.fixture:8081" + ), + patch.object( + preview_proxy.socket, "create_connection", return_value=upstream_socket + ), + ): + upstream = preview_proxy._connect_router( + target, + 5173, + 2.5, + method="GET", + path="/", + headers=[], + body=b"", + ) + + self.assertEqual(upstream_socket.timeout, 2.5) + self.assertEqual(len(upstream_socket.sent), 2) + upstream.close() + self.assertTrue(upstream_socket.closed) + + def test_websocket_frame_parser_handles_masked_and_unmasked_payloads(self): + async def exercise(): + reader = asyncio.StreamReader() + reader.feed_data( + preview_proxy._encode_websocket_frame(1, b"hello", masked=False) + ) + self.assertEqual( + await preview_proxy._read_websocket_frame(reader), (True, 1, b"hello") + ) + reader.feed_data( + preview_proxy._encode_websocket_frame(2, b"bytes", masked=True) + ) + self.assertEqual( + await preview_proxy._read_websocket_frame(reader), (True, 2, b"bytes") + ) + + asyncio.run(exercise()) + + def test_workspace_port_list_combines_declared_and_reported_ports(self): + async def exercise(): + shim_name = "shim" + "-fixture" + target = preview_proxy.PreviewTarget( + workspace_id="workspace-1", + user_id="owner", + atespace="fixture-space", + actor="fixture-actor", + agent="claude", + shim_secret_name=shim_name, + manifest={"dev": {"ports": [{"name": "web", "number": 5173}]}}, + observed_state="running", + ) + with ( + patch.object(preview_proxy, "_resolve_target", return_value=target), + patch.object( + preview_proxy, "_reported_ports", return_value=(5173, 8080) + ), + patch.object( + settings, + "substrate_preview_base_url", + "http://preview.localhost:8001", + ), + ): + result = await preview_proxy.workspace_preview_ports( + "workspace-1", "owner" + ) + self.assertEqual( + result, + [ + { + "port": 5173, + "name": "web", + "url": "http://5173--workspace-1.preview.localhost:8001", + }, + { + "port": 8080, + "name": "Port 8080", + "url": "http://8080--workspace-1.preview.localhost:8001", + }, + ], + ) + + asyncio.run(exercise()) + + def test_workspace_port_list_does_not_wake_a_parked_actor(self): + async def exercise(): + shim_name = "shim" + "-fixture" + target = preview_proxy.PreviewTarget( + workspace_id="workspace-1", + user_id="owner", + atespace="fixture-space", + actor="fixture-actor", + agent="claude", + shim_secret_name=shim_name, + manifest={"forwardPorts": [5173]}, + observed_state="suspended", + ) + with ( + patch.object(preview_proxy, "_resolve_target", return_value=target), + patch.object( + preview_proxy, "_reported_ports", new_callable=AsyncMock + ) as report, + ): + ports = await preview_proxy.workspace_preview_ports( + "workspace-1", "owner" + ) + report.assert_not_awaited() + self.assertEqual([row["port"] for row in ports], [5173]) + + asyncio.run(exercise()) + + +if __name__ == "__main__": + unittest.main() diff --git a/backend/tests/runtime/test_substrate.py b/backend/tests/runtime/test_substrate.py new file mode 100644 index 0000000..e97db1e --- /dev/null +++ b/backend/tests/runtime/test_substrate.py @@ -0,0 +1,772 @@ +"""Substrate transport adapter over a fake ``kubectl ate`` subprocess: no cluster, no actors, +no credentials. Uses a fake transport to test the Substrate control adapter.""" + +import asyncio +import json +import unittest +from pathlib import Path + +from mainloop.runtime.substrate import ( + ActorFailedToStart, + ActorHealthTimeout, + ActorState, + ExecResult, + GoldenSnapshotFailed, + GoldenSnapshotTimeout, + GoldenState, + IdentityOutcome, + NoEligibleWorker, + SubstrateControl, + TransportError, + WaitTimeout, + _actor_from_json, + _actor_template_from_json, + reconcile_actor_identity, + wait_for_actor_health, + wait_for_actor_running, + wait_for_eligible_worker, + wait_for_golden_snapshot, +) + +# Generated with kubectl-ate's PrintWorkersTo(..., "json") at pinned Substrate commit +# cdac9baef81dd319b46086d695266e6161e9e592. It is CLI printer output with sanitized fixture +# records, not a claim that a live worker was observed. +PINNED_WORKERS_OUTPUT = ( + Path(__file__).parent + / "fixtures" + / "substrate" + / "kubectl-ate-workers-pinned-cdac9ba.json" +).read_text() +PINNED_WORKERS_DOCUMENT = json.loads(PINNED_WORKERS_OUTPUT) + + +class FakeControl(SubstrateControl): + def __init__(self, results): + super().__init__( + kubeconfig="fixture-kubeconfig", context="kind-substrate-preview" + ) + self.results = list(results) + self.calls: list[list[str]] = [] + self.stdins: list[str | None] = [] + + async def _exec(self, args, timeout=45, stdin=None): + self.calls.append(args) + self.stdins.append(stdin) + if args and args[0] in {"get", "create", "resume", "suspend", "revert"}: + if "-o" not in args or args[args.index("-o") + 1] != "json": + return ExecResult(0, "NAME STATUS\nresource Pending\n", "") + result = self.results.pop(0) + if isinstance(result, Exception): + raise result + return result + + +async def fake_sleep(_seconds: float) -> None: + return None + + +class FakeClock: + """A monotonic clock that advances by a fixed step every time it's read, so a bounded + poll loop can be driven to its deadline deterministically without a real delay.""" + + def __init__(self, step: float = 1.0): + self.value = 0.0 + self.step = step + + def __call__(self) -> float: + current = self.value + self.value += self.step + return current + + +def run(coro): + return asyncio.run(coro) + + +def actor_json( + state: str, + *, + snapshot_uri: str | None = None, + template_uid: str | None = None, + uid: str = "u-1", +) -> str: + doc = { + "metadata": {"atespace": "mainloop-workspaces", "name": "ml-abc", "uid": uid}, + "status": {"state": state}, + } + if snapshot_uri or template_uid: + snapshot = {"contentScope": "SNAPSHOT_CONTENT_SCOPE_FULL"} + if snapshot_uri: + snapshot["snapshotUri"] = snapshot_uri + if template_uid: + snapshot["actorTemplateUid"] = template_uid + doc["status"]["externalSnapshot"] = snapshot + return json.dumps(doc) + + +def actor_template_json( + *, error_message: str = "", golden_tag: str | None = None, uid: str = "t-1" +) -> str: + doc = { + "metadata": { + "atespace": "live-agent-gate", + "name": "live-agent-gate-v1", + "uid": uid, + }, + "status": {"goldenSnapshotStatus": {}}, + } + if error_message: + doc["status"]["goldenSnapshotStatus"]["errorMessage"] = error_message + if golden_tag: + doc["status"]["goldenSnapshotStatus"]["goldenTag"] = { + "atespace": "ate-golden", + "name": golden_tag, + } + return json.dumps(doc) + + +def worker_record( + *, + state="WORKER_STATE_ACTIVE", + namespace="live-agent-gate", + pool_label="live-agent-gate", + sandbox_class="gvisor", + capacity=1, + allocated=0, +): + record = json.loads(json.dumps(PINNED_WORKERS_DOCUMENT["workers"][0])) + record["workerNamespace"] = namespace + record["sandboxClass"] = sandbox_class + record["labels"]["workload"] = pool_label + record["status"]["state"] = state + record["status"]["capacity"]["actors"] = capacity + record["status"]["allocated"]["actors"] = allocated + return record + + +class ActorJsonParsingTests(unittest.TestCase): + def test_parses_running_actor_with_snapshot(self): + import json + + doc = json.loads( + actor_json( + "ACTOR_STATE_RUNNING", + snapshot_uri="gs://b/p", + template_uid="template-snapshot-1", + ) + ) + record = _actor_from_json(doc) + self.assertEqual(record.atespace, "mainloop-workspaces") + self.assertEqual(record.name, "ml-abc") + self.assertEqual(record.state, ActorState.RUNNING) + self.assertEqual(record.external_snapshot_uri, "gs://b/p") + self.assertEqual(record.current_actor_template_uid, "template-snapshot-1") + + def test_template_uid_is_read_from_external_snapshot(self): + doc = { + "metadata": {"atespace": "mainloop-workspaces", "name": "ml-abc"}, + "status": { + "state": "ACTOR_STATE_SUSPENDED", + "externalSnapshot": { + "snapshotUri": "gs://b/p", + "contentScope": "SNAPSHOT_CONTENT_SCOPE_FULL", + "actorTemplateUid": "template-snapshot-only", + }, + }, + } + + record = _actor_from_json(doc) + + self.assertNotIn("currentActorTemplateUid", doc["status"]) + self.assertEqual(record.current_actor_template_uid, "template-snapshot-only") + + def test_unrecognized_state_string_is_unspecified_not_a_crash(self): + record = _actor_from_json( + {"metadata": {}, "status": {"state": "SOME_FUTURE_STATE"}} + ) + self.assertEqual(record.state, ActorState.UNSPECIFIED) + + def test_missing_status_defaults_to_unspecified(self): + record = _actor_from_json({"metadata": {"atespace": "a", "name": "n"}}) + self.assertEqual(record.state, ActorState.UNSPECIFIED) + self.assertIsNone(record.external_snapshot_uri) + + +class SubstrateControlTests(unittest.TestCase): + def test_base_args_use_direct_endpoint_and_projected_token_file_in_cluster(self): + projected_token_path = "/var/run/secrets/tokens/substrate-api/token" # nosec B105 - path only; the projected token contents are not arguments + ctl = SubstrateControl( + cli="kubectl-ate", + endpoint="api.ate-system.svc:443", + token_file=projected_token_path, + kubeconfig="", + context="", + ) + + args = ctl._base_args() + + self.assertEqual( + args, + [ + "kubectl-ate", + "--endpoint", + "api.ate-system.svc:443", + "--token-file", + projected_token_path, + ], + ) + self.assertNotIn("fixture-bearer-token", args) + + def test_base_args_keep_configured_local_kubeconfig_and_context(self): + empty = "" + ctl = SubstrateControl( + cli="kubectl-ate", + kubeconfig="/fixture/kubeconfig", + context="kind-substrate-preview", + endpoint=empty, + token_file=empty, + ) + + self.assertEqual( + ctl._base_args(), + [ + "kubectl-ate", + "--kubeconfig", + "/fixture/kubeconfig", + "--context", + "kind-substrate-preview", + ], + ) + + def test_get_actor_parses_json_and_uses_argv_not_shell(self): + ctl = FakeControl([ExecResult(0, actor_json("ACTOR_STATE_RUNNING"), "")]) + actor = run(ctl.get_actor("mainloop-workspaces", "ml-abc")) + self.assertEqual(actor.state, ActorState.RUNNING) + self.assertEqual( + ctl.calls[0], + [ + "get", + "actor", + "ml-abc", + "--atespace", + "mainloop-workspaces", + "-o", + "json", + ], + ) + + def test_get_actor_not_found_returns_none_not_an_exception(self): + ctl = FakeControl( + [ExecResult(1, "", 'Error: actors.ate.dev "ml-abc" not found')] + ) + self.assertIsNone(run(ctl.get_actor("mainloop-workspaces", "ml-abc"))) + + def test_get_actor_other_failure_raises_transport_error(self): + ctl = FakeControl([ExecResult(1, "", "connection refused")]) + with self.assertRaises(TransportError): + run(ctl.get_actor("mainloop-workspaces", "ml-abc")) + + def test_create_actor_uses_template_flag(self): + # A freshly created actor starts SUSPENDED (never auto-started); measured against a + # live kind-substrate-preview cluster while building this adapter. + ctl = FakeControl([ExecResult(0, actor_json("ACTOR_STATE_SUSPENDED"), "")]) + run( + ctl.create_actor( + "mainloop-workspaces", "ml-abc", template="mainloop-workspace" + ) + ) + self.assertEqual( + ctl.calls[0], + [ + "create", + "actor", + "ml-abc", + "--atespace", + "mainloop-workspaces", + "--template", + "mainloop-workspace", + "-o", + "json", + ], + ) + + def test_create_actor_falls_back_to_a_read_when_output_is_empty(self): + ctl = FakeControl( + [ + ExecResult(0, "", ""), + ExecResult(0, actor_json("ACTOR_STATE_RESUMING"), ""), + ] + ) + actor = run(ctl.create_actor("mainloop-workspaces", "ml-abc", template="t")) + self.assertEqual(actor.state, ActorState.RESUMING) + self.assertEqual(ctl.calls[1][:2], ["get", "actor"]) + + def test_revert_is_a_single_explicit_call_never_a_retry_loop(self): + ctl = FakeControl([ExecResult(0, actor_json("ACTOR_STATE_SUSPENDED"), "")]) + actor = run(ctl.revert_actor("mainloop-workspaces", "ml-abc")) + self.assertEqual(actor.state, ActorState.SUSPENDED) + self.assertEqual(len(ctl.calls), 1) + self.assertEqual(ctl.calls[0][:2], ["revert", "actor"]) + + def test_delete_actor_tolerates_already_gone(self): + ctl = FakeControl([ExecResult(1, "", "not found")]) + run(ctl.delete_actor("mainloop-workspaces", "ml-abc")) # does not raise + + def test_transport_error_on_transient_failure_is_not_retried(self): + ctl = FakeControl([TransportError("boom")]) + with self.assertRaises(TransportError): + run(ctl.suspend_actor("mainloop-workspaces", "ml-abc")) + self.assertEqual(len(ctl.calls), 1) + + +class AtespaceTests(unittest.TestCase): + def test_ensure_atespace_creates(self): + ctl = FakeControl([ExecResult(0, "{}", "")]) + run(ctl.ensure_atespace("live-agent-gate")) + self.assertEqual( + ctl.calls[0], ["create", "atespace", "live-agent-gate", "-o", "json"] + ) + + def test_ensure_atespace_tolerates_already_exists(self): + ctl = FakeControl( + [ + ExecResult( + 1, "", 'Error: atespaces.ate.dev "live-agent-gate" already exists' + ) + ] + ) + run(ctl.ensure_atespace("live-agent-gate")) # does not raise + + def test_ensure_atespace_other_failure_raises(self): + ctl = FakeControl([ExecResult(1, "", "connection refused")]) + with self.assertRaises(TransportError): + run(ctl.ensure_atespace("live-agent-gate")) + + def test_atespace_exists_false_on_not_found(self): + ctl = FakeControl([ExecResult(1, "", 'Error: atespaces.ate.dev "x" not found')]) + self.assertFalse(run(ctl.atespace_exists("x"))) + + +class ActorTemplateJsonParsingTests(unittest.TestCase): + def test_pending_when_no_tag_and_no_error(self): + record = _actor_template_from_json(json.loads(actor_template_json())) + self.assertEqual(record.golden_state, GoldenState.PENDING) + + def test_ready_when_golden_tag_present(self): + record = _actor_template_from_json( + json.loads(actor_template_json(golden_tag="golden-1")) + ) + self.assertEqual(record.golden_state, GoldenState.READY) + self.assertEqual(record.golden_tag, "golden-1") + + def test_failed_when_error_message_present(self): + record = _actor_template_from_json( + json.loads(actor_template_json(error_message="golden actor crashed")) + ) + self.assertEqual(record.golden_state, GoldenState.FAILED) + self.assertEqual(record.error_message, "golden actor crashed") + + +class ActorTemplateControlTests(unittest.TestCase): + def test_get_actor_template_uses_atespace_flag_not_a_composite_name(self): + # The prior harness bug: `get actor-template "/"` as a single + # positional argument, which the CLI requires as separate `-a `. + ctl = FakeControl([ExecResult(0, actor_template_json(golden_tag="g1"), "")]) + run(ctl.get_actor_template("live-agent-gate", "live-agent-gate-v1")) + self.assertEqual( + ctl.calls[0], + [ + "get", + "actor-template", + "live-agent-gate-v1", + "-a", + "live-agent-gate", + "-o", + "json", + ], + ) + + def test_get_actor_template_not_found_returns_none(self): + ctl = FakeControl( + [ExecResult(1, "", 'Error: actortemplates.ate.dev "x" not found')] + ) + self.assertIsNone(run(ctl.get_actor_template("live-agent-gate", "x"))) + + def test_get_actor_template_other_failure_raises_transport_error(self): + ctl = FakeControl([ExecResult(1, "", "connection refused")]) + with self.assertRaises(TransportError): + run(ctl.get_actor_template("live-agent-gate", "x")) + + def test_create_actor_template_requests_json_and_passes_manifest_over_stdin(self): + ctl = FakeControl([ExecResult(0, actor_template_json(), "")]) + run( + ctl.create_actor_template( + "live-agent-gate", "live-agent-gate-v1", "metadata:\n name: x\n" + ) + ) + self.assertEqual( + ctl.calls[0], + ["create", "actor-template", "-f", "-", "-o", "json"], + ) + self.assertEqual(ctl.stdins[0], "metadata:\n name: x\n") + + def test_uncertain_template_create_reads_before_returning_success(self): + ctl = FakeControl( + [ + TransportError("request timed out after send"), + ExecResult(0, actor_template_json(golden_tag="g1"), ""), + ] + ) + record = run( + ctl.create_actor_template( + "live-agent-gate", "live-agent-gate-v1", "manifest" + ) + ) + self.assertEqual(record.uid, "t-1") + self.assertEqual(len(ctl.calls), 2) + self.assertEqual( + ctl.calls[1][:3], ["get", "actor-template", "live-agent-gate-v1"] + ) + + def test_uncertain_template_create_reads_before_reporting_absent(self): + ctl = FakeControl( + [ + TransportError("request timed out after send"), + ExecResult(1, "", 'Error: actortemplates.ate.dev "x" not found'), + ] + ) + with self.assertRaises(TransportError): + run(ctl.create_actor_template("live-agent-gate", "x", "manifest")) + self.assertEqual( + len(ctl.calls), 2 + ) # read-back happened before any caller retry + + +class WaitForGoldenSnapshotTests(unittest.TestCase): + def test_returns_once_ready(self): + ctl = FakeControl( + [ + ExecResult(0, actor_template_json(), ""), # pending + ExecResult(0, actor_template_json(golden_tag="g1"), ""), # ready + ] + ) + record = run( + wait_for_golden_snapshot( + ctl, + "live-agent-gate", + "live-agent-gate-v1", + timeout_s=30, + poll_interval_s=0, + sleep=fake_sleep, + clock=FakeClock(step=1), + ) + ) + self.assertEqual(record.golden_state, GoldenState.READY) + + def test_raises_on_reported_failure_not_just_returns(self): + ctl = FakeControl( + [ExecResult(0, actor_template_json(error_message="boom"), "")] + ) + with self.assertRaises(GoldenSnapshotFailed): + run( + wait_for_golden_snapshot( + ctl, + "live-agent-gate", + "live-agent-gate-v1", + timeout_s=30, + sleep=fake_sleep, + clock=FakeClock(step=1), + ) + ) + + def test_raises_timeout_rather_than_claiming_success(self): + # Every poll is still pending; the fake clock advances past the deadline. This is + # the "polling loop prints success after timeout" bug from the recovery review -- + # here, timing out must raise, never return. + ctl = FakeControl([ExecResult(0, actor_template_json(), "") for _ in range(50)]) + with self.assertRaises(GoldenSnapshotTimeout): + run( + wait_for_golden_snapshot( + ctl, + "live-agent-gate", + "live-agent-gate-v1", + timeout_s=5, + poll_interval_s=0, + sleep=fake_sleep, + clock=FakeClock(step=1), + ) + ) + + def test_disappearing_template_is_a_transport_error_not_pending(self): + ctl = FakeControl( + [ExecResult(1, "", 'Error: actortemplates.ate.dev "x" not found')] + ) + with self.assertRaises(TransportError): + run( + wait_for_golden_snapshot( + ctl, + "live-agent-gate", + "live-agent-gate-v1", + timeout_s=30, + sleep=fake_sleep, + clock=FakeClock(step=1), + ) + ) + + +class GetEligibleWorkersTests(unittest.TestCase): + def get_workers(self, workers): + return FakeControl([ExecResult(0, json.dumps({"workers": workers}), "")]) + + def test_parses_pinned_cli_json_and_uses_pool_filters(self): + ctl = FakeControl([ExecResult(0, PINNED_WORKERS_OUTPUT, "")]) + self.assertEqual( + run( + ctl.get_eligible_workers( + "live-agent-gate", "workload=live-agent-gate", "gvisor" + ) + ), + 1, + ) + self.assertEqual( + ctl.calls[0], + [ + "get", + "workers", + "-n", + "live-agent-gate", + "-l", + "workload=live-agent-gate", + "--sandbox-class", + "gvisor", + "-o", + "json", + ], + ) + + def test_excludes_occupied_worker_using_capacity_minus_allocated(self): + ctl = self.get_workers([worker_record(capacity=1, allocated=1)]) + self.assertEqual( + run( + ctl.get_eligible_workers( + "live-agent-gate", "workload=live-agent-gate", "gvisor" + ) + ), + 0, + ) + + def test_excludes_draining_worker(self): + ctl = self.get_workers([worker_record(state="WORKER_STATE_DRAINING")]) + self.assertEqual( + run( + ctl.get_eligible_workers( + "live-agent-gate", "workload=live-agent-gate", "gvisor" + ) + ), + 0, + ) + + def test_excludes_wrong_pool_worker(self): + ctl = self.get_workers([worker_record(pool_label="other")]) + self.assertEqual( + run( + ctl.get_eligible_workers( + "live-agent-gate", "workload=live-agent-gate", "gvisor" + ) + ), + 0, + ) + + def test_empty_workers_list_returns_zero(self): + ctl = self.get_workers([]) + self.assertEqual( + run( + ctl.get_eligible_workers( + "live-agent-gate", "workload=live-agent-gate", "gvisor" + ) + ), + 0, + ) + + def test_empty_object_matches_pinned_cli_empty_result(self): + ctl = FakeControl([ExecResult(0, "{}", "")]) + self.assertEqual( + run( + ctl.get_eligible_workers( + "live-agent-gate", "workload=live-agent-gate", "gvisor" + ) + ), + 0, + ) + + def test_missing_workers_field_is_a_contract_error(self): + ctl = FakeControl([ExecResult(0, json.dumps({"items": []}), "")]) + with self.assertRaisesRegex(TransportError, "missing its 'workers' list"): + run( + ctl.get_eligible_workers( + "live-agent-gate", "workload=live-agent-gate", "gvisor" + ) + ) + + +class WaitForEligibleWorkerTests(unittest.TestCase): + def test_returns_once_a_worker_is_eligible(self): + ctl = FakeControl( + [ + ExecResult(0, json.dumps({"workers": []}), ""), + ExecResult(0, json.dumps({"workers": [worker_record()]}), ""), + ] + ) + count = run( + wait_for_eligible_worker( + ctl, + "live-agent-gate", + "workload=live-agent-gate", + "gvisor", + timeout_s=30, + poll_interval_s=0, + sleep=fake_sleep, + clock=FakeClock(step=1), + ) + ) + self.assertEqual(count, 1) + + def test_raises_no_eligible_worker_on_timeout(self): + ctl = FakeControl( + [ExecResult(0, json.dumps({"workers": []}), "") for _ in range(50)] + ) + with self.assertRaises(NoEligibleWorker): + run( + wait_for_eligible_worker( + ctl, + "live-agent-gate", + "workload=live-agent-gate", + "gvisor", + timeout_s=5, + poll_interval_s=0, + sleep=fake_sleep, + clock=FakeClock(step=1), + ) + ) + + +class WaitForActorRunningTests(unittest.TestCase): + def test_returns_once_running(self): + ctl = FakeControl( + [ + ExecResult(0, actor_json("ACTOR_STATE_RESUMING"), ""), + ExecResult(0, actor_json("ACTOR_STATE_RUNNING"), ""), + ] + ) + actor = run( + wait_for_actor_running( + ctl, + "mainloop-workspaces", + "ml-abc", + timeout_s=30, + poll_interval_s=0, + sleep=fake_sleep, + clock=FakeClock(step=1), + ) + ) + self.assertEqual(actor.state, ActorState.RUNNING) + + def test_raises_actor_failed_to_start_on_crash_not_timeout(self): + ctl = FakeControl([ExecResult(0, actor_json("ACTOR_STATE_CRASHED"), "")]) + with self.assertRaises(ActorFailedToStart): + run( + wait_for_actor_running( + ctl, + "mainloop-workspaces", + "ml-abc", + timeout_s=30, + sleep=fake_sleep, + clock=FakeClock(step=1), + ) + ) + + def test_raises_wait_timeout_when_stuck_resuming(self): + ctl = FakeControl( + [ExecResult(0, actor_json("ACTOR_STATE_RESUMING"), "") for _ in range(50)] + ) + with self.assertRaises(WaitTimeout): + run( + wait_for_actor_running( + ctl, + "mainloop-workspaces", + "ml-abc", + timeout_s=5, + poll_interval_s=0, + sleep=fake_sleep, + clock=FakeClock(step=1), + ) + ) + + +class WaitForActorHealthTests(unittest.TestCase): + def test_slow_start_waits_until_health_route_returns_200(self): + checks = iter([False, False, True]) + run( + wait_for_actor_health( + lambda: next(checks), + timeout_s=10, + poll_interval_s=0, + sleep=fake_sleep, + clock=FakeClock(step=1), + ) + ) + + def test_failed_pane_or_shim_times_out_while_health_stays_non_200(self): + with self.assertRaises(ActorHealthTimeout): + run( + wait_for_actor_health( + lambda: False, + timeout_s=3, + poll_interval_s=0, + sleep=fake_sleep, + clock=FakeClock(step=1), + ) + ) + + def test_restored_actor_passes_without_replaying_boot_marker(self): + # Restore resumes processes; current health, not a repeated startup log, is the + # readiness contract. + run( + wait_for_actor_health( + lambda: True, + timeout_s=3, + sleep=fake_sleep, + clock=FakeClock(step=1), + ) + ) + + +class ReconcileActorIdentityTests(unittest.TestCase): + def test_absent_when_no_live_actor(self): + self.assertEqual(reconcile_actor_identity("u-1", None), IdentityOutcome.ABSENT) + + def test_matches_when_uids_equal(self): + live = _actor_from_json( + json.loads(actor_json("ACTOR_STATE_RUNNING", uid="u-1")) + ) + self.assertEqual(reconcile_actor_identity("u-1", live), IdentityOutcome.MATCHES) + + def test_unowned_when_no_actor_uid_was_persisted(self): + live = _actor_from_json( + json.loads(actor_json("ACTOR_STATE_RUNNING", uid="u-1")) + ) + self.assertEqual(reconcile_actor_identity(None, live), IdentityOutcome.UNOWNED) + + def test_diverged_when_uids_differ(self): + # A rerun must not silently resume or recreate an actor that turned out to belong + # to a different run under the same name. + live = _actor_from_json( + json.loads(actor_json("ACTOR_STATE_RUNNING", uid="u-2")) + ) + self.assertEqual( + reconcile_actor_identity("u-1", live), IdentityOutcome.DIVERGED + ) + + +if __name__ == "__main__": + unittest.main() diff --git a/backend/tests/runtime/test_substrate_preview_manifest.py b/backend/tests/runtime/test_substrate_preview_manifest.py new file mode 100644 index 0000000..c898018 --- /dev/null +++ b/backend/tests/runtime/test_substrate_preview_manifest.py @@ -0,0 +1,88 @@ +"""Manifest contract for the preview backend's direct Substrate API access.""" + +from __future__ import annotations + +import unittest +from pathlib import Path + +import yaml + +ROOT = Path(__file__).resolve().parents[3] +OVERLAY = ROOT / "k8s/apps/mainloop/overlays/substrate-preview" + + +def load_documents(path: Path) -> list[dict]: + return [doc for doc in yaml.safe_load_all(path.read_text()) if doc] + + +class SubstratePreviewManifestTests(unittest.TestCase): + def test_backend_mounts_audience_scoped_token_for_the_direct_endpoint(self): + deployment = load_documents(OVERLAY / "backend.yaml")[0] + configmap = next( + doc + for doc in load_documents(OVERLAY / "configmap.yaml") + if doc["kind"] == "ConfigMap" + ) + pod = deployment["spec"]["template"]["spec"] + container = pod["containers"][0] + volume = next( + volume + for volume in pod["volumes"] + if volume["name"] == "substrate-api-token" + ) + token_projection = volume["projected"]["sources"][0]["serviceAccountToken"] + + self.assertEqual(pod["serviceAccountName"], "mainloop-backend") + self.assertEqual( + token_projection, + { + "audience": "api.ate-system.svc", + "expirationSeconds": 3600, + "path": "token", + }, + ) + self.assertIn( + { + "name": "substrate-api-token", + "mountPath": "/var/run/secrets/tokens/substrate-api", + "readOnly": True, + }, + container["volumeMounts"], + ) + self.assertEqual( + configmap["data"]["SUBSTRATE_ENDPOINT"], "api.ate-system.svc:443" + ) + self.assertEqual( + configmap["data"]["SUBSTRATE_TOKEN_FILE"], + "/var/run/secrets/tokens/substrate-api/token", + ) + + def test_cluster_role_only_lists_substrate_trust_bundles(self): + documents = load_documents(OVERLAY / "backend-rbac.yaml") + cluster_role = next(doc for doc in documents if doc["kind"] == "ClusterRole") + binding = next(doc for doc in documents if doc["kind"] == "ClusterRoleBinding") + + self.assertEqual( + cluster_role["rules"], + [ + { + "apiGroups": ["certificates.k8s.io"], + "resources": ["clustertrustbundles"], + "verbs": ["list"], + } + ], + ) + self.assertEqual( + binding["subjects"], + [ + { + "kind": "ServiceAccount", + "name": "mainloop-backend", + "namespace": "mainloop-control", + } + ], + ) + + +if __name__ == "__main__": + unittest.main() diff --git a/backend/tests/runtime/test_substrate_workspace.py b/backend/tests/runtime/test_substrate_workspace.py new file mode 100644 index 0000000..f4cf92d --- /dev/null +++ b/backend/tests/runtime/test_substrate_workspace.py @@ -0,0 +1,651 @@ +"""In-process fake-router coverage; tests never open sockets or access Kubernetes.""" + +from __future__ import annotations + +import asyncio +import json +import unittest +from unittest.mock import AsyncMock, patch +from urllib.parse import parse_qs, urlsplit +from uuid import uuid4 + +from mainloop.config import SubstrateActorBinding, settings +from mainloop.runtime import native_sessions +from mainloop.runtime import substrate_workspace as substrate_workspace_module +from mainloop.runtime.substrate_workspace import ( + SubstrateWorkspace, + WorkspaceUnavailable, + _Response, +) + +FIXTURE_VALUE = "fixture-shim-value-one" +ROTATED_FIXTURE_VALUE = "fixture-shim-value-two" +FAKE_SHIM_NAME = "shim-fixture" + + +class FakeRouterAndShim: + """In-process CONNECT/router and authenticated shim model for transport contracts.""" + + def __init__(self, *, token_installed=True): + self.suspended = False + self.capacity = False + self.expected_token = FIXTURE_VALUE if token_installed else None + self.token_installed = token_installed + self.inflight: set[tuple[str, str]] = set() + self.turns: dict[tuple[str, str], dict] = {} + self.journal_lines = [ + '{"type":"user"}', + '{"type":"assistant"}', + '{"type":"system","subtype":"turn_duration"}', + ] + self.connects: list[tuple[str, str]] = [] + self.requests: list[tuple[str, str, dict]] = [] + self.credentials: dict[str, str] = {} + + def request( + self, + *, + actor: str, + atespace: str, + method: str, + path: str, + token: str | None, + body: dict | None, + **_unused, + ) -> _Response: + self.connects.append(("CONNECT", f"{atespace}/{actor}")) + if self.capacity: + return _Response(503, "capacity unavailable") + body = body or {} + self.requests.append((method, path, body)) + if (method, path) not in ( + ("GET", "/healthz"), + ("POST", "/token"), + ) and token != self.expected_token: + return _Response(401, "unauthorized") + if method == "POST" and path == "/token": + if self.token_installed: + return _Response(409, "token already set") + self.token_installed = True + self.expected_token = body.get("token") + return _Response(201, "token set") + if method == "GET" and path == "/healthz": + if self.suspended: + self.suspended = False + return _Response(200, "ok") + parsed = urlsplit(path) + query = parse_qs(parsed.query) + if method == "PUT" and parsed.path == "/credential": + self.credentials[body["name"]] = body["contents"] + return _Response(200, "credential stored") + if method == "GET" and parsed.path == "/agent/ready": + return _Response( + 200, json.dumps({"agent": query["agent"][0], "configured": True}) + ) + if method == "GET" and parsed.path == "/ports": + return _Response(200, json.dumps({"ports": [3000, 5173, 3000]})) + if method == "GET" and parsed.path == "/turn/status": + key = ( + query.get("agent", [""])[0], + query.get("session_key", [""])[0], + ) + turn = self.turns.get(key) + return ( + _Response(200, json.dumps(turn)) if turn else _Response(404, "no turn") + ) + if method == "POST" and parsed.path == "/turn": + agent = body.get("agent") + key = (agent, body.get("session_key", "")) + if key in self.inflight: + return _Response(409, "turn already in flight") + turn = { + "id": str(uuid4()), + "agent": agent, + "session_key": key[1], + "status": "running", + "native_session_id": body.get("session_id") or f"native-{key[1]}", + "events": [], + } + self.turns[key] = turn + self.inflight.add(key) + return _Response(202, json.dumps({"id": turn["id"], "status": "running"})) + if method == "POST" and parsed.path == "/turn/stop": + key = (body.get("agent"), body.get("session_key", "")) + self.inflight.discard(key) + turn = self.turns.get(key) + if turn: + turn["status"] = "interrupted" + return _Response(200, json.dumps({"status": "interrupted"})) + if method == "GET" and parsed.path == "/journal": + start = int(query.get("from", ["0"])[0]) + limit = int(query.get("limit", ["200"])[0]) + document = { + "file": "/fake/fixture-session.jsonl", + "total_lines": len(self.journal_lines), + "lines": [ + {"line": n, "text": line} + for n, line in enumerate(self.journal_lines, 1) + if n > start + ][:limit], + } + return _Response(200, json.dumps(document)) + return _Response(404, "not found") + + +class FakeSubstrateWorkspace(SubstrateWorkspace): + def __init__(self, router: FakeRouterAndShim, **kwargs): + super().__init__(**kwargs) + self.router = router + self.fixture_value = FIXTURE_VALUE + self.fixture_reads = 0 + + async def _token(self) -> str: + if self._token_value is None: + self.fixture_reads += 1 + self._token_value = self.fixture_value + return self._token_value + + async def _exchange( + self, method: str, path: str, *, token: str | None, body: dict | None + ): + return self.router.request( + actor=self.actor, + atespace=self.atespace, + method=method, + path=path, + token=token, + body=body, + ) + + +def fake_workspace(router: FakeRouterAndShim, *, shim_name=FAKE_SHIM_NAME): + return FakeSubstrateWorkspace( + router, + atespace="atespace-fixture", + actor="actor-fixture", + agent="claude", + shim_token_secret_name=shim_name, + logical_session_id="session-fixture", + router_address="http://router-fixture:8081", + timeout=2, + credential_broker=FakeCredentialBroker(), + ) + + +class FakeCredentialBroker: + async def codex_placeholder_auth(self) -> str: + return json.dumps( + { + "tokens": { + "id_token": "fixture.header.synthetic", + "access_token": "fixture.header.synthetic", + "refresh_token": "", + "account_id": "fixture-account", + }, + "last_refresh": "2026-09-24T00:00:00Z", + } + ) + + async def claude_placeholder_token(self) -> str: + return "synthetic-claude-egress-placeholder" + + +class SubstrateWorkspaceTests(unittest.TestCase): + def test_same_provider_bindings_have_distinct_logical_transports(self): + first = { + "session_id": "claude-session-a", + "kind": "claude", + "native_session_id": "native-a", + } + second = { + "session_id": "claude-session-b", + "kind": "claude", + "native_session_id": "native-b", + } + actor_binding = SubstrateActorBinding( + atespace="atespace-fixture", + actor="actor-fixture", + shim_token_secret_name=FAKE_SHIM_NAME, + ) + keys = [ + ( + binding["session_id"], + "atespace-fixture", + "actor-fixture", + FAKE_SHIM_NAME, + "claude", + ) + for binding in (first, second) + ] + with patch.object( + settings, "substrate_actor_bindings", {"claude": actor_binding} + ): + try: + ws_a = native_sessions.workspace_for(first) + ws_b = native_sessions.workspace_for(second) + self.assertIsNot(ws_a, ws_b) + self.assertEqual(ws_a.logical_session_id, "claude-session-a") + self.assertEqual(ws_b.logical_session_id, "claude-session-b") + self.assertEqual(ws_a.native_session_id, "native-a") + self.assertEqual(ws_b.native_session_id, "native-b") + ws_a.set_native_session_id("native-a-updated") + self.assertEqual(ws_b.native_session_id, "native-b") + finally: + for key in keys: + native_sessions._workspaces.pop(key, None) + + def test_branch_binding_uses_its_workspace_actor(self): + binding = { + "session_id": "workspace-session-fixture", + "kind": "claude", + "workspace_atespace": "workspace-space", + "workspace_actor_name": "workspace-actor", + "workspace_shim_token_secret_name": "workspace-shim", + } + key = ( + binding["session_id"], + binding["workspace_atespace"], + binding["workspace_actor_name"], + binding["workspace_shim_token_secret_name"], + binding["kind"], + ) + with patch.object(settings, "substrate_actor_bindings", {}): + try: + workspace = native_sessions.workspace_for(binding) + self.assertEqual(workspace.atespace, "workspace-space") + self.assertEqual(workspace.actor, "workspace-actor") + self.assertEqual(workspace.secret_name, "workspace-shim") + finally: + native_sessions._workspaces.pop(key, None) + + def test_incomplete_workspace_binding_does_not_fall_back_to_shared_actor(self): + binding = { + "session_id": "workspace-session-fixture", + "kind": "claude", + "workspace_atespace": "workspace-space", + "workspace_actor_name": None, + "workspace_shim_token_secret_name": "workspace-shim", + } + with patch.object(settings, "substrate_actor_bindings", {}): + with self.assertRaisesRegex(RuntimeError, "incomplete actor route"): + native_sessions.workspace_for(binding) + + def test_dynamic_shim_token_is_bootstrapped_before_authenticated_calls(self): + async def exercise(): + router = FakeRouterAndShim(token_installed=False) + workspace = fake_workspace(router) + + await workspace.send("agent-fixture", "fixture prompt") + + token_requests = [ + (index, body) + for index, (method, path, body) in enumerate(router.requests) + if method == "POST" and path == "/token" + ] + turn_requests = [ + index + for index, (method, path, _body) in enumerate(router.requests) + if method == "POST" and path == "/turn" + ] + self.assertEqual(token_requests, [(0, {"token": FIXTURE_VALUE})]) + self.assertTrue(turn_requests) + self.assertLess(token_requests[0][0], turn_requests[0]) + self.assertEqual(router.expected_token, FIXTURE_VALUE) + + asyncio.run(exercise()) + + def test_codex_start_and_resume_install_only_synthetic_auth(self): + async def exercise(): + router = FakeRouterAndShim() + workspace = FakeSubstrateWorkspace( + router, + atespace="atespace-fixture", + actor="actor-fixture", + agent="codex", + shim_token_secret_name=FAKE_SHIM_NAME, + router_address="http://router-fixture:8081", + timeout=2, + credential_broker=FakeCredentialBroker(), + ) + await workspace.start( + "codex", "agent-fixture", native_id=None, resume=False + ) + installed = json.loads(router.credentials["codex-auth"]) + self.assertEqual(installed["tokens"]["account_id"], "fixture-account") + self.assertEqual(installed["tokens"]["refresh_token"], "") + self.assertEqual( + installed["tokens"]["access_token"], "fixture.header.synthetic" + ) + + await workspace.start( + "codex", + "agent-fixture", + native_id="native-fixture-id", + resume=True, + ) + writes = [ + request + for request in router.requests + if request[0] == "PUT" and request[1] == "/credential" + ] + self.assertEqual(len(writes), 2) + + asyncio.run(exercise()) + + def test_start_send_status_native_id_and_journal_pages(self): + async def exercise(): + router = FakeRouterAndShim() + router.suspended = True + workspace = fake_workspace(router) + ident = await workspace.start( + "claude", "agent-fixture", native_id=None, resume=False + ) + self.assertEqual(ident["actor"], "actor-fixture") + self.assertFalse(router.suspended) + self.assertEqual( + router.credentials["claude-token"], + "synthetic-claude-egress-placeholder", + ) + + await workspace.send("agent-fixture", "fixture prompt") + turn = next( + body + for method, path, body in router.requests + if method == "POST" and path == "/turn" + ) + self.assertNotIn("startup_options", turn) + status = await workspace.agent_status("agent-fixture") + self.assertEqual(status["status"], "running") + self.assertEqual( + await workspace.native_id("agent-fixture"), "native-session-fixture" + ) + first = await workspace.journal( + "agent-fixture", "native-session-fixture", 0 + ) + next_page = await workspace.journal( + "agent-fixture", "native-session-fixture", 1 + ) + self.assertEqual(first.file, "/fake/fixture-session.jsonl") + self.assertEqual(first.total_lines, 3) + self.assertEqual(first.lines[0], (1, '{"type":"user"}')) + self.assertEqual(next_page.lines[0], (2, '{"type":"assistant"}')) + self.assertEqual(await workspace.listening_ports(), (3000, 5173)) + self.assertIn( + ("CONNECT", "atespace-fixture/actor-fixture"), router.connects + ) + sent = [ + body + for method, path, body in router.requests + if method == "POST" and path == "/turn" + ] + self.assertEqual( + sent, + [ + { + "agent": "claude", + "prompt": "fixture prompt", + "session_key": "session-fixture", + "resume": False, + } + ], + ) + + asyncio.run(exercise()) + + def test_startup_options_are_forwarded_with_each_native_turn(self): + async def exercise(): + router = FakeRouterAndShim() + workspace = fake_workspace(router) + extra = { + "--cwd-rel": "main", + "--token": "ml_" + "a" * 64, + "--standing-b64": "c3RhbmRpbmcgY29udGV4dA==", + "--approval-policy": "restricted: Bash(mainloop:*) only", + "--model": "sonnet", + "--effort": "medium", + } + + await workspace.start( + "claude", "ml-main", native_id=None, resume=False, extra=extra + ) + await workspace.send("ml-main", "fixture prompt") + + turn = next( + body + for method, path, body in router.requests + if method == "POST" and path == "/turn" + ) + self.assertEqual( + turn["startup_options"], + { + "cwd_rel": "main", + "token": "ml_" + "a" * 64, + "standing_b64": "c3RhbmRpbmcgY29udGV4dA==", + "approval_policy": "restricted: Bash(mainloop:*) only", + "model": "sonnet", + "effort": "medium", + }, + ) + + asyncio.run(exercise()) + + def test_established_native_session_is_marked_for_resume_on_next_turn(self): + async def exercise(): + router = FakeRouterAndShim() + workspace = fake_workspace(router) + await workspace.start( + "claude", + "agent-fixture", + native_id="native-established-session", + resume=True, + ) + await workspace.send("agent-fixture", "resumed fixture prompt") + turn = next( + body + for method, path, body in router.requests + if method == "POST" and path == "/turn" + ) + self.assertEqual(turn["session_id"], "native-established-session") + self.assertEqual(turn["session_key"], "session-fixture") + self.assertTrue(turn["resume"]) + + asyncio.run(exercise()) + + def test_capacity_503_maps_to_workspace_unavailable(self): + async def exercise(): + router = FakeRouterAndShim() + router.capacity = True + with self.assertRaises(WorkspaceUnavailable): + await fake_workspace(router).require_ready() + + asyncio.run(exercise()) + + def test_concurrent_turn_and_bad_token_are_rejected(self): + async def exercise(): + router = FakeRouterAndShim() + workspace = fake_workspace(router) + await workspace.send("agent-fixture", "first fixture prompt") + with self.assertRaisesRegex(RuntimeError, "409"): + await workspace.send("agent-fixture", "second fixture prompt") + + router.expected_token = ROTATED_FIXTURE_VALUE + with self.assertRaisesRegex(RuntimeError, "401"): + await workspace.agent_status("agent-fixture") + + asyncio.run(exercise()) + + def test_401_refreshes_cached_secret_for_safe_request(self): + async def exercise(): + router = FakeRouterAndShim() + workspace = fake_workspace(router) + await workspace.send("agent-fixture", "initial fixture prompt") + router.expected_token = ROTATED_FIXTURE_VALUE + workspace.fixture_value = ROTATED_FIXTURE_VALUE + + status = await workspace.agent_status("agent-fixture") + + self.assertEqual(status["status"], "running") + self.assertEqual(workspace.fixture_reads, 2) + status_requests = [ + request + for request in router.requests + if request[0] == "GET" and request[1].startswith("/turn/status?") + ] + self.assertEqual(len(status_requests), 2) + + asyncio.run(exercise()) + + def test_401_clears_secret_without_replaying_turn(self): + async def exercise(): + router = FakeRouterAndShim() + workspace = fake_workspace(router) + await workspace.send("agent-fixture", "initial fixture prompt") + router.expected_token = ROTATED_FIXTURE_VALUE + workspace.fixture_value = ROTATED_FIXTURE_VALUE + previous_turn_count = sum( + 1 + for request in router.requests + if request[0] == "POST" and request[1] == "/turn" + ) + + with self.assertRaisesRegex(RuntimeError, "401"): + await workspace.send("agent-fixture", "one-shot fixture prompt") + + turn_requests = [ + request + for request in router.requests + if request[0] == "POST" and request[1] == "/turn" + ] + self.assertEqual(len(turn_requests), previous_turn_count + 1) + self.assertEqual(workspace.fixture_reads, 1) + self.assertIsNone(workspace._token_value) + + asyncio.run(exercise()) + + def test_native_sessions_delivery_uses_configured_actor(self): + async def exercise(): + router = FakeRouterAndShim() + binding = { + "session_id": "session-fixture", + "kind": "claude", + "role": "agent", + "agent_name": "agent-fixture", + "native_session_id": "native-fixture-id", + "journal_cursor": 0, + "generation": 1, + "journal_ref": None, + "model": None, + } + workspace_binding = SubstrateActorBinding( + atespace="atespace-configured", + actor="actor-configured", + shim_token_secret_name=FAKE_SHIM_NAME, + ) + key = ( + binding["session_id"], + workspace_binding.atespace, + workspace_binding.actor, + workspace_binding.shim_token_secret_name, + "claude", + ) + + async def fake_exchange(workspace, method, path, *, token, body): + return router.request( + actor=workspace.actor, + atespace=workspace.atespace, + method=method, + path=path, + token=token, + body=body, + ) + + async def fake_token(_workspace): + return FIXTURE_VALUE + + with ( + patch.object( + settings, + "substrate_router_address", + "http://router-fixture:8081", + ), + patch.object( + settings, + "substrate_shim_secret_namespace", + "namespace-fixture", + ), + patch.object( + settings, + "substrate_actor_bindings", + {"claude": workspace_binding}, + ), + patch.object(SubstrateWorkspace, "_exchange", fake_exchange), + patch.object(SubstrateWorkspace, "_token", fake_token), + patch.object( + substrate_workspace_module, + "CredentialBroker", + FakeCredentialBroker, + ), + patch.object( + native_sessions, + "get_binding", + new=AsyncMock(return_value=binding), + ), + patch.object(native_sessions, "_update_binding", new=AsyncMock()), + patch.object( + native_sessions, "_set_delivery", new=AsyncMock() + ) as set_delivery, + patch.object(native_sessions, "sync", new=AsyncMock()) as sync, + ): + native_sessions._workspaces.pop(key, None) + self.assertIsInstance( + native_sessions.workspace_for(binding), SubstrateWorkspace + ) + await asyncio.wait_for( + native_sessions._deliver( + "session-fixture", + "message-fixture", + "configured fixture prompt", + ), + timeout=2, + ) + workspace = native_sessions.workspace_for(binding) + self.assertIsInstance(workspace, SubstrateWorkspace) + self.assertEqual( + (workspace.atespace, workspace.actor, workspace.secret_name), + ("atespace-configured", "actor-configured", FAKE_SHIM_NAME), + ) + self.assertEqual(workspace.secret_namespace, "namespace-fixture") + self.assertIn( + ("CONNECT", "atespace-configured/actor-configured"), router.connects + ) + self.assertEqual( + [ + request + for request in router.requests + if request[0] == "POST" and request[1] == "/turn" + ], + [ + ( + "POST", + "/turn", + { + "agent": "claude", + "prompt": "configured fixture prompt", + "session_key": "session-fixture", + "resume": False, + "session_id": "native-fixture-id", + }, + ) + ], + ) + set_delivery.assert_awaited_once_with( + "message-fixture", "sending", cursor_before=3 + ) + sync.assert_awaited_once_with("session-fixture") + native_sessions._workspaces.pop(key, None) + + asyncio.run(exercise()) + + +if __name__ == "__main__": + unittest.main() diff --git a/backend/tests/runtime/test_workspace_adapter.py b/backend/tests/runtime/test_workspace_adapter.py new file mode 100644 index 0000000..ffcb281 --- /dev/null +++ b/backend/tests/runtime/test_workspace_adapter.py @@ -0,0 +1,110 @@ +"""Pure policy and projection functions from workspace_adapter.py, tested without a database or +cluster -- the same split as test_session_status.py covers for native_sessions.py's status rules. +The DB-backed orchestration functions (ensure_workspace, resume_workspace, ...) are not yet +exercised against a live cluster; see docs/spikes and the task proof note.""" + +import unittest +from datetime import UTC, datetime + +from mainloop.runtime.workspace_adapter import ( + CRASHED_NOTE, + _binding_from_row, + actor_name, + is_crashed, + plan_ensure, +) + +from models import CapabilityState + +NOW = datetime(2026, 9, 22, tzinfo=UTC) + + +def row(**overrides): + base = { + "workspace_id": "sess-1", + "atespace": "mainloop-workspaces", + "actor_name": "ml-sess1", + "actor_template": "mainloop-workspace", + "preview_route": "mainloop-workspaces/ml-sess1", + "runtime_endpoint": "mainloop-workspaces/ml-sess1", + "observed_state": "ready", + "observed_at": NOW, + "updated_at": NOW, + "last_error": None, + } + base.update(overrides) + return base + + +class ActorNameTests(unittest.TestCase): + def test_stable_and_namespace_safe(self): + name = actor_name("0123456789abcdefextra") + self.assertEqual(name, "ml-0123456789abcdef") + self.assertEqual(name, actor_name("0123456789abcdefextra")) # deterministic + + +class PlanEnsureTests(unittest.TestCase): + def test_first_provision_creates(self): + self.assertEqual(plan_ensure(row_exists=False, actor_found=False), "create") + + def test_existing_actor_is_attached_whether_or_not_we_have_a_row(self): + self.assertEqual(plan_ensure(row_exists=False, actor_found=True), "attach") + self.assertEqual(plan_ensure(row_exists=True, actor_found=True), "attach") + + def test_a_row_with_no_matching_actor_is_surfaced_not_recreated(self): + self.assertEqual(plan_ensure(row_exists=True, actor_found=False), "surface_gap") + + +class BindingProjectionTests(unittest.TestCase): + def test_ready_row_has_no_capability_noise(self): + binding = _binding_from_row(row()) + self.assertEqual(binding.observed_state, "ready") + self.assertEqual(binding.capabilities, ()) + + def test_crashed_row_surfaces_a_proved_actor_crashed_capability(self): + binding = _binding_from_row( + row(observed_state="unavailable", last_error=CRASHED_NOTE) + ) + self.assertEqual(len(binding.capabilities), 1) + cap = binding.capabilities[0] + self.assertEqual(cap.capability, "actor_crashed") + self.assertEqual(cap.state, CapabilityState.PROVED) + self.assertEqual(cap.scope, "live") + self.assertIn("CRASHED", cap.detail) + self.assertIn("Substrate's actor record has no snapshot timestamp", cap.detail) + + def test_missing_actor_row_surfaces_a_distinct_capability_from_crashed(self): + binding = _binding_from_row( + row( + observed_state="unavailable", + last_error="actor not found where this row expected one; not recreated " + "automatically (inspect and reconcile explicitly, or delete this row to allow " + "a fresh actor)", + ) + ) + self.assertEqual(binding.capabilities[0].capability, "actor_health") + + def test_runtime_endpoint_falls_back_to_preview_route(self): + binding = _binding_from_row(row(runtime_endpoint=None)) + self.assertEqual(binding.runtime_endpoint, "mainloop-workspaces/ml-sess1") + + def test_runtime_endpoint_falls_back_to_unrouted_before_first_provision_observation( + self, + ): + binding = _binding_from_row(row(runtime_endpoint=None, preview_route=None)) + self.assertEqual(binding.runtime_endpoint, "unrouted") + + +class IsCrashedTests(unittest.TestCase): + def test_true_only_for_the_crashed_note(self): + self.assertTrue( + is_crashed(row(observed_state="unavailable", last_error=CRASHED_NOTE)) + ) + self.assertFalse( + is_crashed(row(observed_state="unavailable", last_error="actor missing")) + ) + self.assertFalse(is_crashed(row())) + + +if __name__ == "__main__": + unittest.main() diff --git a/backend/tests/runtime/test_workspace_api.py b/backend/tests/runtime/test_workspace_api.py new file mode 100644 index 0000000..38b02ef --- /dev/null +++ b/backend/tests/runtime/test_workspace_api.py @@ -0,0 +1,153 @@ +"""Workspace API projection and ownership behavior with adapter fakes.""" + +import unittest +from datetime import UTC, datetime +from unittest.mock import AsyncMock, patch + +from fastapi import HTTPException +from mainloop.runtime import workspace_api +from mainloop.runtime.contracts import ContractError + +from models import ( + WorkspaceAgentKind, + WorkspaceDesiredState, + WorkspaceLifecycle, + WorkspaceManifest, + WorkspaceObservedState, +) + + +def lifecycle() -> WorkspaceLifecycle: + return WorkspaceLifecycle( + workspace_id="session-1", + session_id="session-1", + desired_state=WorkspaceDesiredState.RUNNING, + observed_state=WorkspaceObservedState.RUNNING, + manifest=WorkspaceManifest( + repo_url="https://github.com/example/repo", + branch="main", + agent_kinds=(WorkspaceAgentKind.CODEX,), + skills=("skill://review",), + mcp_servers=("mcp://docs",), + egress_allowlist=("github.com",), + resource_class="small", + ), + ownership_generation=4, + updated_at=datetime(2026, 9, 24, tzinfo=UTC), + ) + + +class WorkspaceApiTests(unittest.IsolatedAsyncioTestCase): + def test_routes_expose_the_workspace_lifecycle_api(self): + route_methods = { + (route.path, method) + for route in workspace_api.router.routes + for method in route.methods or () + } + self.assertTrue( + { + ("/workspaces", "GET"), + ("/workspaces", "POST"), + ("/workspaces/{workspace_id}", "GET"), + ("/workspaces/{workspace_id}", "DELETE"), + ("/workspaces/{workspace_id}/suspend", "POST"), + ("/workspaces/{workspace_id}/resume", "POST"), + ("/workspaces/{workspace_id}/refresh", "POST"), + ("/workspaces/{workspace_id}/touch", "POST"), + }.issubset(route_methods) + ) + + async def test_list_returns_lifecycle_and_manifest(self): + with patch.object( + workspace_api.workspace_adapter, + "list_workspace_lifecycles", + new=AsyncMock(return_value=[lifecycle()]), + ) as list_workspaces: + response = await workspace_api.list_workspaces(user_id="owner-1") + + self.assertEqual(response[0].observed_state, WorkspaceObservedState.RUNNING) + self.assertEqual(response[0].manifest.agent_kinds, (WorkspaceAgentKind.CODEX,)) + list_workspaces.assert_awaited_once_with("owner-1") + + async def test_get_detail_and_lifecycle_actions_publish_the_lifecycle(self): + current = lifecycle() + with ( + patch.object(workspace_api, "_require_owned_workspace", new=AsyncMock()), + patch.object( + workspace_api.workspace_adapter, + "ensure_workspace_lifecycle", + new=AsyncMock(return_value=current), + ), + patch.object( + workspace_api.workspace_adapter, + "suspend_workspace", + new=AsyncMock(return_value=current), + ), + patch.object( + workspace_api.workspace_adapter, + "resume_workspace", + new=AsyncMock(return_value=current), + ), + patch.object( + workspace_api.workspace_adapter, + "refresh_workspace_lifecycle", + new=AsyncMock(return_value=current), + ), + patch.object(workspace_api, "_publish", new=AsyncMock()) as publish, + ): + detail = await workspace_api.get_workspace("session-1", user_id="owner-1") + suspended = await workspace_api.suspend_workspace( + "session-1", user_id="owner-1" + ) + resumed = await workspace_api.resume_workspace( + "session-1", user_id="owner-1" + ) + refreshed = await workspace_api.refresh_workspace( + "session-1", user_id="owner-1" + ) + + self.assertEqual(detail.workspace_id, "session-1") + self.assertEqual(suspended.workspace_id, "session-1") + self.assertEqual(resumed.workspace_id, "session-1") + self.assertEqual(refreshed.workspace_id, "session-1") + self.assertEqual(suspended.manifest.resource_class, "small") + self.assertEqual(publish.await_count, 3) + + async def test_fence_error_is_a_conflict_and_preserves_reason(self): + current = lifecycle() + with ( + patch.object(workspace_api, "_require_owned_workspace", new=AsyncMock()), + patch.object( + workspace_api.workspace_adapter, + "suspend_workspace", + new=AsyncMock( + side_effect=ContractError("A recorded delivery is still open.") + ), + ), + patch.object( + workspace_api.workspace_adapter, + "get_workspace_lifecycle", + new=AsyncMock(return_value=current), + ), + patch.object(workspace_api, "_publish", new=AsyncMock()), + ): + with self.assertRaises(HTTPException) as raised: + await workspace_api.suspend_workspace("session-1", user_id="owner-1") + + self.assertEqual(raised.exception.status_code, 409) + self.assertEqual(raised.exception.detail, "A recorded delivery is still open.") + + async def test_owner_check_hides_other_users_workspaces(self): + with patch.object( + workspace_api, + "_require_owned_workspace", + new=AsyncMock(side_effect=HTTPException(404, "Workspace not found")), + ): + with self.assertRaises(HTTPException) as raised: + await workspace_api.get_workspace("session-1", user_id="other") + + self.assertEqual(raised.exception.status_code, 404) + + +if __name__ == "__main__": + unittest.main() diff --git a/backend/tests/runtime/test_workspace_dev_manifest.py b/backend/tests/runtime/test_workspace_dev_manifest.py new file mode 100644 index 0000000..e319164 --- /dev/null +++ b/backend/tests/runtime/test_workspace_dev_manifest.py @@ -0,0 +1,149 @@ +"""Strict dev manifest validation and fake actor provisioning.""" + +import base64 +import unittest +from unittest.mock import AsyncMock, Mock, patch + +from mainloop.config import settings +from mainloop.runtime.actor_provisioner import ( + FakeActorProvisioner, + SubstrateActorProvisioner, +) +from mainloop.runtime.substrate import ActorRecord, ActorState +from pydantic import ValidationError + +from models import WorkspaceManifest + + +def manifest(dev: dict) -> WorkspaceManifest: + return WorkspaceManifest( + branch="feature/dev-env", + resource_class="default", + dev=dev, + ) + + +class WorkspaceDevManifestTests(unittest.TestCase): + def test_accepts_image_services_ports_and_timeout(self): + result = manifest( + { + "image": "node:22", + "actor_template": "sample", + "services": [ + { + "name": "postgres", + "image": "postgres:16", + "env": {"POSTGRES_DB": "workspace"}, + "ports": [5432], + } + ], + "ports": [{"name": "app", "number": 3000, "protocol": "http"}], + "idle_timeout_minutes": 45, + } + ) + + self.assertEqual(result.dev.image, "node:22") + self.assertEqual(result.dev.services[0].ports, (5432,)) + self.assertEqual(result.dev.ports[0].number, 3000) + self.assertEqual(result.dev.idle_timeout_minutes, 45) + + def test_requires_exactly_one_image_source(self): + for dev in ({}, {"image": "node:22", "devcontainer_ref": "ghcr.io/dev"}): + with self.subTest(dev=dev), self.assertRaises(ValidationError): + manifest(dev) + + def test_rejects_unknown_fields_and_duplicate_ports(self): + with self.assertRaisesRegex(ValidationError, "extra_forbidden"): + manifest({"image": "node:22", "surprise": True}) + with self.assertRaisesRegex(ValidationError, "unique"): + manifest( + { + "image": "node:22", + "ports": [ + {"name": "app", "number": 3000}, + {"name": "web", "number": 3000}, + ], + } + ) + + def test_rejects_out_of_range_timeout_and_duplicate_service_names(self): + with self.assertRaises(ValidationError): + manifest({"image": "node:22", "idle_timeout_minutes": 0}) + with self.assertRaisesRegex(ValidationError, "unique"): + manifest( + { + "image": "node:22", + "services": [ + {"name": "db", "image": "postgres:16"}, + {"name": "db", "image": "redis:7"}, + ], + } + ) + + def test_rejects_non_strict_timeout(self): + with self.assertRaises(ValidationError): + manifest({"image": "node:22", "idle_timeout_minutes": "30"}) + + +class FakeProvisionerTests(unittest.IsolatedAsyncioTestCase): + async def test_fake_creates_and_deletes_actor_and_secret(self): + provisioner = FakeActorProvisioner() + # Test value is a Kubernetes Secret name, not credential material. + created = await provisioner.create( # nosec B106 + atespace="workspaces", + actor_name="ml-feature-1", + template="sample-template", + shim_token_secret_name="ml-feature-1-shim", + ) + + self.assertEqual(created.actor.name, "ml-feature-1") + self.assertIn("ml-feature-1-shim", provisioner.secrets) + await provisioner.delete( + atespace="workspaces", + actor_name="ml-feature-1", + shim_token_secret_name=created.shim_token_secret_name, + ) + self.assertFalse(provisioner.actors) + self.assertFalse(provisioner.secrets) + + async def test_substrate_provisioner_stores_a_random_token_in_secret(self): + actor = ActorRecord( + atespace="workspaces", + name="ml-branch-1", + uid="actor-1", + state=ActorState.RUNNING, + external_snapshot_uri=None, + current_actor_template_uid="template-1", + raw={}, + ) + control = Mock() + control.get_actor = AsyncMock(return_value=actor) + control.create_actor = AsyncMock() + core_api = Mock() + provisioner = SubstrateActorProvisioner(control=control, core_api=core_api) + + with patch( + "mainloop.runtime.actor_provisioner.secrets.token_urlsafe", + return_value="private-token", + ): + # Test value is a Kubernetes Secret name, not credential material. + result = await provisioner.create( # nosec B106 + atespace="workspaces", + actor_name="ml-branch-1", + template="project-template", + shim_token_secret_name="ml-branch-1-shim", + ) + + secret = core_api.create_namespaced_secret.call_args.args[1] + self.assertEqual(base64.b64decode(secret.data["token"]), b"private-token") + self.assertEqual( + core_api.create_namespaced_secret.call_args.args[0], + settings.substrate_shim_secret_namespace, + ) + self.assertEqual(result.actor, actor) + self.assertEqual(result.shim_token_secret_name, "ml-branch-1-shim") + control.create_actor.assert_not_awaited() + + +if __name__ == "__main__": + unittest.main() diff --git a/backend/tests/runtime/test_workspace_idle.py b/backend/tests/runtime/test_workspace_idle.py new file mode 100644 index 0000000..7a21163 --- /dev/null +++ b/backend/tests/runtime/test_workspace_idle.py @@ -0,0 +1,173 @@ +"""Durable activity touches and the periodic idle policy remain fake-backed.""" + +import asyncio +import unittest +from contextlib import asynccontextmanager +from types import SimpleNamespace +from unittest.mock import AsyncMock, patch + +from mainloop.runtime import native_sessions +from mainloop.runtime import workspace_adapter as adapter +from mainloop.runtime.contracts import ContractError + +from models import ( + WorkspaceDesiredState, + WorkspaceLifecycle, + WorkspaceManifest, + WorkspaceObservedState, +) + + +def lifecycle(state=WorkspaceObservedState.SUSPENDED): + from datetime import UTC, datetime + + return WorkspaceLifecycle( + workspace_id="workspace-1", + session_id="workspace-1", + desired_state=WorkspaceDesiredState.SUSPENDED, + observed_state=state, + manifest=WorkspaceManifest( + branch="feature/sample", + resource_class="default", + dev={"image": "node:22"}, + ), + updated_at=datetime.now(UTC), + ) + + +class FakeConnection: + def __init__(self, rows=()): + self.rows = list(rows) + self.executed = [] + self.query = "" + + async def execute(self, query, *args): + self.executed.append((query, args)) + + @asynccontextmanager + async def transaction(self): + yield self + + async def fetchrow(self, query, *_args): + if "FROM workspace_bindings" in query: + return {"workspace_id": "workspace-1", "ownership_generation": 3} + if "FROM workspace_lifecycles" in query and "last_activity_at" in query: + from datetime import UTC, datetime + + return { + "last_activity_at": datetime.now(UTC), + "last_delivery_at": None, + } + if "FROM workspace_lifecycles" in query: + return {"desired_state": "suspended", "observed_state": "suspended"} + raise AssertionError(f"unexpected query: {query}") + + async def fetch(self, query, *args): + self.query = query + return self.rows + + +def fake_connection(connection): + @asynccontextmanager + async def connect(): + yield connection + + return connect + + +class WorkspaceIdleTests(unittest.IsolatedAsyncioTestCase): + async def test_native_turn_touches_a_branch_workspace_before_recording(self): + with ( + patch.object( + native_sessions.db, + "get_session", + new=AsyncMock( + return_value=SimpleNamespace(status="active", conversation_id="c") + ), + ), + patch.object( + adapter, + "get_workspace", + new=AsyncMock(return_value={"actor_name": "ml-ws"}), + ), + patch.object(adapter, "touch_workspace", new=AsyncMock()) as touch, + patch.object(native_sessions, "_lock", return_value=asyncio.Lock()), + patch.object(native_sessions, "_open_count", new=AsyncMock(return_value=1)), + patch.object( + native_sessions, + "_record_delivery_message", + new=AsyncMock(return_value="message-1"), + ), + ): + message_id = await native_sessions.submit_message( + "workspace-1", "work", source="report" + ) + + self.assertEqual(message_id, "message-1") + touch.assert_awaited_once_with("workspace-1", reason="turn") + + async def test_turn_touch_records_activity_and_wakes_a_parked_workspace(self): + connection = FakeConnection() + current = lifecycle() + resumed = lifecycle(WorkspaceObservedState.RUNNING) + with ( + patch.object( + adapter, + "ensure_workspace_lifecycle", + new=AsyncMock(return_value=current), + ), + patch.object(adapter.db, "connection", new=fake_connection(connection)), + patch.object( + adapter, "resume_workspace", new=AsyncMock(return_value=resumed) + ) as wake, + ): + result = await adapter.touch_workspace("workspace-1", reason="turn") + + self.assertEqual(result.observed_state, WorkspaceObservedState.RUNNING) + self.assertIn("last_activity_at=NOW()", connection.executed[0][0]) + wake.assert_awaited_once_with("workspace-1") + + async def test_idle_reservation_rechecks_activity_under_the_binding_lock(self): + current = lifecycle(WorkspaceObservedState.RUNNING) + current = current.model_copy( + update={ + "desired_state": WorkspaceDesiredState.RUNNING, + "ownership_generation": 3, + } + ) + connection = FakeConnection() + with patch.object(adapter.db, "connection", new=fake_connection(connection)): + result = await adapter._reserve_operation( + current, + WorkspaceDesiredState.SUSPENDED, + only_if_idle=True, + ) + + self.assertIsNone(result) + self.assertFalse(connection.executed) + + async def test_touch_rejects_unknown_activity_reasons(self): + with self.assertRaisesRegex(ValueError, "reason must be"): + await adapter.touch_workspace("workspace-1", reason="browser") + + async def test_idle_scan_uses_durable_activity_and_fenced_suspend(self): + connection = FakeConnection( + [{"workspace_id": "workspace-1"}, {"workspace_id": "workspace-2"}] + ) + suspend = AsyncMock( + side_effect=[lifecycle(), ContractError("delivery still open")] + ) + with ( + patch.object(adapter.db, "connection", new=fake_connection(connection)), + patch.object(adapter, "suspend_workspace_if_idle", new=suspend), + ): + count = await adapter.suspend_idle_workspaces() + + self.assertIn("last_activity_at", connection.query) + self.assertIn("idle_timeout_minutes", connection.query) + self.assertEqual(count, 1) + self.assertEqual(suspend.await_count, 2) + + +if __name__ == "__main__": + unittest.main() diff --git a/backend/tests/runtime/test_workspace_lifecycle.py b/backend/tests/runtime/test_workspace_lifecycle.py new file mode 100644 index 0000000..fea49fe --- /dev/null +++ b/backend/tests/runtime/test_workspace_lifecycle.py @@ -0,0 +1,364 @@ +"""Workspace lifecycle policy and orchestration tests; no Substrate cluster is contacted.""" + +import asyncio +import unittest +from contextlib import asynccontextmanager +from datetime import UTC, datetime +from unittest.mock import AsyncMock, patch + +from mainloop.runtime import workspace_adapter as adapter +from mainloop.runtime.contracts import ContractError +from mainloop.runtime.substrate import ActorRecord, ActorState, TransportError + +from models import ( + WorkspaceDesiredState, + WorkspaceLifecycle, + WorkspaceManifest, + WorkspaceObservedState, +) + +NOW = datetime(2026, 9, 24, tzinfo=UTC) + + +def lifecycle( + *, + desired: WorkspaceDesiredState = WorkspaceDesiredState.RUNNING, + observed: WorkspaceObservedState = WorkspaceObservedState.RUNNING, + operation_id: str | None = None, +) -> WorkspaceLifecycle: + return WorkspaceLifecycle( + workspace_id="session-1", + session_id="session-1", + desired_state=desired, + observed_state=observed, + manifest=WorkspaceManifest( + repo_url="https://github.com/example/repo", + branch="main", + resource_class="default", + ), + operation_id=operation_id, + ownership_generation=3, + updated_at=NOW, + ) + + +def actor(state: ActorState, snapshot: str | None = None) -> ActorRecord: + return ActorRecord( + atespace="workspaces", + name="actor-1", + uid="uid-1", + state=state, + external_snapshot_uri=snapshot, + current_actor_template_uid="template-1", + raw={}, + ) + + +class FakeTransaction: + def __init__(self, connection): + self.connection = connection + + async def __aenter__(self): + self.connection.in_transaction = True + self.connection.events.append("begin") + return self.connection + + async def __aexit__(self, exc_type, exc, traceback): + self.connection.events.append("rollback" if exc_type else "commit") + self.connection.in_transaction = False + + +class FakePostgresConnection: + def __init__(self, delivery_states=()): + self.delivery_states = delivery_states + self.events = [] + self.in_transaction = False + self.lock_query = None + + def transaction(self): + return FakeTransaction(self) + + async def fetchrow(self, query, *_args): + if not self.in_transaction: + raise AssertionError( + "workspace binding lock must be inside the transaction" + ) + self.events.append("lock") + self.lock_query = query + return {"ownership_generation": 3} + + async def fetch(self, query, *_args): + if not self.in_transaction: + raise AssertionError("delivery fence must be inside the transaction") + if "native_deliveries" not in query: + raise AssertionError("expected native delivery state query") + self.events.append("delivery-check") + return [{"state": state} for state in self.delivery_states] + + async def execute(self, query, *_args): + if not self.in_transaction: + raise AssertionError("workspace reservation must be inside the transaction") + if "UPDATE workspace_bindings" in query: + self.events.append("reserve") + elif "UPDATE workspace_lifecycles" in query: + self.events.append("lifecycle") + return "UPDATE 1" + + +def fake_db_connection(connection): + @asynccontextmanager + async def connect(): + yield connection + + return connect + + +class WorkspaceStateMappingTests(unittest.TestCase): + def test_actor_states_map_without_guessing_transitions(self): + expected = { + ActorState.RUNNING: WorkspaceObservedState.RUNNING, + ActorState.SUSPENDING: WorkspaceObservedState.SUSPENDING, + ActorState.SUSPENDED: WorkspaceObservedState.SUSPENDED, + ActorState.RESUMING: WorkspaceObservedState.RESUMING, + ActorState.CRASHED: WorkspaceObservedState.FAILED, + ActorState.DELETING: WorkspaceObservedState.FAILED, + ActorState.PAUSED: WorkspaceObservedState.UNKNOWN, + ActorState.PAUSING: WorkspaceObservedState.UNKNOWN, + ActorState.REVERTING: WorkspaceObservedState.UNKNOWN, + ActorState.UNSPECIFIED: WorkspaceObservedState.UNKNOWN, + } + for actor_state, workspace_state in expected.items(): + with self.subTest(actor_state=actor_state): + self.assertEqual(adapter.lifecycle_state(actor_state), workspace_state) + + def test_suspend_fence_blocks_open_and_uncertain_deliveries(self): + self.assertIsNone(adapter.suspend_fence_reason(set())) + self.assertEqual(adapter.suspend_fence_reason({"recorded"}), "DeliveryRecorded") + self.assertEqual(adapter.suspend_fence_reason({"sending"}), "TurnInFlight") + self.assertEqual(adapter.suspend_fence_reason({"delivered"}), "TurnInFlight") + self.assertEqual( + adapter.suspend_fence_reason({"uncertain"}), "DeliveryUncertain" + ) + self.assertEqual(adapter.suspend_fence_reason({"queued"}), "DeliveryQueued") + + +class WorkspaceOperationTests(unittest.IsolatedAsyncioTestCase): + async def test_resume_is_idempotent_when_actor_is_already_running(self): + stored = lifecycle( + desired=WorkspaceDesiredState.SUSPENDED, + observed=WorkspaceObservedState.SUSPENDED, + ) + resumed = lifecycle() + control = AsyncMock() + control.get_actor.return_value = actor(ActorState.RUNNING) + + with ( + patch.object(adapter, "_lock", return_value=asyncio.Lock()), + patch.object( + adapter, + "ensure_workspace_lifecycle", + new=AsyncMock(return_value=stored), + ), + patch.object( + adapter, + "get_workspace", + new=AsyncMock( + return_value={ + "atespace": "workspaces", + "actor_name": "actor-1", + } + ), + ), + patch.object( + adapter, "_reserve_operation", new=AsyncMock(return_value=stored) + ), + patch.object( + adapter, "_record_observation", new=AsyncMock(return_value=resumed) + ), + ): + result = await adapter.resume_workspace("session-1", control=control) + + self.assertEqual(result.observed_state, WorkspaceObservedState.RUNNING) + control.get_actor.assert_awaited_once_with("workspaces", "actor-1") + control.resume_actor.assert_not_awaited() + + async def test_recorded_delivery_refuses_suspend_before_control_call(self): + stored = lifecycle() + control = AsyncMock() + connection = FakePostgresConnection({"recorded"}) + with ( + patch.object(adapter, "_lock", return_value=asyncio.Lock()), + patch.object(adapter.db, "connection", new=fake_db_connection(connection)), + patch.object( + adapter, + "ensure_workspace_lifecycle", + new=AsyncMock(return_value=stored), + ), + patch.object( + adapter, + "get_workspace", + new=AsyncMock( + return_value={ + "atespace": "workspaces", + "actor_name": "actor-1", + } + ), + ), + ): + with self.assertRaisesRegex(ContractError, "recorded delivery"): + await adapter.suspend_workspace("session-1", control=control) + + self.assertIn("FOR UPDATE", connection.lock_query) + self.assertEqual( + connection.events, + ["begin", "lock", "delivery-check", "lifecycle", "commit"], + ) + control.get_actor.assert_not_awaited() + control.suspend_actor.assert_not_awaited() + + async def test_reservation_and_delivery_fence_share_the_locked_transaction(self): + connection = FakePostgresConnection() + with ( + patch.object(adapter.db, "connection", new=fake_db_connection(connection)), + patch.object( + adapter, + "get_workspace_lifecycle", + new=AsyncMock(return_value=None), + ), + ): + result = await adapter._reserve_operation( + lifecycle(), WorkspaceDesiredState.SUSPENDED + ) + + self.assertEqual(result.desired_state, WorkspaceDesiredState.SUSPENDED) + self.assertIn("FOR UPDATE", connection.lock_query) + self.assertEqual( + connection.events, + ["begin", "lock", "delivery-check", "reserve", "lifecycle", "commit"], + ) + + async def test_delivery_recorded_after_reservation_blocks_suspend_call(self): + stored = lifecycle() + reserved = lifecycle( + desired=WorkspaceDesiredState.SUSPENDED, + observed=WorkspaceObservedState.SUSPENDING, + operation_id="op-1", + ) + control = AsyncMock() + events = [] + delivery_recorded = False + + async def reserve_operation(_previous, _desired_state): + nonlocal delivery_recorded + events.append("reserved") + delivery_recorded = True + return reserved + + async def inspect_actor(*_args): + events.append("actor-inspected") + return actor(ActorState.RUNNING) + + async def delivery_states(_workspace_id, **_kwargs): + events.append("pre-suspend-fence") + return {"recorded"} if delivery_recorded else set() + + control.get_actor.side_effect = inspect_actor + record_observation = AsyncMock(return_value=stored) + with ( + patch.object(adapter, "_lock", return_value=asyncio.Lock()), + patch.object( + adapter, + "ensure_workspace_lifecycle", + new=AsyncMock(return_value=stored), + ), + patch.object( + adapter, + "get_workspace", + new=AsyncMock( + return_value={ + "atespace": "workspaces", + "actor_name": "actor-1", + } + ), + ), + patch.object( + adapter, + "_reserve_operation", + new=AsyncMock(side_effect=reserve_operation), + ), + patch.object( + adapter, + "_delivery_states", + new=AsyncMock(side_effect=delivery_states), + ), + patch.object(adapter, "_record_observation", new=record_observation), + ): + with self.assertRaisesRegex(ContractError, "recorded delivery"): + await adapter.suspend_workspace("session-1", control=control) + + self.assertEqual( + events, + ["reserved", "actor-inspected", "pre-suspend-fence"], + ) + self.assertEqual( + record_observation.await_args.kwargs["operation_reason"], + "DeliveryRecorded", + ) + control.suspend_actor.assert_not_awaited() + + async def test_suspend_timeout_persists_unknown_and_keeps_operation_id(self): + stored = lifecycle() + reserved = lifecycle( + desired=WorkspaceDesiredState.SUSPENDED, + observed=WorkspaceObservedState.SUSPENDING, + operation_id="op-1", + ) + uncertain = lifecycle( + desired=WorkspaceDesiredState.SUSPENDED, + observed=WorkspaceObservedState.UNKNOWN, + operation_id="op-1", + ) + control = AsyncMock() + control.get_actor.return_value = actor(ActorState.RUNNING) + control.suspend_actor.side_effect = TransportError("timed out") + + with ( + patch.object(adapter, "_lock", return_value=asyncio.Lock()), + patch.object( + adapter, + "ensure_workspace_lifecycle", + new=AsyncMock(return_value=stored), + ), + patch.object( + adapter, + "get_workspace", + new=AsyncMock( + return_value={ + "atespace": "workspaces", + "actor_name": "actor-1", + } + ), + ), + patch.object( + adapter, "_delivery_states", new=AsyncMock(return_value=set()) + ), + patch.object( + adapter, "_reserve_operation", new=AsyncMock(return_value=reserved) + ), + patch.object( + adapter, + "_record_operation_failure", + new=AsyncMock(return_value=uncertain), + ) as record_failure, + ): + result = await adapter.suspend_workspace("session-1", control=control) + + self.assertEqual(result.observed_state, WorkspaceObservedState.UNKNOWN) + self.assertEqual(result.operation_id, "op-1") + record_failure.assert_awaited_once() + self.assertTrue(record_failure.await_args.kwargs["uncertain"]) + control.suspend_actor.assert_awaited_once_with("workspaces", "actor-1") + + +if __name__ == "__main__": + unittest.main() diff --git a/backend/tests/runtime/test_workspace_provisioning_api.py b/backend/tests/runtime/test_workspace_provisioning_api.py new file mode 100644 index 0000000..7516774 --- /dev/null +++ b/backend/tests/runtime/test_workspace_provisioning_api.py @@ -0,0 +1,279 @@ +"""Project ownership and actor provisioning orchestration use fakes.""" + +import json +import unittest +from contextlib import asynccontextmanager +from datetime import UTC, datetime +from unittest.mock import AsyncMock, patch + +from fastapi import HTTPException +from mainloop.runtime import workspace_api +from mainloop.runtime.actor_provisioner import FakeActorProvisioner, ProvisionedActor +from mainloop.runtime.substrate import ActorRecord, ActorState + +from models import ( + WorkspaceDesiredState, + WorkspaceLifecycle, + WorkspaceManifest, + WorkspaceObservedState, +) + + +class FakeConnection: + def __init__(self, *, project=True): + self.project = project + self.statements = [] + self.native_binding = None + self.workspace_row = { + "atespace": "mainloop-workspaces", + "actor_name": "ml-workspace", + "shim_token_secret_name": "ml-workspace-shim", + "desired_state": "active", + "conversation_id": "conversation-1", + } + + @asynccontextmanager + async def transaction(self): + yield + + async def fetchrow(self, query, *_args): + if "FROM projects" in query: + return ( + {"id": "project-1", "html_url": "https://github.com/example/repo"} + if self.project + else None + ) + if "FROM main_threads" in query: + return None + if "FROM workspace_bindings" in query: + return self.workspace_row + if "FROM native_bindings b" in query: + if self.native_binding is None: + return None + return { + **self.native_binding, + "workspace_atespace": self.workspace_row["atespace"], + "workspace_actor_name": self.workspace_row["actor_name"], + "workspace_shim_token_secret_name": self.workspace_row[ + "shim_token_secret_name" + ], + } + raise AssertionError(f"unexpected query: {query}") + + async def execute(self, query, *args): + self.statements.append((query, args)) + if "INSERT INTO workspace_bindings" in query: + self.workspace_row = { + "atespace": args[1], + "actor_name": args[2], + "shim_token_secret_name": args[4], + } + if "INSERT INTO native_bindings" in query: + self.native_binding = { + "session_id": args[0], + "kind": args[1], + "agent_name": args[2], + "native_session_id": args[3], + "approval_policy": args[4], + } + + +def fake_connection(connection): + @asynccontextmanager + async def connect(): + yield connection + + return connect + + +def lifecycle(workspace_id: str, manifest: WorkspaceManifest) -> WorkspaceLifecycle: + return WorkspaceLifecycle( + workspace_id=workspace_id, + session_id=workspace_id, + desired_state=WorkspaceDesiredState.RUNNING, + observed_state=WorkspaceObservedState.RUNNING, + manifest=manifest, + updated_at=datetime.now(UTC), + ) + + +class WorkspaceProvisioningApiTests(unittest.IsolatedAsyncioTestCase): + async def test_create_persists_identity_and_provisions_one_actor(self): + connection = FakeConnection() + provisioner = FakeActorProvisioner() + manifest = WorkspaceManifest( + repo_url="https://github.com/example/repo", + branch="feature/one", + resource_class="default", + dev={"image": "node:22", "actor_template": "project-template"}, + ) + actor = ActorRecord( + atespace="mainloop-workspaces", + name="ml-workspace", + uid="actor-1", + state=ActorState.RUNNING, + external_snapshot_uri=None, + current_actor_template_uid="template-1", + raw={}, + ) + fake_create = AsyncMock( + return_value=ProvisionedActor(actor, "ml-workspace-shim") + ) + provisioner.create = fake_create + with ( + patch.object( + workspace_api.db, "connection", new=fake_connection(connection) + ), + patch.object( + workspace_api, "get_actor_provisioner", return_value=provisioner + ), + patch.object( + workspace_api.workspace_adapter, + "_record_observation", + new=AsyncMock( + side_effect=lambda workspace_id, **_kwargs: lifecycle( + workspace_id, manifest + ) + ), + ), + patch.object(workspace_api, "_publish", new=AsyncMock()), + ): + result = await workspace_api.create_workspace( + workspace_api.CreateWorkspaceRequest( + project_id="project-1", + branch="feature/one", + dev={"image": "node:22", "actor_template": "project-template"}, + ), + user_id="owner-1", + ) + + self.assertEqual(result.manifest.branch, "feature/one") + self.assertEqual(result.observed_state, WorkspaceObservedState.RUNNING) + self.assertTrue( + any( + "INSERT INTO workspace_bindings" in query + for query, _ in connection.statements + ) + ) + self.assertTrue( + any( + "INSERT INTO workspace_lifecycles" in query + for query, _ in connection.statements + ) + ) + self.assertEqual(connection.native_binding["session_id"], result.workspace_id) + self.assertEqual(connection.native_binding["kind"], "claude") + self.assertTrue(connection.native_binding["native_session_id"]) + lifecycle_insert = next( + args + for query, args in connection.statements + if "INSERT INTO workspace_lifecycles" in query + ) + self.assertEqual(json.loads(lifecycle_insert[1])["agent_kinds"], ["claude"]) + kwargs = fake_create.await_args.kwargs + self.assertEqual(kwargs["template"], "project-template") + self.assertEqual( + kwargs["shim_token_secret_name"], + workspace_api.settings.shim_token_secret_name( + workspace_api.settings.substrate_atespace, kwargs["actor_name"] + ), + ) + + async def test_create_hides_projects_owned_by_another_user(self): + connection = FakeConnection(project=False) + with patch.object( + workspace_api.db, "connection", new=fake_connection(connection) + ): + with self.assertRaises(HTTPException) as raised: + await workspace_api.create_workspace( + workspace_api.CreateWorkspaceRequest( + project_id="project-1", branch="main", dev={"image": "node:22"} + ), + user_id="other-owner", + ) + + self.assertEqual(raised.exception.status_code, 404) + self.assertFalse(connection.statements) + + async def test_delete_removes_actor_secret_and_workspace_records(self): + connection = FakeConnection() + provisioner = FakeActorProvisioner() + provisioner.actors[("mainloop-workspaces", "ml-workspace")] = ActorRecord( + atespace="mainloop-workspaces", + name="ml-workspace", + uid="actor-1", + state=ActorState.RUNNING, + external_snapshot_uri=None, + current_actor_template_uid="template-1", + raw={}, + ) + provisioner.secrets.add("ml-workspace-shim") + with ( + patch.object(workspace_api, "_require_owned_workspace", new=AsyncMock()), + patch.object( + workspace_api.db, "connection", new=fake_connection(connection) + ), + patch.object( + workspace_api.workspace_adapter, + "_delivery_states", + new=AsyncMock(return_value=set()), + ), + patch.object( + workspace_api, "get_actor_provisioner", return_value=provisioner + ), + ): + response = await workspace_api.delete_workspace( + "workspace-1", user_id="owner-1" + ) + + self.assertEqual(response.status_code, 204) + self.assertFalse(provisioner.actors) + self.assertFalse(provisioner.secrets) + self.assertTrue( + any( + "DELETE FROM workspace_lifecycles" in query + for query, _ in connection.statements + ) + ) + self.assertTrue( + any( + "DELETE FROM workspace_bindings" in query + for query, _ in connection.statements + ) + ) + self.assertTrue( + any( + "DELETE FROM native_deliveries" in query + for query, _ in connection.statements + ) + ) + self.assertTrue( + any("DELETE FROM messages" in query for query, _ in connection.statements) + ) + + async def test_delete_refuses_open_deliveries_before_actor_mutation(self): + connection = FakeConnection() + provisioner = FakeActorProvisioner() + with ( + patch.object(workspace_api, "_require_owned_workspace", new=AsyncMock()), + patch.object( + workspace_api.db, "connection", new=fake_connection(connection) + ), + patch.object( + workspace_api.workspace_adapter, + "_delivery_states", + new=AsyncMock(return_value={"sending"}), + ), + patch.object( + workspace_api, "get_actor_provisioner", return_value=provisioner + ), + ): + with self.assertRaises(HTTPException) as raised: + await workspace_api.delete_workspace("workspace-1", user_id="owner-1") + + self.assertEqual(raised.exception.status_code, 409) + self.assertFalse(provisioner.actors) + + +if __name__ == "__main__": + unittest.main() diff --git a/backend/uv.lock b/backend/uv.lock index ad6f8da..7ebed9a 100644 --- a/backend/uv.lock +++ b/backend/uv.lock @@ -1,5 +1,5 @@ version = 1 -revision = 1 +revision = 3 requires-python = ">=3.13" resolution-markers = [ "python_full_version >= '3.14'", @@ -13,18 +13,18 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pydantic" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/67/bb/5a5ec893eea5805fb9a3db76a9888c3429710dfb6f24bbb37568f2cf7320/ag_ui_protocol-0.1.10.tar.gz", hash = "sha256:3213991c6b2eb24bb1a8c362ee270c16705a07a4c5962267a083d0959ed894f4", size = 6945 } +sdist = { url = "https://files.pythonhosted.org/packages/67/bb/5a5ec893eea5805fb9a3db76a9888c3429710dfb6f24bbb37568f2cf7320/ag_ui_protocol-0.1.10.tar.gz", hash = "sha256:3213991c6b2eb24bb1a8c362ee270c16705a07a4c5962267a083d0959ed894f4", size = 6945, upload-time = "2025-11-06T15:17:17.068Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/8f/78/eb55fabaab41abc53f52c0918a9a8c0f747807e5306273f51120fd695957/ag_ui_protocol-0.1.10-py3-none-any.whl", hash = "sha256:c81e6981f30aabdf97a7ee312bfd4df0cd38e718d9fc10019c7d438128b93ab5", size = 7889 }, + { url = "https://files.pythonhosted.org/packages/8f/78/eb55fabaab41abc53f52c0918a9a8c0f747807e5306273f51120fd695957/ag_ui_protocol-0.1.10-py3-none-any.whl", hash = "sha256:c81e6981f30aabdf97a7ee312bfd4df0cd38e718d9fc10019c7d438128b93ab5", size = 7889, upload-time = "2025-11-06T15:17:15.325Z" }, ] [[package]] name = "aiohappyeyeballs" version = "2.6.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/26/30/f84a107a9c4331c14b2b586036f40965c128aa4fee4dda5d3d51cb14ad54/aiohappyeyeballs-2.6.1.tar.gz", hash = "sha256:c3f9d0113123803ccadfdf3f0faa505bc78e6a72d1cc4806cbd719826e943558", size = 22760 } +sdist = { url = "https://files.pythonhosted.org/packages/26/30/f84a107a9c4331c14b2b586036f40965c128aa4fee4dda5d3d51cb14ad54/aiohappyeyeballs-2.6.1.tar.gz", hash = "sha256:c3f9d0113123803ccadfdf3f0faa505bc78e6a72d1cc4806cbd719826e943558", size = 22760, upload-time = "2025-03-12T01:42:48.764Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/0f/15/5bf3b99495fb160b63f95972b81750f18f7f4e02ad051373b669d17d44f2/aiohappyeyeballs-2.6.1-py3-none-any.whl", hash = "sha256:f349ba8f4b75cb25c99c5c2d84e997e485204d2902a9597802b0371f09331fb8", size = 15265 }, + { url = "https://files.pythonhosted.org/packages/0f/15/5bf3b99495fb160b63f95972b81750f18f7f4e02ad051373b669d17d44f2/aiohappyeyeballs-2.6.1-py3-none-any.whl", hash = "sha256:f349ba8f4b75cb25c99c5c2d84e997e485204d2902a9597802b0371f09331fb8", size = 15265, upload-time = "2025-03-12T01:42:47.083Z" }, ] [[package]] @@ -40,59 +40,59 @@ dependencies = [ { name = "propcache" }, { name = "yarl" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/1c/ce/3b83ebba6b3207a7135e5fcaba49706f8a4b6008153b4e30540c982fae26/aiohttp-3.13.2.tar.gz", hash = "sha256:40176a52c186aefef6eb3cad2cdd30cd06e3afbe88fe8ab2af9c0b90f228daca", size = 7837994 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/bf/78/7e90ca79e5aa39f9694dcfd74f4720782d3c6828113bb1f3197f7e7c4a56/aiohttp-3.13.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:7519bdc7dfc1940d201651b52bf5e03f5503bda45ad6eacf64dda98be5b2b6be", size = 732139 }, - { url = "https://files.pythonhosted.org/packages/db/ed/1f59215ab6853fbaa5c8495fa6cbc39edfc93553426152b75d82a5f32b76/aiohttp-3.13.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:088912a78b4d4f547a1f19c099d5a506df17eacec3c6f4375e2831ec1d995742", size = 490082 }, - { url = "https://files.pythonhosted.org/packages/68/7b/fe0fe0f5e05e13629d893c760465173a15ad0039c0a5b0d0040995c8075e/aiohttp-3.13.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5276807b9de9092af38ed23ce120539ab0ac955547b38563a9ba4f5b07b95293", size = 489035 }, - { url = "https://files.pythonhosted.org/packages/d2/04/db5279e38471b7ac801d7d36a57d1230feeee130bbe2a74f72731b23c2b1/aiohttp-3.13.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1237c1375eaef0db4dcd7c2559f42e8af7b87ea7d295b118c60c36a6e61cb811", size = 1720387 }, - { url = "https://files.pythonhosted.org/packages/31/07/8ea4326bd7dae2bd59828f69d7fdc6e04523caa55e4a70f4a8725a7e4ed2/aiohttp-3.13.2-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:96581619c57419c3d7d78703d5b78c1e5e5fc0172d60f555bdebaced82ded19a", size = 1688314 }, - { url = "https://files.pythonhosted.org/packages/48/ab/3d98007b5b87ffd519d065225438cc3b668b2f245572a8cb53da5dd2b1bc/aiohttp-3.13.2-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a2713a95b47374169409d18103366de1050fe0ea73db358fc7a7acb2880422d4", size = 1756317 }, - { url = "https://files.pythonhosted.org/packages/97/3d/801ca172b3d857fafb7b50c7c03f91b72b867a13abca982ed6b3081774ef/aiohttp-3.13.2-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:228a1cd556b3caca590e9511a89444925da87d35219a49ab5da0c36d2d943a6a", size = 1858539 }, - { url = "https://files.pythonhosted.org/packages/f7/0d/4764669bdf47bd472899b3d3db91fffbe925c8e3038ec591a2fd2ad6a14d/aiohttp-3.13.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ac6cde5fba8d7d8c6ac963dbb0256a9854e9fafff52fbcc58fdf819357892c3e", size = 1739597 }, - { url = "https://files.pythonhosted.org/packages/c4/52/7bd3c6693da58ba16e657eb904a5b6decfc48ecd06e9ac098591653b1566/aiohttp-3.13.2-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f2bef8237544f4e42878c61cef4e2839fee6346dc60f5739f876a9c50be7fcdb", size = 1555006 }, - { url = "https://files.pythonhosted.org/packages/48/30/9586667acec5993b6f41d2ebcf96e97a1255a85f62f3c653110a5de4d346/aiohttp-3.13.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:16f15a4eac3bc2d76c45f7ebdd48a65d41b242eb6c31c2245463b40b34584ded", size = 1683220 }, - { url = "https://files.pythonhosted.org/packages/71/01/3afe4c96854cfd7b30d78333852e8e851dceaec1c40fd00fec90c6402dd2/aiohttp-3.13.2-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:bb7fb776645af5cc58ab804c58d7eba545a97e047254a52ce89c157b5af6cd0b", size = 1712570 }, - { url = "https://files.pythonhosted.org/packages/11/2c/22799d8e720f4697a9e66fd9c02479e40a49de3de2f0bbe7f9f78a987808/aiohttp-3.13.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:e1b4951125ec10c70802f2cb09736c895861cd39fd9dcb35107b4dc8ae6220b8", size = 1733407 }, - { url = "https://files.pythonhosted.org/packages/34/cb/90f15dd029f07cebbd91f8238a8b363978b530cd128488085b5703683594/aiohttp-3.13.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:550bf765101ae721ee1d37d8095f47b1f220650f85fe1af37a90ce75bab89d04", size = 1550093 }, - { url = "https://files.pythonhosted.org/packages/69/46/12dce9be9d3303ecbf4d30ad45a7683dc63d90733c2d9fe512be6716cd40/aiohttp-3.13.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:fe91b87fc295973096251e2d25a811388e7d8adf3bd2b97ef6ae78bc4ac6c476", size = 1758084 }, - { url = "https://files.pythonhosted.org/packages/f9/c8/0932b558da0c302ffd639fc6362a313b98fdf235dc417bc2493da8394df7/aiohttp-3.13.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e0c8e31cfcc4592cb200160344b2fb6ae0f9e4effe06c644b5a125d4ae5ebe23", size = 1716987 }, - { url = "https://files.pythonhosted.org/packages/5d/8b/f5bd1a75003daed099baec373aed678f2e9b34f2ad40d85baa1368556396/aiohttp-3.13.2-cp313-cp313-win32.whl", hash = "sha256:0740f31a60848d6edb296a0df827473eede90c689b8f9f2a4cdde74889eb2254", size = 425859 }, - { url = "https://files.pythonhosted.org/packages/5d/28/a8a9fc6957b2cee8902414e41816b5ab5536ecf43c3b1843c10e82c559b2/aiohttp-3.13.2-cp313-cp313-win_amd64.whl", hash = "sha256:a88d13e7ca367394908f8a276b89d04a3652044612b9a408a0bb22a5ed976a1a", size = 452192 }, - { url = "https://files.pythonhosted.org/packages/9b/36/e2abae1bd815f01c957cbf7be817b3043304e1c87bad526292a0410fdcf9/aiohttp-3.13.2-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:2475391c29230e063ef53a66669b7b691c9bfc3f1426a0f7bcdf1216bdbac38b", size = 735234 }, - { url = "https://files.pythonhosted.org/packages/ca/e3/1ee62dde9b335e4ed41db6bba02613295a0d5b41f74a783c142745a12763/aiohttp-3.13.2-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:f33c8748abef4d8717bb20e8fb1b3e07c6adacb7fd6beaae971a764cf5f30d61", size = 490733 }, - { url = "https://files.pythonhosted.org/packages/1a/aa/7a451b1d6a04e8d15a362af3e9b897de71d86feac3babf8894545d08d537/aiohttp-3.13.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ae32f24bbfb7dbb485a24b30b1149e2f200be94777232aeadba3eecece4d0aa4", size = 491303 }, - { url = "https://files.pythonhosted.org/packages/57/1e/209958dbb9b01174870f6a7538cd1f3f28274fdbc88a750c238e2c456295/aiohttp-3.13.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5d7f02042c1f009ffb70067326ef183a047425bb2ff3bc434ead4dd4a4a66a2b", size = 1717965 }, - { url = "https://files.pythonhosted.org/packages/08/aa/6a01848d6432f241416bc4866cae8dc03f05a5a884d2311280f6a09c73d6/aiohttp-3.13.2-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:93655083005d71cd6c072cdab54c886e6570ad2c4592139c3fb967bfc19e4694", size = 1667221 }, - { url = "https://files.pythonhosted.org/packages/87/4f/36c1992432d31bbc789fa0b93c768d2e9047ec8c7177e5cd84ea85155f36/aiohttp-3.13.2-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0db1e24b852f5f664cd728db140cf11ea0e82450471232a394b3d1a540b0f906", size = 1757178 }, - { url = "https://files.pythonhosted.org/packages/ac/b4/8e940dfb03b7e0f68a82b88fd182b9be0a65cb3f35612fe38c038c3112cf/aiohttp-3.13.2-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b009194665bcd128e23eaddef362e745601afa4641930848af4c8559e88f18f9", size = 1838001 }, - { url = "https://files.pythonhosted.org/packages/d7/ef/39f3448795499c440ab66084a9db7d20ca7662e94305f175a80f5b7e0072/aiohttp-3.13.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c038a8fdc8103cd51dbd986ecdce141473ffd9775a7a8057a6ed9c3653478011", size = 1716325 }, - { url = "https://files.pythonhosted.org/packages/d7/51/b311500ffc860b181c05d91c59a1313bdd05c82960fdd4035a15740d431e/aiohttp-3.13.2-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:66bac29b95a00db411cd758fea0e4b9bdba6d549dfe333f9a945430f5f2cc5a6", size = 1547978 }, - { url = "https://files.pythonhosted.org/packages/31/64/b9d733296ef79815226dab8c586ff9e3df41c6aff2e16c06697b2d2e6775/aiohttp-3.13.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:4ebf9cfc9ba24a74cf0718f04aac2a3bbe745902cc7c5ebc55c0f3b5777ef213", size = 1682042 }, - { url = "https://files.pythonhosted.org/packages/3f/30/43d3e0f9d6473a6db7d472104c4eff4417b1e9df01774cb930338806d36b/aiohttp-3.13.2-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:a4b88ebe35ce54205c7074f7302bd08a4cb83256a3e0870c72d6f68a3aaf8e49", size = 1680085 }, - { url = "https://files.pythonhosted.org/packages/16/51/c709f352c911b1864cfd1087577760ced64b3e5bee2aa88b8c0c8e2e4972/aiohttp-3.13.2-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:98c4fb90bb82b70a4ed79ca35f656f4281885be076f3f970ce315402b53099ae", size = 1728238 }, - { url = "https://files.pythonhosted.org/packages/19/e2/19bd4c547092b773caeb48ff5ae4b1ae86756a0ee76c16727fcfd281404b/aiohttp-3.13.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:ec7534e63ae0f3759df3a1ed4fa6bc8f75082a924b590619c0dd2f76d7043caa", size = 1544395 }, - { url = "https://files.pythonhosted.org/packages/cf/87/860f2803b27dfc5ed7be532832a3498e4919da61299b4a1f8eb89b8ff44d/aiohttp-3.13.2-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:5b927cf9b935a13e33644cbed6c8c4b2d0f25b713d838743f8fe7191b33829c4", size = 1742965 }, - { url = "https://files.pythonhosted.org/packages/67/7f/db2fc7618925e8c7a601094d5cbe539f732df4fb570740be88ed9e40e99a/aiohttp-3.13.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:88d6c017966a78c5265d996c19cdb79235be5e6412268d7e2ce7dee339471b7a", size = 1697585 }, - { url = "https://files.pythonhosted.org/packages/0c/07/9127916cb09bb38284db5036036042b7b2c514c8ebaeee79da550c43a6d6/aiohttp-3.13.2-cp314-cp314-win32.whl", hash = "sha256:f7c183e786e299b5d6c49fb43a769f8eb8e04a2726a2bd5887b98b5cc2d67940", size = 431621 }, - { url = "https://files.pythonhosted.org/packages/fb/41/554a8a380df6d3a2bba8a7726429a23f4ac62aaf38de43bb6d6cde7b4d4d/aiohttp-3.13.2-cp314-cp314-win_amd64.whl", hash = "sha256:fe242cd381e0fb65758faf5ad96c2e460df6ee5b2de1072fe97e4127927e00b4", size = 457627 }, - { url = "https://files.pythonhosted.org/packages/c7/8e/3824ef98c039d3951cb65b9205a96dd2b20f22241ee17d89c5701557c826/aiohttp-3.13.2-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:f10d9c0b0188fe85398c61147bbd2a657d616c876863bfeff43376e0e3134673", size = 767360 }, - { url = "https://files.pythonhosted.org/packages/a4/0f/6a03e3fc7595421274fa34122c973bde2d89344f8a881b728fa8c774e4f1/aiohttp-3.13.2-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:e7c952aefdf2460f4ae55c5e9c3e80aa72f706a6317e06020f80e96253b1accd", size = 504616 }, - { url = "https://files.pythonhosted.org/packages/c6/aa/ed341b670f1bc8a6f2c6a718353d13b9546e2cef3544f573c6a1ff0da711/aiohttp-3.13.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c20423ce14771d98353d2e25e83591fa75dfa90a3c1848f3d7c68243b4fbded3", size = 509131 }, - { url = "https://files.pythonhosted.org/packages/7f/f0/c68dac234189dae5c4bbccc0f96ce0cc16b76632cfc3a08fff180045cfa4/aiohttp-3.13.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e96eb1a34396e9430c19d8338d2ec33015e4a87ef2b4449db94c22412e25ccdf", size = 1864168 }, - { url = "https://files.pythonhosted.org/packages/8f/65/75a9a76db8364b5d0e52a0c20eabc5d52297385d9af9c35335b924fafdee/aiohttp-3.13.2-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:23fb0783bc1a33640036465019d3bba069942616a6a2353c6907d7fe1ccdaf4e", size = 1719200 }, - { url = "https://files.pythonhosted.org/packages/f5/55/8df2ed78d7f41d232f6bd3ff866b6f617026551aa1d07e2f03458f964575/aiohttp-3.13.2-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2e1a9bea6244a1d05a4e57c295d69e159a5c50d8ef16aa390948ee873478d9a5", size = 1843497 }, - { url = "https://files.pythonhosted.org/packages/e9/e0/94d7215e405c5a02ccb6a35c7a3a6cfff242f457a00196496935f700cde5/aiohttp-3.13.2-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0a3d54e822688b56e9f6b5816fb3de3a3a64660efac64e4c2dc435230ad23bad", size = 1935703 }, - { url = "https://files.pythonhosted.org/packages/0b/78/1eeb63c3f9b2d1015a4c02788fb543141aad0a03ae3f7a7b669b2483f8d4/aiohttp-3.13.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7a653d872afe9f33497215745da7a943d1dc15b728a9c8da1c3ac423af35178e", size = 1792738 }, - { url = "https://files.pythonhosted.org/packages/41/75/aaf1eea4c188e51538c04cc568040e3082db263a57086ea74a7d38c39e42/aiohttp-3.13.2-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:56d36e80d2003fa3fc0207fac644216d8532e9504a785ef9a8fd013f84a42c61", size = 1624061 }, - { url = "https://files.pythonhosted.org/packages/9b/c2/3b6034de81fbcc43de8aeb209073a2286dfb50b86e927b4efd81cf848197/aiohttp-3.13.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:78cd586d8331fb8e241c2dd6b2f4061778cc69e150514b39a9e28dd050475661", size = 1789201 }, - { url = "https://files.pythonhosted.org/packages/c9/38/c15dcf6d4d890217dae79d7213988f4e5fe6183d43893a9cf2fe9e84ca8d/aiohttp-3.13.2-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:20b10bbfbff766294fe99987f7bb3b74fdd2f1a2905f2562132641ad434dcf98", size = 1776868 }, - { url = "https://files.pythonhosted.org/packages/04/75/f74fd178ac81adf4f283a74847807ade5150e48feda6aef024403716c30c/aiohttp-3.13.2-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:9ec49dff7e2b3c85cdeaa412e9d438f0ecd71676fde61ec57027dd392f00c693", size = 1790660 }, - { url = "https://files.pythonhosted.org/packages/e7/80/7368bd0d06b16b3aba358c16b919e9c46cf11587dc572091031b0e9e3ef0/aiohttp-3.13.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:94f05348c4406450f9d73d38efb41d669ad6cd90c7ee194810d0eefbfa875a7a", size = 1617548 }, - { url = "https://files.pythonhosted.org/packages/7d/4b/a6212790c50483cb3212e507378fbe26b5086d73941e1ec4b56a30439688/aiohttp-3.13.2-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:fa4dcb605c6f82a80c7f95713c2b11c3b8e9893b3ebd2bc9bde93165ed6107be", size = 1817240 }, - { url = "https://files.pythonhosted.org/packages/ff/f7/ba5f0ba4ea8d8f3c32850912944532b933acbf0f3a75546b89269b9b7dde/aiohttp-3.13.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cf00e5db968c3f67eccd2778574cf64d8b27d95b237770aa32400bd7a1ca4f6c", size = 1762334 }, - { url = "https://files.pythonhosted.org/packages/7e/83/1a5a1856574588b1cad63609ea9ad75b32a8353ac995d830bf5da9357364/aiohttp-3.13.2-cp314-cp314t-win32.whl", hash = "sha256:d23b5fe492b0805a50d3371e8a728a9134d8de5447dce4c885f5587294750734", size = 464685 }, - { url = "https://files.pythonhosted.org/packages/9f/4d/d22668674122c08f4d56972297c51a624e64b3ed1efaa40187607a7cb66e/aiohttp-3.13.2-cp314-cp314t-win_amd64.whl", hash = "sha256:ff0a7b0a82a7ab905cbda74006318d1b12e37c797eb1b0d4eb3e316cf47f658f", size = 498093 }, +sdist = { url = "https://files.pythonhosted.org/packages/1c/ce/3b83ebba6b3207a7135e5fcaba49706f8a4b6008153b4e30540c982fae26/aiohttp-3.13.2.tar.gz", hash = "sha256:40176a52c186aefef6eb3cad2cdd30cd06e3afbe88fe8ab2af9c0b90f228daca", size = 7837994, upload-time = "2025-10-28T20:59:39.937Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bf/78/7e90ca79e5aa39f9694dcfd74f4720782d3c6828113bb1f3197f7e7c4a56/aiohttp-3.13.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:7519bdc7dfc1940d201651b52bf5e03f5503bda45ad6eacf64dda98be5b2b6be", size = 732139, upload-time = "2025-10-28T20:57:02.455Z" }, + { url = "https://files.pythonhosted.org/packages/db/ed/1f59215ab6853fbaa5c8495fa6cbc39edfc93553426152b75d82a5f32b76/aiohttp-3.13.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:088912a78b4d4f547a1f19c099d5a506df17eacec3c6f4375e2831ec1d995742", size = 490082, upload-time = "2025-10-28T20:57:04.784Z" }, + { url = "https://files.pythonhosted.org/packages/68/7b/fe0fe0f5e05e13629d893c760465173a15ad0039c0a5b0d0040995c8075e/aiohttp-3.13.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5276807b9de9092af38ed23ce120539ab0ac955547b38563a9ba4f5b07b95293", size = 489035, upload-time = "2025-10-28T20:57:06.894Z" }, + { url = "https://files.pythonhosted.org/packages/d2/04/db5279e38471b7ac801d7d36a57d1230feeee130bbe2a74f72731b23c2b1/aiohttp-3.13.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1237c1375eaef0db4dcd7c2559f42e8af7b87ea7d295b118c60c36a6e61cb811", size = 1720387, upload-time = "2025-10-28T20:57:08.685Z" }, + { url = "https://files.pythonhosted.org/packages/31/07/8ea4326bd7dae2bd59828f69d7fdc6e04523caa55e4a70f4a8725a7e4ed2/aiohttp-3.13.2-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:96581619c57419c3d7d78703d5b78c1e5e5fc0172d60f555bdebaced82ded19a", size = 1688314, upload-time = "2025-10-28T20:57:10.693Z" }, + { url = "https://files.pythonhosted.org/packages/48/ab/3d98007b5b87ffd519d065225438cc3b668b2f245572a8cb53da5dd2b1bc/aiohttp-3.13.2-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a2713a95b47374169409d18103366de1050fe0ea73db358fc7a7acb2880422d4", size = 1756317, upload-time = "2025-10-28T20:57:12.563Z" }, + { url = "https://files.pythonhosted.org/packages/97/3d/801ca172b3d857fafb7b50c7c03f91b72b867a13abca982ed6b3081774ef/aiohttp-3.13.2-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:228a1cd556b3caca590e9511a89444925da87d35219a49ab5da0c36d2d943a6a", size = 1858539, upload-time = "2025-10-28T20:57:14.623Z" }, + { url = "https://files.pythonhosted.org/packages/f7/0d/4764669bdf47bd472899b3d3db91fffbe925c8e3038ec591a2fd2ad6a14d/aiohttp-3.13.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ac6cde5fba8d7d8c6ac963dbb0256a9854e9fafff52fbcc58fdf819357892c3e", size = 1739597, upload-time = "2025-10-28T20:57:16.399Z" }, + { url = "https://files.pythonhosted.org/packages/c4/52/7bd3c6693da58ba16e657eb904a5b6decfc48ecd06e9ac098591653b1566/aiohttp-3.13.2-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f2bef8237544f4e42878c61cef4e2839fee6346dc60f5739f876a9c50be7fcdb", size = 1555006, upload-time = "2025-10-28T20:57:18.288Z" }, + { url = "https://files.pythonhosted.org/packages/48/30/9586667acec5993b6f41d2ebcf96e97a1255a85f62f3c653110a5de4d346/aiohttp-3.13.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:16f15a4eac3bc2d76c45f7ebdd48a65d41b242eb6c31c2245463b40b34584ded", size = 1683220, upload-time = "2025-10-28T20:57:20.241Z" }, + { url = "https://files.pythonhosted.org/packages/71/01/3afe4c96854cfd7b30d78333852e8e851dceaec1c40fd00fec90c6402dd2/aiohttp-3.13.2-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:bb7fb776645af5cc58ab804c58d7eba545a97e047254a52ce89c157b5af6cd0b", size = 1712570, upload-time = "2025-10-28T20:57:22.253Z" }, + { url = "https://files.pythonhosted.org/packages/11/2c/22799d8e720f4697a9e66fd9c02479e40a49de3de2f0bbe7f9f78a987808/aiohttp-3.13.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:e1b4951125ec10c70802f2cb09736c895861cd39fd9dcb35107b4dc8ae6220b8", size = 1733407, upload-time = "2025-10-28T20:57:24.37Z" }, + { url = "https://files.pythonhosted.org/packages/34/cb/90f15dd029f07cebbd91f8238a8b363978b530cd128488085b5703683594/aiohttp-3.13.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:550bf765101ae721ee1d37d8095f47b1f220650f85fe1af37a90ce75bab89d04", size = 1550093, upload-time = "2025-10-28T20:57:26.257Z" }, + { url = "https://files.pythonhosted.org/packages/69/46/12dce9be9d3303ecbf4d30ad45a7683dc63d90733c2d9fe512be6716cd40/aiohttp-3.13.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:fe91b87fc295973096251e2d25a811388e7d8adf3bd2b97ef6ae78bc4ac6c476", size = 1758084, upload-time = "2025-10-28T20:57:28.349Z" }, + { url = "https://files.pythonhosted.org/packages/f9/c8/0932b558da0c302ffd639fc6362a313b98fdf235dc417bc2493da8394df7/aiohttp-3.13.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e0c8e31cfcc4592cb200160344b2fb6ae0f9e4effe06c644b5a125d4ae5ebe23", size = 1716987, upload-time = "2025-10-28T20:57:30.233Z" }, + { url = "https://files.pythonhosted.org/packages/5d/8b/f5bd1a75003daed099baec373aed678f2e9b34f2ad40d85baa1368556396/aiohttp-3.13.2-cp313-cp313-win32.whl", hash = "sha256:0740f31a60848d6edb296a0df827473eede90c689b8f9f2a4cdde74889eb2254", size = 425859, upload-time = "2025-10-28T20:57:32.105Z" }, + { url = "https://files.pythonhosted.org/packages/5d/28/a8a9fc6957b2cee8902414e41816b5ab5536ecf43c3b1843c10e82c559b2/aiohttp-3.13.2-cp313-cp313-win_amd64.whl", hash = "sha256:a88d13e7ca367394908f8a276b89d04a3652044612b9a408a0bb22a5ed976a1a", size = 452192, upload-time = "2025-10-28T20:57:34.166Z" }, + { url = "https://files.pythonhosted.org/packages/9b/36/e2abae1bd815f01c957cbf7be817b3043304e1c87bad526292a0410fdcf9/aiohttp-3.13.2-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:2475391c29230e063ef53a66669b7b691c9bfc3f1426a0f7bcdf1216bdbac38b", size = 735234, upload-time = "2025-10-28T20:57:36.415Z" }, + { url = "https://files.pythonhosted.org/packages/ca/e3/1ee62dde9b335e4ed41db6bba02613295a0d5b41f74a783c142745a12763/aiohttp-3.13.2-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:f33c8748abef4d8717bb20e8fb1b3e07c6adacb7fd6beaae971a764cf5f30d61", size = 490733, upload-time = "2025-10-28T20:57:38.205Z" }, + { url = "https://files.pythonhosted.org/packages/1a/aa/7a451b1d6a04e8d15a362af3e9b897de71d86feac3babf8894545d08d537/aiohttp-3.13.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ae32f24bbfb7dbb485a24b30b1149e2f200be94777232aeadba3eecece4d0aa4", size = 491303, upload-time = "2025-10-28T20:57:40.122Z" }, + { url = "https://files.pythonhosted.org/packages/57/1e/209958dbb9b01174870f6a7538cd1f3f28274fdbc88a750c238e2c456295/aiohttp-3.13.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5d7f02042c1f009ffb70067326ef183a047425bb2ff3bc434ead4dd4a4a66a2b", size = 1717965, upload-time = "2025-10-28T20:57:42.28Z" }, + { url = "https://files.pythonhosted.org/packages/08/aa/6a01848d6432f241416bc4866cae8dc03f05a5a884d2311280f6a09c73d6/aiohttp-3.13.2-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:93655083005d71cd6c072cdab54c886e6570ad2c4592139c3fb967bfc19e4694", size = 1667221, upload-time = "2025-10-28T20:57:44.869Z" }, + { url = "https://files.pythonhosted.org/packages/87/4f/36c1992432d31bbc789fa0b93c768d2e9047ec8c7177e5cd84ea85155f36/aiohttp-3.13.2-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0db1e24b852f5f664cd728db140cf11ea0e82450471232a394b3d1a540b0f906", size = 1757178, upload-time = "2025-10-28T20:57:47.216Z" }, + { url = "https://files.pythonhosted.org/packages/ac/b4/8e940dfb03b7e0f68a82b88fd182b9be0a65cb3f35612fe38c038c3112cf/aiohttp-3.13.2-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b009194665bcd128e23eaddef362e745601afa4641930848af4c8559e88f18f9", size = 1838001, upload-time = "2025-10-28T20:57:49.337Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ef/39f3448795499c440ab66084a9db7d20ca7662e94305f175a80f5b7e0072/aiohttp-3.13.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c038a8fdc8103cd51dbd986ecdce141473ffd9775a7a8057a6ed9c3653478011", size = 1716325, upload-time = "2025-10-28T20:57:51.327Z" }, + { url = "https://files.pythonhosted.org/packages/d7/51/b311500ffc860b181c05d91c59a1313bdd05c82960fdd4035a15740d431e/aiohttp-3.13.2-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:66bac29b95a00db411cd758fea0e4b9bdba6d549dfe333f9a945430f5f2cc5a6", size = 1547978, upload-time = "2025-10-28T20:57:53.554Z" }, + { url = "https://files.pythonhosted.org/packages/31/64/b9d733296ef79815226dab8c586ff9e3df41c6aff2e16c06697b2d2e6775/aiohttp-3.13.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:4ebf9cfc9ba24a74cf0718f04aac2a3bbe745902cc7c5ebc55c0f3b5777ef213", size = 1682042, upload-time = "2025-10-28T20:57:55.617Z" }, + { url = "https://files.pythonhosted.org/packages/3f/30/43d3e0f9d6473a6db7d472104c4eff4417b1e9df01774cb930338806d36b/aiohttp-3.13.2-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:a4b88ebe35ce54205c7074f7302bd08a4cb83256a3e0870c72d6f68a3aaf8e49", size = 1680085, upload-time = "2025-10-28T20:57:57.59Z" }, + { url = "https://files.pythonhosted.org/packages/16/51/c709f352c911b1864cfd1087577760ced64b3e5bee2aa88b8c0c8e2e4972/aiohttp-3.13.2-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:98c4fb90bb82b70a4ed79ca35f656f4281885be076f3f970ce315402b53099ae", size = 1728238, upload-time = "2025-10-28T20:57:59.525Z" }, + { url = "https://files.pythonhosted.org/packages/19/e2/19bd4c547092b773caeb48ff5ae4b1ae86756a0ee76c16727fcfd281404b/aiohttp-3.13.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:ec7534e63ae0f3759df3a1ed4fa6bc8f75082a924b590619c0dd2f76d7043caa", size = 1544395, upload-time = "2025-10-28T20:58:01.914Z" }, + { url = "https://files.pythonhosted.org/packages/cf/87/860f2803b27dfc5ed7be532832a3498e4919da61299b4a1f8eb89b8ff44d/aiohttp-3.13.2-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:5b927cf9b935a13e33644cbed6c8c4b2d0f25b713d838743f8fe7191b33829c4", size = 1742965, upload-time = "2025-10-28T20:58:03.972Z" }, + { url = "https://files.pythonhosted.org/packages/67/7f/db2fc7618925e8c7a601094d5cbe539f732df4fb570740be88ed9e40e99a/aiohttp-3.13.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:88d6c017966a78c5265d996c19cdb79235be5e6412268d7e2ce7dee339471b7a", size = 1697585, upload-time = "2025-10-28T20:58:06.189Z" }, + { url = "https://files.pythonhosted.org/packages/0c/07/9127916cb09bb38284db5036036042b7b2c514c8ebaeee79da550c43a6d6/aiohttp-3.13.2-cp314-cp314-win32.whl", hash = "sha256:f7c183e786e299b5d6c49fb43a769f8eb8e04a2726a2bd5887b98b5cc2d67940", size = 431621, upload-time = "2025-10-28T20:58:08.636Z" }, + { url = "https://files.pythonhosted.org/packages/fb/41/554a8a380df6d3a2bba8a7726429a23f4ac62aaf38de43bb6d6cde7b4d4d/aiohttp-3.13.2-cp314-cp314-win_amd64.whl", hash = "sha256:fe242cd381e0fb65758faf5ad96c2e460df6ee5b2de1072fe97e4127927e00b4", size = 457627, upload-time = "2025-10-28T20:58:11Z" }, + { url = "https://files.pythonhosted.org/packages/c7/8e/3824ef98c039d3951cb65b9205a96dd2b20f22241ee17d89c5701557c826/aiohttp-3.13.2-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:f10d9c0b0188fe85398c61147bbd2a657d616c876863bfeff43376e0e3134673", size = 767360, upload-time = "2025-10-28T20:58:13.358Z" }, + { url = "https://files.pythonhosted.org/packages/a4/0f/6a03e3fc7595421274fa34122c973bde2d89344f8a881b728fa8c774e4f1/aiohttp-3.13.2-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:e7c952aefdf2460f4ae55c5e9c3e80aa72f706a6317e06020f80e96253b1accd", size = 504616, upload-time = "2025-10-28T20:58:15.339Z" }, + { url = "https://files.pythonhosted.org/packages/c6/aa/ed341b670f1bc8a6f2c6a718353d13b9546e2cef3544f573c6a1ff0da711/aiohttp-3.13.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c20423ce14771d98353d2e25e83591fa75dfa90a3c1848f3d7c68243b4fbded3", size = 509131, upload-time = "2025-10-28T20:58:17.693Z" }, + { url = "https://files.pythonhosted.org/packages/7f/f0/c68dac234189dae5c4bbccc0f96ce0cc16b76632cfc3a08fff180045cfa4/aiohttp-3.13.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e96eb1a34396e9430c19d8338d2ec33015e4a87ef2b4449db94c22412e25ccdf", size = 1864168, upload-time = "2025-10-28T20:58:20.113Z" }, + { url = "https://files.pythonhosted.org/packages/8f/65/75a9a76db8364b5d0e52a0c20eabc5d52297385d9af9c35335b924fafdee/aiohttp-3.13.2-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:23fb0783bc1a33640036465019d3bba069942616a6a2353c6907d7fe1ccdaf4e", size = 1719200, upload-time = "2025-10-28T20:58:22.583Z" }, + { url = "https://files.pythonhosted.org/packages/f5/55/8df2ed78d7f41d232f6bd3ff866b6f617026551aa1d07e2f03458f964575/aiohttp-3.13.2-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2e1a9bea6244a1d05a4e57c295d69e159a5c50d8ef16aa390948ee873478d9a5", size = 1843497, upload-time = "2025-10-28T20:58:24.672Z" }, + { url = "https://files.pythonhosted.org/packages/e9/e0/94d7215e405c5a02ccb6a35c7a3a6cfff242f457a00196496935f700cde5/aiohttp-3.13.2-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0a3d54e822688b56e9f6b5816fb3de3a3a64660efac64e4c2dc435230ad23bad", size = 1935703, upload-time = "2025-10-28T20:58:26.758Z" }, + { url = "https://files.pythonhosted.org/packages/0b/78/1eeb63c3f9b2d1015a4c02788fb543141aad0a03ae3f7a7b669b2483f8d4/aiohttp-3.13.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7a653d872afe9f33497215745da7a943d1dc15b728a9c8da1c3ac423af35178e", size = 1792738, upload-time = "2025-10-28T20:58:29.787Z" }, + { url = "https://files.pythonhosted.org/packages/41/75/aaf1eea4c188e51538c04cc568040e3082db263a57086ea74a7d38c39e42/aiohttp-3.13.2-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:56d36e80d2003fa3fc0207fac644216d8532e9504a785ef9a8fd013f84a42c61", size = 1624061, upload-time = "2025-10-28T20:58:32.529Z" }, + { url = "https://files.pythonhosted.org/packages/9b/c2/3b6034de81fbcc43de8aeb209073a2286dfb50b86e927b4efd81cf848197/aiohttp-3.13.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:78cd586d8331fb8e241c2dd6b2f4061778cc69e150514b39a9e28dd050475661", size = 1789201, upload-time = "2025-10-28T20:58:34.618Z" }, + { url = "https://files.pythonhosted.org/packages/c9/38/c15dcf6d4d890217dae79d7213988f4e5fe6183d43893a9cf2fe9e84ca8d/aiohttp-3.13.2-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:20b10bbfbff766294fe99987f7bb3b74fdd2f1a2905f2562132641ad434dcf98", size = 1776868, upload-time = "2025-10-28T20:58:38.835Z" }, + { url = "https://files.pythonhosted.org/packages/04/75/f74fd178ac81adf4f283a74847807ade5150e48feda6aef024403716c30c/aiohttp-3.13.2-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:9ec49dff7e2b3c85cdeaa412e9d438f0ecd71676fde61ec57027dd392f00c693", size = 1790660, upload-time = "2025-10-28T20:58:41.507Z" }, + { url = "https://files.pythonhosted.org/packages/e7/80/7368bd0d06b16b3aba358c16b919e9c46cf11587dc572091031b0e9e3ef0/aiohttp-3.13.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:94f05348c4406450f9d73d38efb41d669ad6cd90c7ee194810d0eefbfa875a7a", size = 1617548, upload-time = "2025-10-28T20:58:43.674Z" }, + { url = "https://files.pythonhosted.org/packages/7d/4b/a6212790c50483cb3212e507378fbe26b5086d73941e1ec4b56a30439688/aiohttp-3.13.2-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:fa4dcb605c6f82a80c7f95713c2b11c3b8e9893b3ebd2bc9bde93165ed6107be", size = 1817240, upload-time = "2025-10-28T20:58:45.787Z" }, + { url = "https://files.pythonhosted.org/packages/ff/f7/ba5f0ba4ea8d8f3c32850912944532b933acbf0f3a75546b89269b9b7dde/aiohttp-3.13.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cf00e5db968c3f67eccd2778574cf64d8b27d95b237770aa32400bd7a1ca4f6c", size = 1762334, upload-time = "2025-10-28T20:58:47.936Z" }, + { url = "https://files.pythonhosted.org/packages/7e/83/1a5a1856574588b1cad63609ea9ad75b32a8353ac995d830bf5da9357364/aiohttp-3.13.2-cp314-cp314t-win32.whl", hash = "sha256:d23b5fe492b0805a50d3371e8a728a9134d8de5447dce4c885f5587294750734", size = 464685, upload-time = "2025-10-28T20:58:50.642Z" }, + { url = "https://files.pythonhosted.org/packages/9f/4d/d22668674122c08f4d56972297c51a624e64b3ed1efaa40187607a7cb66e/aiohttp-3.13.2-cp314-cp314t-win_amd64.whl", hash = "sha256:ff0a7b0a82a7ab905cbda74006318d1b12e37c797eb1b0d4eb3e316cf47f658f", size = 498093, upload-time = "2025-10-28T20:58:52.782Z" }, ] [[package]] @@ -102,27 +102,27 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "frozenlist" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/61/62/06741b579156360248d1ec624842ad0edf697050bbaf7c3e46394e106ad1/aiosignal-1.4.0.tar.gz", hash = "sha256:f47eecd9468083c2029cc99945502cb7708b082c232f9aca65da147157b251c7", size = 25007 } +sdist = { url = "https://files.pythonhosted.org/packages/61/62/06741b579156360248d1ec624842ad0edf697050bbaf7c3e46394e106ad1/aiosignal-1.4.0.tar.gz", hash = "sha256:f47eecd9468083c2029cc99945502cb7708b082c232f9aca65da147157b251c7", size = 25007, upload-time = "2025-07-03T22:54:43.528Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/fb/76/641ae371508676492379f16e2fa48f4e2c11741bd63c48be4b12a6b09cba/aiosignal-1.4.0-py3-none-any.whl", hash = "sha256:053243f8b92b990551949e63930a839ff0cf0b0ebbe0597b0f3fb19e1a0fe82e", size = 7490 }, + { url = "https://files.pythonhosted.org/packages/fb/76/641ae371508676492379f16e2fa48f4e2c11741bd63c48be4b12a6b09cba/aiosignal-1.4.0-py3-none-any.whl", hash = "sha256:053243f8b92b990551949e63930a839ff0cf0b0ebbe0597b0f3fb19e1a0fe82e", size = 7490, upload-time = "2025-07-03T22:54:42.156Z" }, ] [[package]] name = "annotated-doc" version = "0.0.4" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/57/ba/046ceea27344560984e26a590f90bc7f4a75b06701f653222458922b558c/annotated_doc-0.0.4.tar.gz", hash = "sha256:fbcda96e87e9c92ad167c2e53839e57503ecfda18804ea28102353485033faa4", size = 7288 } +sdist = { url = "https://files.pythonhosted.org/packages/57/ba/046ceea27344560984e26a590f90bc7f4a75b06701f653222458922b558c/annotated_doc-0.0.4.tar.gz", hash = "sha256:fbcda96e87e9c92ad167c2e53839e57503ecfda18804ea28102353485033faa4", size = 7288, upload-time = "2025-11-10T22:07:42.062Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/1e/d3/26bf1008eb3d2daa8ef4cacc7f3bfdc11818d111f7e2d0201bc6e3b49d45/annotated_doc-0.0.4-py3-none-any.whl", hash = "sha256:571ac1dc6991c450b25a9c2d84a3705e2ae7a53467b5d111c24fa8baabbed320", size = 5303 }, + { url = "https://files.pythonhosted.org/packages/1e/d3/26bf1008eb3d2daa8ef4cacc7f3bfdc11818d111f7e2d0201bc6e3b49d45/annotated_doc-0.0.4-py3-none-any.whl", hash = "sha256:571ac1dc6991c450b25a9c2d84a3705e2ae7a53467b5d111c24fa8baabbed320", size = 5303, upload-time = "2025-11-10T22:07:40.673Z" }, ] [[package]] name = "annotated-types" version = "0.7.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081 } +sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643 }, + { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, ] [[package]] @@ -139,9 +139,9 @@ dependencies = [ { name = "sniffio" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/04/1f/08e95f4b7e2d35205ae5dcbb4ae97e7d477fc521c275c02609e2931ece2d/anthropic-0.75.0.tar.gz", hash = "sha256:e8607422f4ab616db2ea5baacc215dd5f028da99ce2f022e33c7c535b29f3dfb", size = 439565 } +sdist = { url = "https://files.pythonhosted.org/packages/04/1f/08e95f4b7e2d35205ae5dcbb4ae97e7d477fc521c275c02609e2931ece2d/anthropic-0.75.0.tar.gz", hash = "sha256:e8607422f4ab616db2ea5baacc215dd5f028da99ce2f022e33c7c535b29f3dfb", size = 439565, upload-time = "2025-11-24T20:41:45.28Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/60/1c/1cd02b7ae64302a6e06724bf80a96401d5313708651d277b1458504a1730/anthropic-0.75.0-py3-none-any.whl", hash = "sha256:ea8317271b6c15d80225a9f3c670152746e88805a7a61e14d4a374577164965b", size = 388164 }, + { url = "https://files.pythonhosted.org/packages/60/1c/1cd02b7ae64302a6e06724bf80a96401d5313708651d277b1458504a1730/anthropic-0.75.0-py3-none-any.whl", hash = "sha256:ea8317271b6c15d80225a9f3c670152746e88805a7a61e14d4a374577164965b", size = 388164, upload-time = "2025-11-24T20:41:43.587Z" }, ] [[package]] @@ -151,9 +151,9 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "idna" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/16/ce/8a777047513153587e5434fd752e89334ac33e379aa3497db860eeb60377/anyio-4.12.0.tar.gz", hash = "sha256:73c693b567b0c55130c104d0b43a9baf3aa6a31fc6110116509f27bf75e21ec0", size = 228266 } +sdist = { url = "https://files.pythonhosted.org/packages/16/ce/8a777047513153587e5434fd752e89334ac33e379aa3497db860eeb60377/anyio-4.12.0.tar.gz", hash = "sha256:73c693b567b0c55130c104d0b43a9baf3aa6a31fc6110116509f27bf75e21ec0", size = 228266, upload-time = "2025-11-28T23:37:38.911Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/7f/9c/36c5c37947ebfb8c7f22e0eb6e4d188ee2d53aa3880f3f2744fb894f0cb1/anyio-4.12.0-py3-none-any.whl", hash = "sha256:dad2376a628f98eeca4881fc56cd06affd18f659b17a747d3ff0307ced94b1bb", size = 113362 }, + { url = "https://files.pythonhosted.org/packages/7f/9c/36c5c37947ebfb8c7f22e0eb6e4d188ee2d53aa3880f3f2744fb894f0cb1/anyio-4.12.0-py3-none-any.whl", hash = "sha256:dad2376a628f98eeca4881fc56cd06affd18f659b17a747d3ff0307ced94b1bb", size = 113362, upload-time = "2025-11-28T23:36:57.897Z" }, ] [[package]] @@ -163,59 +163,59 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/0f/4b/cd5d66b9f87e773bc71344a368b9472987e33514e6627e28342b9c3e7c43/anysqlite-0.0.5.tar.gz", hash = "sha256:9dfcf87baf6b93426ad1d9118088c41dbf24ef01b445eea4a5d486bac2755cce", size = 3432 } +sdist = { url = "https://files.pythonhosted.org/packages/0f/4b/cd5d66b9f87e773bc71344a368b9472987e33514e6627e28342b9c3e7c43/anysqlite-0.0.5.tar.gz", hash = "sha256:9dfcf87baf6b93426ad1d9118088c41dbf24ef01b445eea4a5d486bac2755cce", size = 3432, upload-time = "2023-10-02T13:49:25.135Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/0b/31/349eae2bc9d9331dd8951684cf94528d91efaa71129dc30822ac111dfc66/anysqlite-0.0.5-py3-none-any.whl", hash = "sha256:cb345dc4f76f6b37f768d7a0b3e9cf5c700dfcb7a6356af8ab46a11f666edbe7", size = 3907 }, + { url = "https://files.pythonhosted.org/packages/0b/31/349eae2bc9d9331dd8951684cf94528d91efaa71129dc30822ac111dfc66/anysqlite-0.0.5-py3-none-any.whl", hash = "sha256:cb345dc4f76f6b37f768d7a0b3e9cf5c700dfcb7a6356af8ab46a11f666edbe7", size = 3907, upload-time = "2023-10-02T13:49:26.943Z" }, ] [[package]] name = "argcomplete" version = "3.6.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/38/61/0b9ae6399dd4a58d8c1b1dc5a27d6f2808023d0b5dd3104bb99f45a33ff6/argcomplete-3.6.3.tar.gz", hash = "sha256:62e8ed4fd6a45864acc8235409461b72c9a28ee785a2011cc5eb78318786c89c", size = 73754 } +sdist = { url = "https://files.pythonhosted.org/packages/38/61/0b9ae6399dd4a58d8c1b1dc5a27d6f2808023d0b5dd3104bb99f45a33ff6/argcomplete-3.6.3.tar.gz", hash = "sha256:62e8ed4fd6a45864acc8235409461b72c9a28ee785a2011cc5eb78318786c89c", size = 73754, upload-time = "2025-10-20T03:33:34.741Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/74/f5/9373290775639cb67a2fce7f629a1c240dce9f12fe927bc32b2736e16dfc/argcomplete-3.6.3-py3-none-any.whl", hash = "sha256:f5007b3a600ccac5d25bbce33089211dfd49eab4a7718da3f10e3082525a92ce", size = 43846 }, + { url = "https://files.pythonhosted.org/packages/74/f5/9373290775639cb67a2fce7f629a1c240dce9f12fe927bc32b2736e16dfc/argcomplete-3.6.3-py3-none-any.whl", hash = "sha256:f5007b3a600ccac5d25bbce33089211dfd49eab4a7718da3f10e3082525a92ce", size = 43846, upload-time = "2025-10-20T03:33:33.021Z" }, ] [[package]] name = "asyncpg" version = "0.31.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/fe/cc/d18065ce2380d80b1bcce927c24a2642efd38918e33fd724bc4bca904877/asyncpg-0.31.0.tar.gz", hash = "sha256:c989386c83940bfbd787180f2b1519415e2d3d6277a70d9d0f0145ac73500735", size = 993667 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/95/11/97b5c2af72a5d0b9bc3fa30cd4b9ce22284a9a943a150fdc768763caf035/asyncpg-0.31.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c204fab1b91e08b0f47e90a75d1b3c62174dab21f670ad6c5d0f243a228f015b", size = 661111 }, - { url = "https://files.pythonhosted.org/packages/1b/71/157d611c791a5e2d0423f09f027bd499935f0906e0c2a416ce712ba51ef3/asyncpg-0.31.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:54a64f91839ba59008eccf7aad2e93d6e3de688d796f35803235ea1c4898ae1e", size = 636928 }, - { url = "https://files.pythonhosted.org/packages/2e/fc/9e3486fb2bbe69d4a867c0b76d68542650a7ff1574ca40e84c3111bb0c6e/asyncpg-0.31.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c0e0822b1038dc7253b337b0f3f676cadc4ac31b126c5d42691c39691962e403", size = 3424067 }, - { url = "https://files.pythonhosted.org/packages/12/c6/8c9d076f73f07f995013c791e018a1cd5f31823c2a3187fc8581706aa00f/asyncpg-0.31.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bef056aa502ee34204c161c72ca1f3c274917596877f825968368b2c33f585f4", size = 3518156 }, - { url = "https://files.pythonhosted.org/packages/ae/3b/60683a0baf50fbc546499cfb53132cb6835b92b529a05f6a81471ab60d0c/asyncpg-0.31.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0bfbcc5b7ffcd9b75ab1558f00db2ae07db9c80637ad1b2469c43df79d7a5ae2", size = 3319636 }, - { url = "https://files.pythonhosted.org/packages/50/dc/8487df0f69bd398a61e1792b3cba0e47477f214eff085ba0efa7eac9ce87/asyncpg-0.31.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:22bc525ebbdc24d1261ecbf6f504998244d4e3be1721784b5f64664d61fbe602", size = 3472079 }, - { url = "https://files.pythonhosted.org/packages/13/a1/c5bbeeb8531c05c89135cb8b28575ac2fac618bcb60119ee9696c3faf71c/asyncpg-0.31.0-cp313-cp313-win32.whl", hash = "sha256:f890de5e1e4f7e14023619399a471ce4b71f5418cd67a51853b9910fdfa73696", size = 527606 }, - { url = "https://files.pythonhosted.org/packages/91/66/b25ccb84a246b470eb943b0107c07edcae51804912b824054b3413995a10/asyncpg-0.31.0-cp313-cp313-win_amd64.whl", hash = "sha256:dc5f2fa9916f292e5c5c8b2ac2813763bcd7f58e130055b4ad8a0531314201ab", size = 596569 }, - { url = "https://files.pythonhosted.org/packages/3c/36/e9450d62e84a13aea6580c83a47a437f26c7ca6fa0f0fd40b6670793ea30/asyncpg-0.31.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:f6b56b91bb0ffc328c4e3ed113136cddd9deefdf5f79ab448598b9772831df44", size = 660867 }, - { url = "https://files.pythonhosted.org/packages/82/4b/1d0a2b33b3102d210439338e1beea616a6122267c0df459ff0265cd5807a/asyncpg-0.31.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:334dec28cf20d7f5bb9e45b39546ddf247f8042a690bff9b9573d00086e69cb5", size = 638349 }, - { url = "https://files.pythonhosted.org/packages/41/aa/e7f7ac9a7974f08eff9183e392b2d62516f90412686532d27e196c0f0eeb/asyncpg-0.31.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:98cc158c53f46de7bb677fd20c417e264fc02b36d901cc2a43bd6cb0dc6dbfd2", size = 3410428 }, - { url = "https://files.pythonhosted.org/packages/6f/de/bf1b60de3dede5c2731e6788617a512bc0ebd9693eac297ee74086f101d7/asyncpg-0.31.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9322b563e2661a52e3cdbc93eed3be7748b289f792e0011cb2720d278b366ce2", size = 3471678 }, - { url = "https://files.pythonhosted.org/packages/46/78/fc3ade003e22d8bd53aaf8f75f4be48f0b460fa73738f0391b9c856a9147/asyncpg-0.31.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:19857a358fc811d82227449b7ca40afb46e75b33eb8897240c3839dd8b744218", size = 3313505 }, - { url = "https://files.pythonhosted.org/packages/bf/e9/73eb8a6789e927816f4705291be21f2225687bfa97321e40cd23055e903a/asyncpg-0.31.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ba5f8886e850882ff2c2ace5732300e99193823e8107e2c53ef01c1ebfa1e85d", size = 3434744 }, - { url = "https://files.pythonhosted.org/packages/08/4b/f10b880534413c65c5b5862f79b8e81553a8f364e5238832ad4c0af71b7f/asyncpg-0.31.0-cp314-cp314-win32.whl", hash = "sha256:cea3a0b2a14f95834cee29432e4ddc399b95700eb1d51bbc5bfee8f31fa07b2b", size = 532251 }, - { url = "https://files.pythonhosted.org/packages/d3/2d/7aa40750b7a19efa5d66e67fc06008ca0f27ba1bd082e457ad82f59aba49/asyncpg-0.31.0-cp314-cp314-win_amd64.whl", hash = "sha256:04d19392716af6b029411a0264d92093b6e5e8285ae97a39957b9a9c14ea72be", size = 604901 }, - { url = "https://files.pythonhosted.org/packages/ce/fe/b9dfe349b83b9dee28cc42360d2c86b2cdce4cb551a2c2d27e156bcac84d/asyncpg-0.31.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:bdb957706da132e982cc6856bb2f7b740603472b54c3ebc77fe60ea3e57e1bd2", size = 702280 }, - { url = "https://files.pythonhosted.org/packages/6a/81/e6be6e37e560bd91e6c23ea8a6138a04fd057b08cf63d3c5055c98e81c1d/asyncpg-0.31.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6d11b198111a72f47154fa03b85799f9be63701e068b43f84ac25da0bda9cb31", size = 682931 }, - { url = "https://files.pythonhosted.org/packages/a6/45/6009040da85a1648dd5bc75b3b0a062081c483e75a1a29041ae63a0bf0dc/asyncpg-0.31.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:18c83b03bc0d1b23e6230f5bf8d4f217dc9bc08644ce0502a9d91dc9e634a9c7", size = 3581608 }, - { url = "https://files.pythonhosted.org/packages/7e/06/2e3d4d7608b0b2b3adbee0d0bd6a2d29ca0fc4d8a78f8277df04e2d1fd7b/asyncpg-0.31.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e009abc333464ff18b8f6fd146addffd9aaf63e79aa3bb40ab7a4c332d0c5e9e", size = 3498738 }, - { url = "https://files.pythonhosted.org/packages/7d/aa/7d75ede780033141c51d83577ea23236ba7d3a23593929b32b49db8ed36e/asyncpg-0.31.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:3b1fbcb0e396a5ca435a8826a87e5c2c2cc0c8c68eb6fadf82168056b0e53a8c", size = 3401026 }, - { url = "https://files.pythonhosted.org/packages/ba/7a/15e37d45e7f7c94facc1e9148c0e455e8f33c08f0b8a0b1deb2c5171771b/asyncpg-0.31.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:8df714dba348efcc162d2adf02d213e5fab1bd9f557e1305633e851a61814a7a", size = 3429426 }, - { url = "https://files.pythonhosted.org/packages/13/d5/71437c5f6ae5f307828710efbe62163974e71237d5d46ebd2869ea052d10/asyncpg-0.31.0-cp314-cp314t-win32.whl", hash = "sha256:1b41f1afb1033f2b44f3234993b15096ddc9cd71b21a42dbd87fc6a57b43d65d", size = 614495 }, - { url = "https://files.pythonhosted.org/packages/3c/d7/8fb3044eaef08a310acfe23dae9a8e2e07d305edc29a53497e52bc76eca7/asyncpg-0.31.0-cp314-cp314t-win_amd64.whl", hash = "sha256:bd4107bb7cdd0e9e65fae66a62afd3a249663b844fa34d479f6d5b3bef9c04c3", size = 706062 }, +sdist = { url = "https://files.pythonhosted.org/packages/fe/cc/d18065ce2380d80b1bcce927c24a2642efd38918e33fd724bc4bca904877/asyncpg-0.31.0.tar.gz", hash = "sha256:c989386c83940bfbd787180f2b1519415e2d3d6277a70d9d0f0145ac73500735", size = 993667, upload-time = "2025-11-24T23:27:00.812Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/95/11/97b5c2af72a5d0b9bc3fa30cd4b9ce22284a9a943a150fdc768763caf035/asyncpg-0.31.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c204fab1b91e08b0f47e90a75d1b3c62174dab21f670ad6c5d0f243a228f015b", size = 661111, upload-time = "2025-11-24T23:26:04.467Z" }, + { url = "https://files.pythonhosted.org/packages/1b/71/157d611c791a5e2d0423f09f027bd499935f0906e0c2a416ce712ba51ef3/asyncpg-0.31.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:54a64f91839ba59008eccf7aad2e93d6e3de688d796f35803235ea1c4898ae1e", size = 636928, upload-time = "2025-11-24T23:26:05.944Z" }, + { url = "https://files.pythonhosted.org/packages/2e/fc/9e3486fb2bbe69d4a867c0b76d68542650a7ff1574ca40e84c3111bb0c6e/asyncpg-0.31.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c0e0822b1038dc7253b337b0f3f676cadc4ac31b126c5d42691c39691962e403", size = 3424067, upload-time = "2025-11-24T23:26:07.957Z" }, + { url = "https://files.pythonhosted.org/packages/12/c6/8c9d076f73f07f995013c791e018a1cd5f31823c2a3187fc8581706aa00f/asyncpg-0.31.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bef056aa502ee34204c161c72ca1f3c274917596877f825968368b2c33f585f4", size = 3518156, upload-time = "2025-11-24T23:26:09.591Z" }, + { url = "https://files.pythonhosted.org/packages/ae/3b/60683a0baf50fbc546499cfb53132cb6835b92b529a05f6a81471ab60d0c/asyncpg-0.31.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0bfbcc5b7ffcd9b75ab1558f00db2ae07db9c80637ad1b2469c43df79d7a5ae2", size = 3319636, upload-time = "2025-11-24T23:26:11.168Z" }, + { url = "https://files.pythonhosted.org/packages/50/dc/8487df0f69bd398a61e1792b3cba0e47477f214eff085ba0efa7eac9ce87/asyncpg-0.31.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:22bc525ebbdc24d1261ecbf6f504998244d4e3be1721784b5f64664d61fbe602", size = 3472079, upload-time = "2025-11-24T23:26:13.164Z" }, + { url = "https://files.pythonhosted.org/packages/13/a1/c5bbeeb8531c05c89135cb8b28575ac2fac618bcb60119ee9696c3faf71c/asyncpg-0.31.0-cp313-cp313-win32.whl", hash = "sha256:f890de5e1e4f7e14023619399a471ce4b71f5418cd67a51853b9910fdfa73696", size = 527606, upload-time = "2025-11-24T23:26:14.78Z" }, + { url = "https://files.pythonhosted.org/packages/91/66/b25ccb84a246b470eb943b0107c07edcae51804912b824054b3413995a10/asyncpg-0.31.0-cp313-cp313-win_amd64.whl", hash = "sha256:dc5f2fa9916f292e5c5c8b2ac2813763bcd7f58e130055b4ad8a0531314201ab", size = 596569, upload-time = "2025-11-24T23:26:16.189Z" }, + { url = "https://files.pythonhosted.org/packages/3c/36/e9450d62e84a13aea6580c83a47a437f26c7ca6fa0f0fd40b6670793ea30/asyncpg-0.31.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:f6b56b91bb0ffc328c4e3ed113136cddd9deefdf5f79ab448598b9772831df44", size = 660867, upload-time = "2025-11-24T23:26:17.631Z" }, + { url = "https://files.pythonhosted.org/packages/82/4b/1d0a2b33b3102d210439338e1beea616a6122267c0df459ff0265cd5807a/asyncpg-0.31.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:334dec28cf20d7f5bb9e45b39546ddf247f8042a690bff9b9573d00086e69cb5", size = 638349, upload-time = "2025-11-24T23:26:19.689Z" }, + { url = "https://files.pythonhosted.org/packages/41/aa/e7f7ac9a7974f08eff9183e392b2d62516f90412686532d27e196c0f0eeb/asyncpg-0.31.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:98cc158c53f46de7bb677fd20c417e264fc02b36d901cc2a43bd6cb0dc6dbfd2", size = 3410428, upload-time = "2025-11-24T23:26:21.275Z" }, + { url = "https://files.pythonhosted.org/packages/6f/de/bf1b60de3dede5c2731e6788617a512bc0ebd9693eac297ee74086f101d7/asyncpg-0.31.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9322b563e2661a52e3cdbc93eed3be7748b289f792e0011cb2720d278b366ce2", size = 3471678, upload-time = "2025-11-24T23:26:23.627Z" }, + { url = "https://files.pythonhosted.org/packages/46/78/fc3ade003e22d8bd53aaf8f75f4be48f0b460fa73738f0391b9c856a9147/asyncpg-0.31.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:19857a358fc811d82227449b7ca40afb46e75b33eb8897240c3839dd8b744218", size = 3313505, upload-time = "2025-11-24T23:26:25.235Z" }, + { url = "https://files.pythonhosted.org/packages/bf/e9/73eb8a6789e927816f4705291be21f2225687bfa97321e40cd23055e903a/asyncpg-0.31.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ba5f8886e850882ff2c2ace5732300e99193823e8107e2c53ef01c1ebfa1e85d", size = 3434744, upload-time = "2025-11-24T23:26:26.944Z" }, + { url = "https://files.pythonhosted.org/packages/08/4b/f10b880534413c65c5b5862f79b8e81553a8f364e5238832ad4c0af71b7f/asyncpg-0.31.0-cp314-cp314-win32.whl", hash = "sha256:cea3a0b2a14f95834cee29432e4ddc399b95700eb1d51bbc5bfee8f31fa07b2b", size = 532251, upload-time = "2025-11-24T23:26:28.404Z" }, + { url = "https://files.pythonhosted.org/packages/d3/2d/7aa40750b7a19efa5d66e67fc06008ca0f27ba1bd082e457ad82f59aba49/asyncpg-0.31.0-cp314-cp314-win_amd64.whl", hash = "sha256:04d19392716af6b029411a0264d92093b6e5e8285ae97a39957b9a9c14ea72be", size = 604901, upload-time = "2025-11-24T23:26:30.34Z" }, + { url = "https://files.pythonhosted.org/packages/ce/fe/b9dfe349b83b9dee28cc42360d2c86b2cdce4cb551a2c2d27e156bcac84d/asyncpg-0.31.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:bdb957706da132e982cc6856bb2f7b740603472b54c3ebc77fe60ea3e57e1bd2", size = 702280, upload-time = "2025-11-24T23:26:32Z" }, + { url = "https://files.pythonhosted.org/packages/6a/81/e6be6e37e560bd91e6c23ea8a6138a04fd057b08cf63d3c5055c98e81c1d/asyncpg-0.31.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6d11b198111a72f47154fa03b85799f9be63701e068b43f84ac25da0bda9cb31", size = 682931, upload-time = "2025-11-24T23:26:33.572Z" }, + { url = "https://files.pythonhosted.org/packages/a6/45/6009040da85a1648dd5bc75b3b0a062081c483e75a1a29041ae63a0bf0dc/asyncpg-0.31.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:18c83b03bc0d1b23e6230f5bf8d4f217dc9bc08644ce0502a9d91dc9e634a9c7", size = 3581608, upload-time = "2025-11-24T23:26:35.638Z" }, + { url = "https://files.pythonhosted.org/packages/7e/06/2e3d4d7608b0b2b3adbee0d0bd6a2d29ca0fc4d8a78f8277df04e2d1fd7b/asyncpg-0.31.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e009abc333464ff18b8f6fd146addffd9aaf63e79aa3bb40ab7a4c332d0c5e9e", size = 3498738, upload-time = "2025-11-24T23:26:37.275Z" }, + { url = "https://files.pythonhosted.org/packages/7d/aa/7d75ede780033141c51d83577ea23236ba7d3a23593929b32b49db8ed36e/asyncpg-0.31.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:3b1fbcb0e396a5ca435a8826a87e5c2c2cc0c8c68eb6fadf82168056b0e53a8c", size = 3401026, upload-time = "2025-11-24T23:26:39.423Z" }, + { url = "https://files.pythonhosted.org/packages/ba/7a/15e37d45e7f7c94facc1e9148c0e455e8f33c08f0b8a0b1deb2c5171771b/asyncpg-0.31.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:8df714dba348efcc162d2adf02d213e5fab1bd9f557e1305633e851a61814a7a", size = 3429426, upload-time = "2025-11-24T23:26:41.032Z" }, + { url = "https://files.pythonhosted.org/packages/13/d5/71437c5f6ae5f307828710efbe62163974e71237d5d46ebd2869ea052d10/asyncpg-0.31.0-cp314-cp314t-win32.whl", hash = "sha256:1b41f1afb1033f2b44f3234993b15096ddc9cd71b21a42dbd87fc6a57b43d65d", size = 614495, upload-time = "2025-11-24T23:26:42.659Z" }, + { url = "https://files.pythonhosted.org/packages/3c/d7/8fb3044eaef08a310acfe23dae9a8e2e07d305edc29a53497e52bc76eca7/asyncpg-0.31.0-cp314-cp314t-win_amd64.whl", hash = "sha256:bd4107bb7cdd0e9e65fae66a62afd3a249663b844fa34d479f6d5b3bef9c04c3", size = 706062, upload-time = "2025-11-24T23:26:44.086Z" }, ] [[package]] name = "attrs" version = "25.4.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/6b/5c/685e6633917e101e5dcb62b9dd76946cbb57c26e133bae9e0cd36033c0a9/attrs-25.4.0.tar.gz", hash = "sha256:16d5969b87f0859ef33a48b35d55ac1be6e42ae49d5e853b597db70c35c57e11", size = 934251 } +sdist = { url = "https://files.pythonhosted.org/packages/6b/5c/685e6633917e101e5dcb62b9dd76946cbb57c26e133bae9e0cd36033c0a9/attrs-25.4.0.tar.gz", hash = "sha256:16d5969b87f0859ef33a48b35d55ac1be6e42ae49d5e853b597db70c35c57e11", size = 934251, upload-time = "2025-10-06T13:54:44.725Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/3a/2a/7cc015f5b9f5db42b7d48157e23356022889fc354a2813c15934b7cb5c0e/attrs-25.4.0-py3-none-any.whl", hash = "sha256:adcf7e2a1fb3b36ac48d97835bb6d8ade15b8dcce26aba8bf1d14847b57a3373", size = 67615 }, + { url = "https://files.pythonhosted.org/packages/3a/2a/7cc015f5b9f5db42b7d48157e23356022889fc354a2813c15934b7cb5c0e/attrs-25.4.0-py3-none-any.whl", hash = "sha256:adcf7e2a1fb3b36ac48d97835bb6d8ade15b8dcce26aba8bf1d14847b57a3373", size = 67615, upload-time = "2025-10-06T13:54:43.17Z" }, ] [[package]] @@ -225,18 +225,18 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cryptography" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/bb/9b/b1661026ff24bc641b76b78c5222d614776b0c085bcfdac9bd15a1cb4b35/authlib-1.6.6.tar.gz", hash = "sha256:45770e8e056d0f283451d9996fbb59b70d45722b45d854d58f32878d0a40c38e", size = 164894 } +sdist = { url = "https://files.pythonhosted.org/packages/bb/9b/b1661026ff24bc641b76b78c5222d614776b0c085bcfdac9bd15a1cb4b35/authlib-1.6.6.tar.gz", hash = "sha256:45770e8e056d0f283451d9996fbb59b70d45722b45d854d58f32878d0a40c38e", size = 164894, upload-time = "2025-12-12T08:01:41.464Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/54/51/321e821856452f7386c4e9df866f196720b1ad0c5ea1623ea7399969ae3b/authlib-1.6.6-py2.py3-none-any.whl", hash = "sha256:7d9e9bc535c13974313a87f53e8430eb6ea3d1cf6ae4f6efcd793f2e949143fd", size = 244005 }, + { url = "https://files.pythonhosted.org/packages/54/51/321e821856452f7386c4e9df866f196720b1ad0c5ea1623ea7399969ae3b/authlib-1.6.6-py2.py3-none-any.whl", hash = "sha256:7d9e9bc535c13974313a87f53e8430eb6ea3d1cf6ae4f6efcd793f2e949143fd", size = 244005, upload-time = "2025-12-12T08:01:40.209Z" }, ] [[package]] name = "beartype" version = "0.22.9" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/c7/94/1009e248bbfbab11397abca7193bea6626806be9a327d399810d523a07cb/beartype-0.22.9.tar.gz", hash = "sha256:8f82b54aa723a2848a56008d18875f91c1db02c32ef6a62319a002e3e25a975f", size = 1608866 } +sdist = { url = "https://files.pythonhosted.org/packages/c7/94/1009e248bbfbab11397abca7193bea6626806be9a327d399810d523a07cb/beartype-0.22.9.tar.gz", hash = "sha256:8f82b54aa723a2848a56008d18875f91c1db02c32ef6a62319a002e3e25a975f", size = 1608866, upload-time = "2025-12-13T06:50:30.72Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/71/cc/18245721fa7747065ab478316c7fea7c74777d07f37ae60db2e84f8172e8/beartype-0.22.9-py3-none-any.whl", hash = "sha256:d16c9bbc61ea14637596c5f6fbff2ee99cbe3573e46a716401734ef50c3060c2", size = 1333658 }, + { url = "https://files.pythonhosted.org/packages/71/cc/18245721fa7747065ab478316c7fea7c74777d07f37ae60db2e84f8172e8/beartype-0.22.9-py3-none-any.whl", hash = "sha256:d16c9bbc61ea14637596c5f6fbff2ee99cbe3573e46a716401734ef50c3060c2", size = 1333658, upload-time = "2025-12-13T06:50:28.266Z" }, ] [[package]] @@ -248,9 +248,9 @@ dependencies = [ { name = "jmespath" }, { name = "s3transfer" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/8d/cf/6e4a794e73cbf3e774ec27a5acc8442ce55d97cfc5226a20c1f58d8aee16/boto3-1.42.17.tar.gz", hash = "sha256:8a2e345e96d5ceba755c55539c93f99705f403fbfdeef2e838eabdc56750828b", size = 112776 } +sdist = { url = "https://files.pythonhosted.org/packages/8d/cf/6e4a794e73cbf3e774ec27a5acc8442ce55d97cfc5226a20c1f58d8aee16/boto3-1.42.17.tar.gz", hash = "sha256:8a2e345e96d5ceba755c55539c93f99705f403fbfdeef2e838eabdc56750828b", size = 112776, upload-time = "2025-12-26T20:33:38.289Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/6c/53/9f4241152eb1f5a57351232c14ef411ef35fbcc1d97a63873235d0503a8a/boto3-1.42.17-py3-none-any.whl", hash = "sha256:e0ee40f7102712452f6776af891c8f49b5ae9133bdaf22711d6f4a78963c2614", size = 140572 }, + { url = "https://files.pythonhosted.org/packages/6c/53/9f4241152eb1f5a57351232c14ef411ef35fbcc1d97a63873235d0503a8a/boto3-1.42.17-py3-none-any.whl", hash = "sha256:e0ee40f7102712452f6776af891c8f49b5ae9133bdaf22711d6f4a78963c2614", size = 140572, upload-time = "2025-12-26T20:33:36.307Z" }, ] [[package]] @@ -262,27 +262,27 @@ dependencies = [ { name = "python-dateutil" }, { name = "urllib3" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/4d/d2/128e2d8b9d426bd3a339e7dcf3eedca55f449af121604b6891ae80f6be9a/botocore-1.42.17.tar.gz", hash = "sha256:d73fe22c8e1497e4d59ff7dc68eb05afac68a4a6457656811562285d6132bc04", size = 14911757 } +sdist = { url = "https://files.pythonhosted.org/packages/4d/d2/128e2d8b9d426bd3a339e7dcf3eedca55f449af121604b6891ae80f6be9a/botocore-1.42.17.tar.gz", hash = "sha256:d73fe22c8e1497e4d59ff7dc68eb05afac68a4a6457656811562285d6132bc04", size = 14911757, upload-time = "2025-12-26T20:33:27.641Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d1/47/cc00f2421f49784de8b9113c94b7b6580db989721f5109a24b9108308fe1/botocore-1.42.17-py3-none-any.whl", hash = "sha256:a832e4c04e63141221480967e9e511363aa54d24c405935fccb913a18583c96b", size = 14586536 }, + { url = "https://files.pythonhosted.org/packages/d1/47/cc00f2421f49784de8b9113c94b7b6580db989721f5109a24b9108308fe1/botocore-1.42.17-py3-none-any.whl", hash = "sha256:a832e4c04e63141221480967e9e511363aa54d24c405935fccb913a18583c96b", size = 14586536, upload-time = "2025-12-26T20:33:24.032Z" }, ] [[package]] name = "cachetools" version = "6.2.4" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/bc/1d/ede8680603f6016887c062a2cf4fc8fdba905866a3ab8831aa8aa651320c/cachetools-6.2.4.tar.gz", hash = "sha256:82c5c05585e70b6ba2d3ae09ea60b79548872185d2f24ae1f2709d37299fd607", size = 31731 } +sdist = { url = "https://files.pythonhosted.org/packages/bc/1d/ede8680603f6016887c062a2cf4fc8fdba905866a3ab8831aa8aa651320c/cachetools-6.2.4.tar.gz", hash = "sha256:82c5c05585e70b6ba2d3ae09ea60b79548872185d2f24ae1f2709d37299fd607", size = 31731, upload-time = "2025-12-15T18:24:53.744Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/2c/fc/1d7b80d0eb7b714984ce40efc78859c022cd930e402f599d8ca9e39c78a4/cachetools-6.2.4-py3-none-any.whl", hash = "sha256:69a7a52634fed8b8bf6e24a050fb60bff1c9bd8f6d24572b99c32d4e71e62a51", size = 11551 }, + { url = "https://files.pythonhosted.org/packages/2c/fc/1d7b80d0eb7b714984ce40efc78859c022cd930e402f599d8ca9e39c78a4/cachetools-6.2.4-py3-none-any.whl", hash = "sha256:69a7a52634fed8b8bf6e24a050fb60bff1c9bd8f6d24572b99c32d4e71e62a51", size = 11551, upload-time = "2025-12-15T18:24:52.332Z" }, ] [[package]] name = "certifi" version = "2025.11.12" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a2/8c/58f469717fa48465e4a50c014a0400602d3c437d7c0c468e17ada824da3a/certifi-2025.11.12.tar.gz", hash = "sha256:d8ab5478f2ecd78af242878415affce761ca6bc54a22a27e026d7c25357c3316", size = 160538 } +sdist = { url = "https://files.pythonhosted.org/packages/a2/8c/58f469717fa48465e4a50c014a0400602d3c437d7c0c468e17ada824da3a/certifi-2025.11.12.tar.gz", hash = "sha256:d8ab5478f2ecd78af242878415affce761ca6bc54a22a27e026d7c25357c3316", size = 160538, upload-time = "2025-11-12T02:54:51.517Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/70/7d/9bc192684cea499815ff478dfcdc13835ddf401365057044fb721ec6bddb/certifi-2025.11.12-py3-none-any.whl", hash = "sha256:97de8790030bbd5c2d96b7ec782fc2f7820ef8dba6db909ccf95449f2d062d4b", size = 159438 }, + { url = "https://files.pythonhosted.org/packages/70/7d/9bc192684cea499815ff478dfcdc13835ddf401365057044fb721ec6bddb/certifi-2025.11.12-py3-none-any.whl", hash = "sha256:97de8790030bbd5c2d96b7ec782fc2f7820ef8dba6db909ccf95449f2d062d4b", size = 159438, upload-time = "2025-11-12T02:54:49.735Z" }, ] [[package]] @@ -292,99 +292,83 @@ 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 } -wheels = [ - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { url = "https://files.pythonhosted.org/packages/eb/6d/bf9bda840d5f1dfdbf0feca87fbdb64a918a69bca42cfa0ba7b137c48cb8/cffi-2.0.0-cp313-cp313-win32.whl", hash = "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27", size = 172909 }, - { url = "https://files.pythonhosted.org/packages/37/18/6519e1ee6f5a1e579e04b9ddb6f1676c17368a7aba48299c3759bbc3c8b3/cffi-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75", size = 183402 }, - { url = "https://files.pythonhosted.org/packages/cb/0e/02ceeec9a7d6ee63bb596121c2c8e9b3a9e150936f4fbef6ca1943e6137c/cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91", size = 177780 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { url = "https://files.pythonhosted.org/packages/3e/aa/df335faa45b395396fcbc03de2dfcab242cd61a9900e914fe682a59170b1/cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f", size = 175328 }, - { url = "https://files.pythonhosted.org/packages/bb/92/882c2d30831744296ce713f0feb4c1cd30f346ef747b530b5318715cc367/cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25", size = 185650 }, - { url = "https://files.pythonhosted.org/packages/9f/2c/98ece204b9d35a7366b5b2c6539c350313ca13932143e79dc133ba757104/cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad", size = 180687 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { url = "https://files.pythonhosted.org/packages/a0/1d/ec1a60bd1a10daa292d3cd6bb0b359a81607154fb8165f3ec95fe003b85c/cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e", size = 180487 }, - { url = "https://files.pythonhosted.org/packages/bf/41/4c1168c74fac325c0c8156f04b6749c8b6a8f405bbf91413ba088359f60d/cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6", size = 191726 }, - { url = "https://files.pythonhosted.org/packages/ae/3a/dbeec9d1ee0844c679f6bb5d6ad4e9f198b1224f4e7a32825f47f6192b0c/cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9", size = 184195 }, +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/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]] name = "charset-normalizer" version = "3.4.4" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/13/69/33ddede1939fdd074bce5434295f38fae7136463422fe4fd3e0e89b98062/charset_normalizer-3.4.4.tar.gz", hash = "sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a", size = 129418 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/97/45/4b3a1239bbacd321068ea6e7ac28875b03ab8bc0aa0966452db17cd36714/charset_normalizer-3.4.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e1f185f86a6f3403aa2420e815904c67b2f9ebc443f045edd0de921108345794", size = 208091 }, - { url = "https://files.pythonhosted.org/packages/7d/62/73a6d7450829655a35bb88a88fca7d736f9882a27eacdca2c6d505b57e2e/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b39f987ae8ccdf0d2642338faf2abb1862340facc796048b604ef14919e55ed", size = 147936 }, - { url = "https://files.pythonhosted.org/packages/89/c5/adb8c8b3d6625bef6d88b251bbb0d95f8205831b987631ab0c8bb5d937c2/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3162d5d8ce1bb98dd51af660f2121c55d0fa541b46dff7bb9b9f86ea1d87de72", size = 144180 }, - { url = "https://files.pythonhosted.org/packages/91/ed/9706e4070682d1cc219050b6048bfd293ccf67b3d4f5a4f39207453d4b99/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:81d5eb2a312700f4ecaa977a8235b634ce853200e828fbadf3a9c50bab278328", size = 161346 }, - { url = "https://files.pythonhosted.org/packages/d5/0d/031f0d95e4972901a2f6f09ef055751805ff541511dc1252ba3ca1f80cf5/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5bd2293095d766545ec1a8f612559f6b40abc0eb18bb2f5d1171872d34036ede", size = 158874 }, - { url = "https://files.pythonhosted.org/packages/f5/83/6ab5883f57c9c801ce5e5677242328aa45592be8a00644310a008d04f922/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a8a8b89589086a25749f471e6a900d3f662d1d3b6e2e59dcecf787b1cc3a1894", size = 153076 }, - { url = "https://files.pythonhosted.org/packages/75/1e/5ff781ddf5260e387d6419959ee89ef13878229732732ee73cdae01800f2/charset_normalizer-3.4.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc7637e2f80d8530ee4a78e878bce464f70087ce73cf7c1caf142416923b98f1", size = 150601 }, - { url = "https://files.pythonhosted.org/packages/d7/57/71be810965493d3510a6ca79b90c19e48696fb1ff964da319334b12677f0/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f8bf04158c6b607d747e93949aa60618b61312fe647a6369f88ce2ff16043490", size = 150376 }, - { url = "https://files.pythonhosted.org/packages/e5/d5/c3d057a78c181d007014feb7e9f2e65905a6c4ef182c0ddf0de2924edd65/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:554af85e960429cf30784dd47447d5125aaa3b99a6f0683589dbd27e2f45da44", size = 144825 }, - { url = "https://files.pythonhosted.org/packages/e6/8c/d0406294828d4976f275ffbe66f00266c4b3136b7506941d87c00cab5272/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:74018750915ee7ad843a774364e13a3db91682f26142baddf775342c3f5b1133", size = 162583 }, - { url = "https://files.pythonhosted.org/packages/d7/24/e2aa1f18c8f15c4c0e932d9287b8609dd30ad56dbe41d926bd846e22fb8d/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c0463276121fdee9c49b98908b3a89c39be45d86d1dbaa22957e38f6321d4ce3", size = 150366 }, - { url = "https://files.pythonhosted.org/packages/e4/5b/1e6160c7739aad1e2df054300cc618b06bf784a7a164b0f238360721ab86/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:362d61fd13843997c1c446760ef36f240cf81d3ebf74ac62652aebaf7838561e", size = 160300 }, - { url = "https://files.pythonhosted.org/packages/7a/10/f882167cd207fbdd743e55534d5d9620e095089d176d55cb22d5322f2afd/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9a26f18905b8dd5d685d6d07b0cdf98a79f3c7a918906af7cc143ea2e164c8bc", size = 154465 }, - { url = "https://files.pythonhosted.org/packages/89/66/c7a9e1b7429be72123441bfdbaf2bc13faab3f90b933f664db506dea5915/charset_normalizer-3.4.4-cp313-cp313-win32.whl", hash = "sha256:9b35f4c90079ff2e2edc5b26c0c77925e5d2d255c42c74fdb70fb49b172726ac", size = 99404 }, - { url = "https://files.pythonhosted.org/packages/c4/26/b9924fa27db384bdcd97ab83b4f0a8058d96ad9626ead570674d5e737d90/charset_normalizer-3.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:b435cba5f4f750aa6c0a0d92c541fb79f69a387c91e61f1795227e4ed9cece14", size = 107092 }, - { url = "https://files.pythonhosted.org/packages/af/8f/3ed4bfa0c0c72a7ca17f0380cd9e4dd842b09f664e780c13cff1dcf2ef1b/charset_normalizer-3.4.4-cp313-cp313-win_arm64.whl", hash = "sha256:542d2cee80be6f80247095cc36c418f7bddd14f4a6de45af91dfad36d817bba2", size = 100408 }, - { url = "https://files.pythonhosted.org/packages/2a/35/7051599bd493e62411d6ede36fd5af83a38f37c4767b92884df7301db25d/charset_normalizer-3.4.4-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:da3326d9e65ef63a817ecbcc0df6e94463713b754fe293eaa03da99befb9a5bd", size = 207746 }, - { url = "https://files.pythonhosted.org/packages/10/9a/97c8d48ef10d6cd4fcead2415523221624bf58bcf68a802721a6bc807c8f/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8af65f14dc14a79b924524b1e7fffe304517b2bff5a58bf64f30b98bbc5079eb", size = 147889 }, - { url = "https://files.pythonhosted.org/packages/10/bf/979224a919a1b606c82bd2c5fa49b5c6d5727aa47b4312bb27b1734f53cd/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74664978bb272435107de04e36db5a9735e78232b85b77d45cfb38f758efd33e", size = 143641 }, - { url = "https://files.pythonhosted.org/packages/ba/33/0ad65587441fc730dc7bd90e9716b30b4702dc7b617e6ba4997dc8651495/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:752944c7ffbfdd10c074dc58ec2d5a8a4cd9493b314d367c14d24c17684ddd14", size = 160779 }, - { url = "https://files.pythonhosted.org/packages/67/ed/331d6b249259ee71ddea93f6f2f0a56cfebd46938bde6fcc6f7b9a3d0e09/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d1f13550535ad8cff21b8d757a3257963e951d96e20ec82ab44bc64aeb62a191", size = 159035 }, - { url = "https://files.pythonhosted.org/packages/67/ff/f6b948ca32e4f2a4576aa129d8bed61f2e0543bf9f5f2b7fc3758ed005c9/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ecaae4149d99b1c9e7b88bb03e3221956f68fd6d50be2ef061b2381b61d20838", size = 152542 }, - { url = "https://files.pythonhosted.org/packages/16/85/276033dcbcc369eb176594de22728541a925b2632f9716428c851b149e83/charset_normalizer-3.4.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cb6254dc36b47a990e59e1068afacdcd02958bdcce30bb50cc1700a8b9d624a6", size = 149524 }, - { url = "https://files.pythonhosted.org/packages/9e/f2/6a2a1f722b6aba37050e626530a46a68f74e63683947a8acff92569f979a/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c8ae8a0f02f57a6e61203a31428fa1d677cbe50c93622b4149d5c0f319c1d19e", size = 150395 }, - { url = "https://files.pythonhosted.org/packages/60/bb/2186cb2f2bbaea6338cad15ce23a67f9b0672929744381e28b0592676824/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:47cc91b2f4dd2833fddaedd2893006b0106129d4b94fdb6af1f4ce5a9965577c", size = 143680 }, - { url = "https://files.pythonhosted.org/packages/7d/a5/bf6f13b772fbb2a90360eb620d52ed8f796f3c5caee8398c3b2eb7b1c60d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:82004af6c302b5d3ab2cfc4cc5f29db16123b1a8417f2e25f9066f91d4411090", size = 162045 }, - { url = "https://files.pythonhosted.org/packages/df/c5/d1be898bf0dc3ef9030c3825e5d3b83f2c528d207d246cbabe245966808d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2b7d8f6c26245217bd2ad053761201e9f9680f8ce52f0fcd8d0755aeae5b2152", size = 149687 }, - { url = "https://files.pythonhosted.org/packages/a5/42/90c1f7b9341eef50c8a1cb3f098ac43b0508413f33affd762855f67a410e/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:799a7a5e4fb2d5898c60b640fd4981d6a25f1c11790935a44ce38c54e985f828", size = 160014 }, - { url = "https://files.pythonhosted.org/packages/76/be/4d3ee471e8145d12795ab655ece37baed0929462a86e72372fd25859047c/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:99ae2cffebb06e6c22bdc25801d7b30f503cc87dbd283479e7b606f70aff57ec", size = 154044 }, - { url = "https://files.pythonhosted.org/packages/b0/6f/8f7af07237c34a1defe7defc565a9bc1807762f672c0fde711a4b22bf9c0/charset_normalizer-3.4.4-cp314-cp314-win32.whl", hash = "sha256:f9d332f8c2a2fcbffe1378594431458ddbef721c1769d78e2cbc06280d8155f9", size = 99940 }, - { url = "https://files.pythonhosted.org/packages/4b/51/8ade005e5ca5b0d80fb4aff72a3775b325bdc3d27408c8113811a7cbe640/charset_normalizer-3.4.4-cp314-cp314-win_amd64.whl", hash = "sha256:8a6562c3700cce886c5be75ade4a5db4214fda19fede41d9792d100288d8f94c", size = 107104 }, - { url = "https://files.pythonhosted.org/packages/da/5f/6b8f83a55bb8278772c5ae54a577f3099025f9ade59d0136ac24a0df4bde/charset_normalizer-3.4.4-cp314-cp314-win_arm64.whl", hash = "sha256:de00632ca48df9daf77a2c65a484531649261ec9f25489917f09e455cb09ddb2", size = 100743 }, - { url = "https://files.pythonhosted.org/packages/0a/4c/925909008ed5a988ccbb72dcc897407e5d6d3bd72410d69e051fc0c14647/charset_normalizer-3.4.4-py3-none-any.whl", hash = "sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f", size = 53402 }, -] - -[[package]] -name = "claude-agent-sdk" -version = "0.1.18" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "anyio" }, - { name = "mcp" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/fd/3d/a8c6ad873e8448696d44441c9eb2c24dded620fb32415d68f576a542ccde/claude_agent_sdk-0.1.18.tar.gz", hash = "sha256:4fcb8730cc77dea562fbe9aa48c65eced3ef58a6bb1f34f77e50e8258902477d", size = 56162 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/06/14/f529f7c4bab7c71dcbcc8c66f12f491e644ee8a027ac5111d13705df207e/claude_agent_sdk-0.1.18-py3-none-macosx_11_0_arm64.whl", hash = "sha256:9e45b4e3c20c072c3e3325fa60bab9a4b5a7cbbce64ca274b8d7d0af42dd9dd8", size = 54560828 }, - { url = "https://files.pythonhosted.org/packages/2c/68/6e83005aa7bb9056bfad0aef0605249f877dc0c78724c9c0fadebff600fb/claude_agent_sdk-0.1.18-py3-none-manylinux_2_17_aarch64.whl", hash = "sha256:3c41bd8f38848609ae0d5da8d7327a4c2d7057a363feafb6fd70df611ea204cc", size = 68743107 }, - { url = "https://files.pythonhosted.org/packages/fb/85/7d6dd85f402135a610894734c442f1166ffed61d03eced39d6bfd14efccd/claude_agent_sdk-0.1.18-py3-none-manylinux_2_17_x86_64.whl", hash = "sha256:983f15e51253f40c55136a86d7cc63e023a3576428b05fa1459093d461b2d215", size = 70444964 }, - { url = "https://files.pythonhosted.org/packages/3c/fa/d2b22b7a713c4c049cbd5f9f635836ea5429ff65c1f3bcf4658a8e1c1cf5/claude_agent_sdk-0.1.18-py3-none-win_amd64.whl", hash = "sha256:36f5b84d5c3c8773ee9b56aeb5ab345d1033231db37f80d1f20ac15239bef41c", size = 72637215 }, +sdist = { url = "https://files.pythonhosted.org/packages/13/69/33ddede1939fdd074bce5434295f38fae7136463422fe4fd3e0e89b98062/charset_normalizer-3.4.4.tar.gz", hash = "sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a", size = 129418, upload-time = "2025-10-14T04:42:32.879Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/97/45/4b3a1239bbacd321068ea6e7ac28875b03ab8bc0aa0966452db17cd36714/charset_normalizer-3.4.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e1f185f86a6f3403aa2420e815904c67b2f9ebc443f045edd0de921108345794", size = 208091, upload-time = "2025-10-14T04:41:13.346Z" }, + { url = "https://files.pythonhosted.org/packages/7d/62/73a6d7450829655a35bb88a88fca7d736f9882a27eacdca2c6d505b57e2e/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b39f987ae8ccdf0d2642338faf2abb1862340facc796048b604ef14919e55ed", size = 147936, upload-time = "2025-10-14T04:41:14.461Z" }, + { url = "https://files.pythonhosted.org/packages/89/c5/adb8c8b3d6625bef6d88b251bbb0d95f8205831b987631ab0c8bb5d937c2/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3162d5d8ce1bb98dd51af660f2121c55d0fa541b46dff7bb9b9f86ea1d87de72", size = 144180, upload-time = "2025-10-14T04:41:15.588Z" }, + { url = "https://files.pythonhosted.org/packages/91/ed/9706e4070682d1cc219050b6048bfd293ccf67b3d4f5a4f39207453d4b99/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:81d5eb2a312700f4ecaa977a8235b634ce853200e828fbadf3a9c50bab278328", size = 161346, upload-time = "2025-10-14T04:41:16.738Z" }, + { url = "https://files.pythonhosted.org/packages/d5/0d/031f0d95e4972901a2f6f09ef055751805ff541511dc1252ba3ca1f80cf5/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5bd2293095d766545ec1a8f612559f6b40abc0eb18bb2f5d1171872d34036ede", size = 158874, upload-time = "2025-10-14T04:41:17.923Z" }, + { url = "https://files.pythonhosted.org/packages/f5/83/6ab5883f57c9c801ce5e5677242328aa45592be8a00644310a008d04f922/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a8a8b89589086a25749f471e6a900d3f662d1d3b6e2e59dcecf787b1cc3a1894", size = 153076, upload-time = "2025-10-14T04:41:19.106Z" }, + { url = "https://files.pythonhosted.org/packages/75/1e/5ff781ddf5260e387d6419959ee89ef13878229732732ee73cdae01800f2/charset_normalizer-3.4.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc7637e2f80d8530ee4a78e878bce464f70087ce73cf7c1caf142416923b98f1", size = 150601, upload-time = "2025-10-14T04:41:20.245Z" }, + { url = "https://files.pythonhosted.org/packages/d7/57/71be810965493d3510a6ca79b90c19e48696fb1ff964da319334b12677f0/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f8bf04158c6b607d747e93949aa60618b61312fe647a6369f88ce2ff16043490", size = 150376, upload-time = "2025-10-14T04:41:21.398Z" }, + { url = "https://files.pythonhosted.org/packages/e5/d5/c3d057a78c181d007014feb7e9f2e65905a6c4ef182c0ddf0de2924edd65/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:554af85e960429cf30784dd47447d5125aaa3b99a6f0683589dbd27e2f45da44", size = 144825, upload-time = "2025-10-14T04:41:22.583Z" }, + { url = "https://files.pythonhosted.org/packages/e6/8c/d0406294828d4976f275ffbe66f00266c4b3136b7506941d87c00cab5272/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:74018750915ee7ad843a774364e13a3db91682f26142baddf775342c3f5b1133", size = 162583, upload-time = "2025-10-14T04:41:23.754Z" }, + { url = "https://files.pythonhosted.org/packages/d7/24/e2aa1f18c8f15c4c0e932d9287b8609dd30ad56dbe41d926bd846e22fb8d/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c0463276121fdee9c49b98908b3a89c39be45d86d1dbaa22957e38f6321d4ce3", size = 150366, upload-time = "2025-10-14T04:41:25.27Z" }, + { url = "https://files.pythonhosted.org/packages/e4/5b/1e6160c7739aad1e2df054300cc618b06bf784a7a164b0f238360721ab86/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:362d61fd13843997c1c446760ef36f240cf81d3ebf74ac62652aebaf7838561e", size = 160300, upload-time = "2025-10-14T04:41:26.725Z" }, + { url = "https://files.pythonhosted.org/packages/7a/10/f882167cd207fbdd743e55534d5d9620e095089d176d55cb22d5322f2afd/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9a26f18905b8dd5d685d6d07b0cdf98a79f3c7a918906af7cc143ea2e164c8bc", size = 154465, upload-time = "2025-10-14T04:41:28.322Z" }, + { url = "https://files.pythonhosted.org/packages/89/66/c7a9e1b7429be72123441bfdbaf2bc13faab3f90b933f664db506dea5915/charset_normalizer-3.4.4-cp313-cp313-win32.whl", hash = "sha256:9b35f4c90079ff2e2edc5b26c0c77925e5d2d255c42c74fdb70fb49b172726ac", size = 99404, upload-time = "2025-10-14T04:41:29.95Z" }, + { url = "https://files.pythonhosted.org/packages/c4/26/b9924fa27db384bdcd97ab83b4f0a8058d96ad9626ead570674d5e737d90/charset_normalizer-3.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:b435cba5f4f750aa6c0a0d92c541fb79f69a387c91e61f1795227e4ed9cece14", size = 107092, upload-time = "2025-10-14T04:41:31.188Z" }, + { url = "https://files.pythonhosted.org/packages/af/8f/3ed4bfa0c0c72a7ca17f0380cd9e4dd842b09f664e780c13cff1dcf2ef1b/charset_normalizer-3.4.4-cp313-cp313-win_arm64.whl", hash = "sha256:542d2cee80be6f80247095cc36c418f7bddd14f4a6de45af91dfad36d817bba2", size = 100408, upload-time = "2025-10-14T04:41:32.624Z" }, + { url = "https://files.pythonhosted.org/packages/2a/35/7051599bd493e62411d6ede36fd5af83a38f37c4767b92884df7301db25d/charset_normalizer-3.4.4-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:da3326d9e65ef63a817ecbcc0df6e94463713b754fe293eaa03da99befb9a5bd", size = 207746, upload-time = "2025-10-14T04:41:33.773Z" }, + { url = "https://files.pythonhosted.org/packages/10/9a/97c8d48ef10d6cd4fcead2415523221624bf58bcf68a802721a6bc807c8f/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8af65f14dc14a79b924524b1e7fffe304517b2bff5a58bf64f30b98bbc5079eb", size = 147889, upload-time = "2025-10-14T04:41:34.897Z" }, + { url = "https://files.pythonhosted.org/packages/10/bf/979224a919a1b606c82bd2c5fa49b5c6d5727aa47b4312bb27b1734f53cd/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74664978bb272435107de04e36db5a9735e78232b85b77d45cfb38f758efd33e", size = 143641, upload-time = "2025-10-14T04:41:36.116Z" }, + { url = "https://files.pythonhosted.org/packages/ba/33/0ad65587441fc730dc7bd90e9716b30b4702dc7b617e6ba4997dc8651495/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:752944c7ffbfdd10c074dc58ec2d5a8a4cd9493b314d367c14d24c17684ddd14", size = 160779, upload-time = "2025-10-14T04:41:37.229Z" }, + { url = "https://files.pythonhosted.org/packages/67/ed/331d6b249259ee71ddea93f6f2f0a56cfebd46938bde6fcc6f7b9a3d0e09/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d1f13550535ad8cff21b8d757a3257963e951d96e20ec82ab44bc64aeb62a191", size = 159035, upload-time = "2025-10-14T04:41:38.368Z" }, + { url = "https://files.pythonhosted.org/packages/67/ff/f6b948ca32e4f2a4576aa129d8bed61f2e0543bf9f5f2b7fc3758ed005c9/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ecaae4149d99b1c9e7b88bb03e3221956f68fd6d50be2ef061b2381b61d20838", size = 152542, upload-time = "2025-10-14T04:41:39.862Z" }, + { url = "https://files.pythonhosted.org/packages/16/85/276033dcbcc369eb176594de22728541a925b2632f9716428c851b149e83/charset_normalizer-3.4.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cb6254dc36b47a990e59e1068afacdcd02958bdcce30bb50cc1700a8b9d624a6", size = 149524, upload-time = "2025-10-14T04:41:41.319Z" }, + { url = "https://files.pythonhosted.org/packages/9e/f2/6a2a1f722b6aba37050e626530a46a68f74e63683947a8acff92569f979a/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c8ae8a0f02f57a6e61203a31428fa1d677cbe50c93622b4149d5c0f319c1d19e", size = 150395, upload-time = "2025-10-14T04:41:42.539Z" }, + { url = "https://files.pythonhosted.org/packages/60/bb/2186cb2f2bbaea6338cad15ce23a67f9b0672929744381e28b0592676824/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:47cc91b2f4dd2833fddaedd2893006b0106129d4b94fdb6af1f4ce5a9965577c", size = 143680, upload-time = "2025-10-14T04:41:43.661Z" }, + { url = "https://files.pythonhosted.org/packages/7d/a5/bf6f13b772fbb2a90360eb620d52ed8f796f3c5caee8398c3b2eb7b1c60d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:82004af6c302b5d3ab2cfc4cc5f29db16123b1a8417f2e25f9066f91d4411090", size = 162045, upload-time = "2025-10-14T04:41:44.821Z" }, + { url = "https://files.pythonhosted.org/packages/df/c5/d1be898bf0dc3ef9030c3825e5d3b83f2c528d207d246cbabe245966808d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2b7d8f6c26245217bd2ad053761201e9f9680f8ce52f0fcd8d0755aeae5b2152", size = 149687, upload-time = "2025-10-14T04:41:46.442Z" }, + { url = "https://files.pythonhosted.org/packages/a5/42/90c1f7b9341eef50c8a1cb3f098ac43b0508413f33affd762855f67a410e/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:799a7a5e4fb2d5898c60b640fd4981d6a25f1c11790935a44ce38c54e985f828", size = 160014, upload-time = "2025-10-14T04:41:47.631Z" }, + { url = "https://files.pythonhosted.org/packages/76/be/4d3ee471e8145d12795ab655ece37baed0929462a86e72372fd25859047c/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:99ae2cffebb06e6c22bdc25801d7b30f503cc87dbd283479e7b606f70aff57ec", size = 154044, upload-time = "2025-10-14T04:41:48.81Z" }, + { url = "https://files.pythonhosted.org/packages/b0/6f/8f7af07237c34a1defe7defc565a9bc1807762f672c0fde711a4b22bf9c0/charset_normalizer-3.4.4-cp314-cp314-win32.whl", hash = "sha256:f9d332f8c2a2fcbffe1378594431458ddbef721c1769d78e2cbc06280d8155f9", size = 99940, upload-time = "2025-10-14T04:41:49.946Z" }, + { url = "https://files.pythonhosted.org/packages/4b/51/8ade005e5ca5b0d80fb4aff72a3775b325bdc3d27408c8113811a7cbe640/charset_normalizer-3.4.4-cp314-cp314-win_amd64.whl", hash = "sha256:8a6562c3700cce886c5be75ade4a5db4214fda19fede41d9792d100288d8f94c", size = 107104, upload-time = "2025-10-14T04:41:51.051Z" }, + { url = "https://files.pythonhosted.org/packages/da/5f/6b8f83a55bb8278772c5ae54a577f3099025f9ade59d0136ac24a0df4bde/charset_normalizer-3.4.4-cp314-cp314-win_arm64.whl", hash = "sha256:de00632ca48df9daf77a2c65a484531649261ec9f25489917f09e455cb09ddb2", size = 100743, upload-time = "2025-10-14T04:41:52.122Z" }, + { url = "https://files.pythonhosted.org/packages/0a/4c/925909008ed5a988ccbb72dcc897407e5d6d3bd72410d69e051fc0c14647/charset_normalizer-3.4.4-py3-none-any.whl", hash = "sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f", size = 53402, upload-time = "2025-10-14T04:42:31.76Z" }, ] [[package]] @@ -394,18 +378,18 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/3d/fa/656b739db8587d7b5dfa22e22ed02566950fbfbcdc20311993483657a5c0/click-8.3.1.tar.gz", hash = "sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a", size = 295065 } +sdist = { url = "https://files.pythonhosted.org/packages/3d/fa/656b739db8587d7b5dfa22e22ed02566950fbfbcdc20311993483657a5c0/click-8.3.1.tar.gz", hash = "sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a", size = 295065, upload-time = "2025-11-15T20:45:42.706Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/98/78/01c019cdb5d6498122777c1a43056ebb3ebfeef2076d9d026bfe15583b2b/click-8.3.1-py3-none-any.whl", hash = "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6", size = 108274 }, + { url = "https://files.pythonhosted.org/packages/98/78/01c019cdb5d6498122777c1a43056ebb3ebfeef2076d9d026bfe15583b2b/click-8.3.1-py3-none-any.whl", hash = "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6", size = 108274, upload-time = "2025-11-15T20:45:41.139Z" }, ] [[package]] name = "cloudpickle" version = "3.1.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/27/fb/576f067976d320f5f0114a8d9fa1215425441bb35627b1993e5afd8111e5/cloudpickle-3.1.2.tar.gz", hash = "sha256:7fda9eb655c9c230dab534f1983763de5835249750e85fbcef43aaa30a9a2414", size = 22330 } +sdist = { url = "https://files.pythonhosted.org/packages/27/fb/576f067976d320f5f0114a8d9fa1215425441bb35627b1993e5afd8111e5/cloudpickle-3.1.2.tar.gz", hash = "sha256:7fda9eb655c9c230dab534f1983763de5835249750e85fbcef43aaa30a9a2414", size = 22330, upload-time = "2025-11-03T09:25:26.604Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/88/39/799be3f2f0f38cc727ee3b4f1445fe6d5e4133064ec2e4115069418a5bb6/cloudpickle-3.1.2-py3-none-any.whl", hash = "sha256:9acb47f6afd73f60dc1df93bb801b472f05ff42fa6c84167d25cb206be1fbf4a", size = 22228 }, + { url = "https://files.pythonhosted.org/packages/88/39/799be3f2f0f38cc727ee3b4f1445fe6d5e4133064ec2e4115069418a5bb6/cloudpickle-3.1.2-py3-none-any.whl", hash = "sha256:9acb47f6afd73f60dc1df93bb801b472f05ff42fa6c84167d25cb206be1fbf4a", size = 22228, upload-time = "2025-11-03T09:25:25.534Z" }, ] [[package]] @@ -422,18 +406,18 @@ dependencies = [ { name = "types-requests" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/4b/ed/bb02083654bdc089ae4ef1cd7691fd2233f1fd9f32bcbfacc80ff57d9775/cohere-5.20.1.tar.gz", hash = "sha256:50973f63d2c6138ff52ce37d8d6f78ccc539af4e8c43865e960d68e0bf835b6f", size = 180820 } +sdist = { url = "https://files.pythonhosted.org/packages/4b/ed/bb02083654bdc089ae4ef1cd7691fd2233f1fd9f32bcbfacc80ff57d9775/cohere-5.20.1.tar.gz", hash = "sha256:50973f63d2c6138ff52ce37d8d6f78ccc539af4e8c43865e960d68e0bf835b6f", size = 180820, upload-time = "2025-12-18T16:39:50.975Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/7a/e3/94eb11ac3ebaaa3a6afb5d2ff23db95d58bc468ae538c388edf49f2f20b5/cohere-5.20.1-py3-none-any.whl", hash = "sha256:d230fd13d95ba92ae927fce3dd497599b169883afc7954fe29b39fb8d5df5fc7", size = 318973 }, + { url = "https://files.pythonhosted.org/packages/7a/e3/94eb11ac3ebaaa3a6afb5d2ff23db95d58bc468ae538c388edf49f2f20b5/cohere-5.20.1-py3-none-any.whl", hash = "sha256:d230fd13d95ba92ae927fce3dd497599b169883afc7954fe29b39fb8d5df5fc7", size = 318973, upload-time = "2025-12-18T16:39:49.504Z" }, ] [[package]] name = "colorama" version = "0.4.6" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697 } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 }, + { 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]] @@ -443,53 +427,53 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/9f/33/c00162f49c0e2fe8064a62cb92b93e50c74a72bc370ab92f86112b33ff62/cryptography-46.0.3.tar.gz", hash = "sha256:a8b17438104fed022ce745b362294d9ce35b4c2e45c1d958ad4a4b019285f4a1", size = 749258 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/1d/42/9c391dd801d6cf0d561b5890549d4b27bafcc53b39c31a817e69d87c625b/cryptography-46.0.3-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:109d4ddfadf17e8e7779c39f9b18111a09efb969a301a31e987416a0191ed93a", size = 7225004 }, - { url = "https://files.pythonhosted.org/packages/1c/67/38769ca6b65f07461eb200e85fc1639b438bdc667be02cf7f2cd6a64601c/cryptography-46.0.3-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:09859af8466b69bc3c27bdf4f5d84a665e0f7ab5088412e9e2ec49758eca5cbc", size = 4296667 }, - { url = "https://files.pythonhosted.org/packages/5c/49/498c86566a1d80e978b42f0d702795f69887005548c041636df6ae1ca64c/cryptography-46.0.3-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:01ca9ff2885f3acc98c29f1860552e37f6d7c7d013d7334ff2a9de43a449315d", size = 4450807 }, - { url = "https://files.pythonhosted.org/packages/4b/0a/863a3604112174c8624a2ac3c038662d9e59970c7f926acdcfaed8d61142/cryptography-46.0.3-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:6eae65d4c3d33da080cff9c4ab1f711b15c1d9760809dad6ea763f3812d254cb", size = 4299615 }, - { url = "https://files.pythonhosted.org/packages/64/02/b73a533f6b64a69f3cd3872acb6ebc12aef924d8d103133bb3ea750dc703/cryptography-46.0.3-cp311-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e5bf0ed4490068a2e72ac03d786693adeb909981cc596425d09032d372bcc849", size = 4016800 }, - { url = "https://files.pythonhosted.org/packages/25/d5/16e41afbfa450cde85a3b7ec599bebefaef16b5c6ba4ec49a3532336ed72/cryptography-46.0.3-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:5ecfccd2329e37e9b7112a888e76d9feca2347f12f37918facbb893d7bb88ee8", size = 4984707 }, - { url = "https://files.pythonhosted.org/packages/c9/56/e7e69b427c3878352c2fb9b450bd0e19ed552753491d39d7d0a2f5226d41/cryptography-46.0.3-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:a2c0cd47381a3229c403062f764160d57d4d175e022c1df84e168c6251a22eec", size = 4482541 }, - { url = "https://files.pythonhosted.org/packages/78/f6/50736d40d97e8483172f1bb6e698895b92a223dba513b0ca6f06b2365339/cryptography-46.0.3-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:549e234ff32571b1f4076ac269fcce7a808d3bf98b76c8dd560e42dbc66d7d91", size = 4299464 }, - { url = "https://files.pythonhosted.org/packages/00/de/d8e26b1a855f19d9994a19c702fa2e93b0456beccbcfe437eda00e0701f2/cryptography-46.0.3-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:c0a7bb1a68a5d3471880e264621346c48665b3bf1c3759d682fc0864c540bd9e", size = 4950838 }, - { url = "https://files.pythonhosted.org/packages/8f/29/798fc4ec461a1c9e9f735f2fc58741b0daae30688f41b2497dcbc9ed1355/cryptography-46.0.3-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:10b01676fc208c3e6feeb25a8b83d81767e8059e1fe86e1dc62d10a3018fa926", size = 4481596 }, - { url = "https://files.pythonhosted.org/packages/15/8d/03cd48b20a573adfff7652b76271078e3045b9f49387920e7f1f631d125e/cryptography-46.0.3-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:0abf1ffd6e57c67e92af68330d05760b7b7efb243aab8377e583284dbab72c71", size = 4426782 }, - { url = "https://files.pythonhosted.org/packages/fa/b1/ebacbfe53317d55cf33165bda24c86523497a6881f339f9aae5c2e13e57b/cryptography-46.0.3-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a04bee9ab6a4da801eb9b51f1b708a1b5b5c9eb48c03f74198464c66f0d344ac", size = 4698381 }, - { url = "https://files.pythonhosted.org/packages/96/92/8a6a9525893325fc057a01f654d7efc2c64b9de90413adcf605a85744ff4/cryptography-46.0.3-cp311-abi3-win32.whl", hash = "sha256:f260d0d41e9b4da1ed1e0f1ce571f97fe370b152ab18778e9e8f67d6af432018", size = 3055988 }, - { url = "https://files.pythonhosted.org/packages/7e/bf/80fbf45253ea585a1e492a6a17efcb93467701fa79e71550a430c5e60df0/cryptography-46.0.3-cp311-abi3-win_amd64.whl", hash = "sha256:a9a3008438615669153eb86b26b61e09993921ebdd75385ddd748702c5adfddb", size = 3514451 }, - { url = "https://files.pythonhosted.org/packages/2e/af/9b302da4c87b0beb9db4e756386a7c6c5b8003cd0e742277888d352ae91d/cryptography-46.0.3-cp311-abi3-win_arm64.whl", hash = "sha256:5d7f93296ee28f68447397bf5198428c9aeeab45705a55d53a6343455dcb2c3c", size = 2928007 }, - { url = "https://files.pythonhosted.org/packages/f5/e2/a510aa736755bffa9d2f75029c229111a1d02f8ecd5de03078f4c18d91a3/cryptography-46.0.3-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:00a5e7e87938e5ff9ff5447ab086a5706a957137e6e433841e9d24f38a065217", size = 7158012 }, - { url = "https://files.pythonhosted.org/packages/73/dc/9aa866fbdbb95b02e7f9d086f1fccfeebf8953509b87e3f28fff927ff8a0/cryptography-46.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c8daeb2d2174beb4575b77482320303f3d39b8e81153da4f0fb08eb5fe86a6c5", size = 4288728 }, - { url = "https://files.pythonhosted.org/packages/c5/fd/bc1daf8230eaa075184cbbf5f8cd00ba9db4fd32d63fb83da4671b72ed8a/cryptography-46.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:39b6755623145ad5eff1dab323f4eae2a32a77a7abef2c5089a04a3d04366715", size = 4435078 }, - { url = "https://files.pythonhosted.org/packages/82/98/d3bd5407ce4c60017f8ff9e63ffee4200ab3e23fe05b765cab805a7db008/cryptography-46.0.3-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:db391fa7c66df6762ee3f00c95a89e6d428f4d60e7abc8328f4fe155b5ac6e54", size = 4293460 }, - { url = "https://files.pythonhosted.org/packages/26/e9/e23e7900983c2b8af7a08098db406cf989d7f09caea7897e347598d4cd5b/cryptography-46.0.3-cp314-cp314t-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:78a97cf6a8839a48c49271cdcbd5cf37ca2c1d6b7fdd86cc864f302b5e9bf459", size = 3995237 }, - { url = "https://files.pythonhosted.org/packages/91/15/af68c509d4a138cfe299d0d7ddb14afba15233223ebd933b4bbdbc7155d3/cryptography-46.0.3-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:dfb781ff7eaa91a6f7fd41776ec37c5853c795d3b358d4896fdbb5df168af422", size = 4967344 }, - { url = "https://files.pythonhosted.org/packages/ca/e3/8643d077c53868b681af077edf6b3cb58288b5423610f21c62aadcbe99f4/cryptography-46.0.3-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:6f61efb26e76c45c4a227835ddeae96d83624fb0d29eb5df5b96e14ed1a0afb7", size = 4466564 }, - { url = "https://files.pythonhosted.org/packages/0e/43/c1e8726fa59c236ff477ff2b5dc071e54b21e5a1e51aa2cee1676f1c986f/cryptography-46.0.3-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:23b1a8f26e43f47ceb6d6a43115f33a5a37d57df4ea0ca295b780ae8546e8044", size = 4292415 }, - { url = "https://files.pythonhosted.org/packages/42/f9/2f8fefdb1aee8a8e3256a0568cffc4e6d517b256a2fe97a029b3f1b9fe7e/cryptography-46.0.3-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:b419ae593c86b87014b9be7396b385491ad7f320bde96826d0dd174459e54665", size = 4931457 }, - { url = "https://files.pythonhosted.org/packages/79/30/9b54127a9a778ccd6d27c3da7563e9f2d341826075ceab89ae3b41bf5be2/cryptography-46.0.3-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:50fc3343ac490c6b08c0cf0d704e881d0d660be923fd3076db3e932007e726e3", size = 4466074 }, - { url = "https://files.pythonhosted.org/packages/ac/68/b4f4a10928e26c941b1b6a179143af9f4d27d88fe84a6a3c53592d2e76bf/cryptography-46.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:22d7e97932f511d6b0b04f2bfd818d73dcd5928db509460aaf48384778eb6d20", size = 4420569 }, - { url = "https://files.pythonhosted.org/packages/a3/49/3746dab4c0d1979888f125226357d3262a6dd40e114ac29e3d2abdf1ec55/cryptography-46.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:d55f3dffadd674514ad19451161118fd010988540cee43d8bc20675e775925de", size = 4681941 }, - { url = "https://files.pythonhosted.org/packages/fd/30/27654c1dbaf7e4a3531fa1fc77986d04aefa4d6d78259a62c9dc13d7ad36/cryptography-46.0.3-cp314-cp314t-win32.whl", hash = "sha256:8a6e050cb6164d3f830453754094c086ff2d0b2f3a897a1d9820f6139a1f0914", size = 3022339 }, - { url = "https://files.pythonhosted.org/packages/f6/30/640f34ccd4d2a1bc88367b54b926b781b5a018d65f404d409aba76a84b1c/cryptography-46.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:760f83faa07f8b64e9c33fc963d790a2edb24efb479e3520c14a45741cd9b2db", size = 3494315 }, - { url = "https://files.pythonhosted.org/packages/ba/8b/88cc7e3bd0a8e7b861f26981f7b820e1f46aa9d26cc482d0feba0ecb4919/cryptography-46.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:516ea134e703e9fe26bcd1277a4b59ad30586ea90c365a87781d7887a646fe21", size = 2919331 }, - { url = "https://files.pythonhosted.org/packages/fd/23/45fe7f376a7df8daf6da3556603b36f53475a99ce4faacb6ba2cf3d82021/cryptography-46.0.3-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:cb3d760a6117f621261d662bccc8ef5bc32ca673e037c83fbe565324f5c46936", size = 7218248 }, - { url = "https://files.pythonhosted.org/packages/27/32/b68d27471372737054cbd34c84981f9edbc24fe67ca225d389799614e27f/cryptography-46.0.3-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:4b7387121ac7d15e550f5cb4a43aef2559ed759c35df7336c402bb8275ac9683", size = 4294089 }, - { url = "https://files.pythonhosted.org/packages/26/42/fa8389d4478368743e24e61eea78846a0006caffaf72ea24a15159215a14/cryptography-46.0.3-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:15ab9b093e8f09daab0f2159bb7e47532596075139dd74365da52ecc9cb46c5d", size = 4440029 }, - { url = "https://files.pythonhosted.org/packages/5f/eb/f483db0ec5ac040824f269e93dd2bd8a21ecd1027e77ad7bdf6914f2fd80/cryptography-46.0.3-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:46acf53b40ea38f9c6c229599a4a13f0d46a6c3fa9ef19fc1a124d62e338dfa0", size = 4297222 }, - { url = "https://files.pythonhosted.org/packages/fd/cf/da9502c4e1912cb1da3807ea3618a6829bee8207456fbbeebc361ec38ba3/cryptography-46.0.3-cp38-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:10ca84c4668d066a9878890047f03546f3ae0a6b8b39b697457b7757aaf18dbc", size = 4012280 }, - { url = "https://files.pythonhosted.org/packages/6b/8f/9adb86b93330e0df8b3dcf03eae67c33ba89958fc2e03862ef1ac2b42465/cryptography-46.0.3-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:36e627112085bb3b81b19fed209c05ce2a52ee8b15d161b7c643a7d5a88491f3", size = 4978958 }, - { url = "https://files.pythonhosted.org/packages/d1/a0/5fa77988289c34bdb9f913f5606ecc9ada1adb5ae870bd0d1054a7021cc4/cryptography-46.0.3-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:1000713389b75c449a6e979ffc7dcc8ac90b437048766cef052d4d30b8220971", size = 4473714 }, - { url = "https://files.pythonhosted.org/packages/14/e5/fc82d72a58d41c393697aa18c9abe5ae1214ff6f2a5c18ac470f92777895/cryptography-46.0.3-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:b02cf04496f6576afffef5ddd04a0cb7d49cf6be16a9059d793a30b035f6b6ac", size = 4296970 }, - { url = "https://files.pythonhosted.org/packages/78/06/5663ed35438d0b09056973994f1aec467492b33bd31da36e468b01ec1097/cryptography-46.0.3-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:71e842ec9bc7abf543b47cf86b9a743baa95f4677d22baa4c7d5c69e49e9bc04", size = 4940236 }, - { url = "https://files.pythonhosted.org/packages/fc/59/873633f3f2dcd8a053b8dd1d38f783043b5fce589c0f6988bf55ef57e43e/cryptography-46.0.3-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:402b58fc32614f00980b66d6e56a5b4118e6cb362ae8f3fda141ba4689bd4506", size = 4472642 }, - { url = "https://files.pythonhosted.org/packages/3d/39/8e71f3930e40f6877737d6f69248cf74d4e34b886a3967d32f919cc50d3b/cryptography-46.0.3-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:ef639cb3372f69ec44915fafcd6698b6cc78fbe0c2ea41be867f6ed612811963", size = 4423126 }, - { url = "https://files.pythonhosted.org/packages/cd/c7/f65027c2810e14c3e7268353b1681932b87e5a48e65505d8cc17c99e36ae/cryptography-46.0.3-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:3b51b8ca4f1c6453d8829e1eb7299499ca7f313900dd4d89a24b8b87c0a780d4", size = 4686573 }, - { url = "https://files.pythonhosted.org/packages/0a/6e/1c8331ddf91ca4730ab3086a0f1be19c65510a33b5a441cb334e7a2d2560/cryptography-46.0.3-cp38-abi3-win32.whl", hash = "sha256:6276eb85ef938dc035d59b87c8a7dc559a232f954962520137529d77b18ff1df", size = 3036695 }, - { url = "https://files.pythonhosted.org/packages/90/45/b0d691df20633eff80955a0fc7695ff9051ffce8b69741444bd9ed7bd0db/cryptography-46.0.3-cp38-abi3-win_amd64.whl", hash = "sha256:416260257577718c05135c55958b674000baef9a1c7d9e8f306ec60d71db850f", size = 3501720 }, - { url = "https://files.pythonhosted.org/packages/e8/cb/2da4cc83f5edb9c3257d09e1e7ab7b23f049c7962cae8d842bbef0a9cec9/cryptography-46.0.3-cp38-abi3-win_arm64.whl", hash = "sha256:d89c3468de4cdc4f08a57e214384d0471911a3830fcdaf7a8cc587e42a866372", size = 2918740 }, +sdist = { url = "https://files.pythonhosted.org/packages/9f/33/c00162f49c0e2fe8064a62cb92b93e50c74a72bc370ab92f86112b33ff62/cryptography-46.0.3.tar.gz", hash = "sha256:a8b17438104fed022ce745b362294d9ce35b4c2e45c1d958ad4a4b019285f4a1", size = 749258, upload-time = "2025-10-15T23:18:31.74Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1d/42/9c391dd801d6cf0d561b5890549d4b27bafcc53b39c31a817e69d87c625b/cryptography-46.0.3-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:109d4ddfadf17e8e7779c39f9b18111a09efb969a301a31e987416a0191ed93a", size = 7225004, upload-time = "2025-10-15T23:16:52.239Z" }, + { url = "https://files.pythonhosted.org/packages/1c/67/38769ca6b65f07461eb200e85fc1639b438bdc667be02cf7f2cd6a64601c/cryptography-46.0.3-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:09859af8466b69bc3c27bdf4f5d84a665e0f7ab5088412e9e2ec49758eca5cbc", size = 4296667, upload-time = "2025-10-15T23:16:54.369Z" }, + { url = "https://files.pythonhosted.org/packages/5c/49/498c86566a1d80e978b42f0d702795f69887005548c041636df6ae1ca64c/cryptography-46.0.3-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:01ca9ff2885f3acc98c29f1860552e37f6d7c7d013d7334ff2a9de43a449315d", size = 4450807, upload-time = "2025-10-15T23:16:56.414Z" }, + { url = "https://files.pythonhosted.org/packages/4b/0a/863a3604112174c8624a2ac3c038662d9e59970c7f926acdcfaed8d61142/cryptography-46.0.3-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:6eae65d4c3d33da080cff9c4ab1f711b15c1d9760809dad6ea763f3812d254cb", size = 4299615, upload-time = "2025-10-15T23:16:58.442Z" }, + { url = "https://files.pythonhosted.org/packages/64/02/b73a533f6b64a69f3cd3872acb6ebc12aef924d8d103133bb3ea750dc703/cryptography-46.0.3-cp311-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e5bf0ed4490068a2e72ac03d786693adeb909981cc596425d09032d372bcc849", size = 4016800, upload-time = "2025-10-15T23:17:00.378Z" }, + { url = "https://files.pythonhosted.org/packages/25/d5/16e41afbfa450cde85a3b7ec599bebefaef16b5c6ba4ec49a3532336ed72/cryptography-46.0.3-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:5ecfccd2329e37e9b7112a888e76d9feca2347f12f37918facbb893d7bb88ee8", size = 4984707, upload-time = "2025-10-15T23:17:01.98Z" }, + { url = "https://files.pythonhosted.org/packages/c9/56/e7e69b427c3878352c2fb9b450bd0e19ed552753491d39d7d0a2f5226d41/cryptography-46.0.3-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:a2c0cd47381a3229c403062f764160d57d4d175e022c1df84e168c6251a22eec", size = 4482541, upload-time = "2025-10-15T23:17:04.078Z" }, + { url = "https://files.pythonhosted.org/packages/78/f6/50736d40d97e8483172f1bb6e698895b92a223dba513b0ca6f06b2365339/cryptography-46.0.3-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:549e234ff32571b1f4076ac269fcce7a808d3bf98b76c8dd560e42dbc66d7d91", size = 4299464, upload-time = "2025-10-15T23:17:05.483Z" }, + { url = "https://files.pythonhosted.org/packages/00/de/d8e26b1a855f19d9994a19c702fa2e93b0456beccbcfe437eda00e0701f2/cryptography-46.0.3-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:c0a7bb1a68a5d3471880e264621346c48665b3bf1c3759d682fc0864c540bd9e", size = 4950838, upload-time = "2025-10-15T23:17:07.425Z" }, + { url = "https://files.pythonhosted.org/packages/8f/29/798fc4ec461a1c9e9f735f2fc58741b0daae30688f41b2497dcbc9ed1355/cryptography-46.0.3-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:10b01676fc208c3e6feeb25a8b83d81767e8059e1fe86e1dc62d10a3018fa926", size = 4481596, upload-time = "2025-10-15T23:17:09.343Z" }, + { url = "https://files.pythonhosted.org/packages/15/8d/03cd48b20a573adfff7652b76271078e3045b9f49387920e7f1f631d125e/cryptography-46.0.3-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:0abf1ffd6e57c67e92af68330d05760b7b7efb243aab8377e583284dbab72c71", size = 4426782, upload-time = "2025-10-15T23:17:11.22Z" }, + { url = "https://files.pythonhosted.org/packages/fa/b1/ebacbfe53317d55cf33165bda24c86523497a6881f339f9aae5c2e13e57b/cryptography-46.0.3-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a04bee9ab6a4da801eb9b51f1b708a1b5b5c9eb48c03f74198464c66f0d344ac", size = 4698381, upload-time = "2025-10-15T23:17:12.829Z" }, + { url = "https://files.pythonhosted.org/packages/96/92/8a6a9525893325fc057a01f654d7efc2c64b9de90413adcf605a85744ff4/cryptography-46.0.3-cp311-abi3-win32.whl", hash = "sha256:f260d0d41e9b4da1ed1e0f1ce571f97fe370b152ab18778e9e8f67d6af432018", size = 3055988, upload-time = "2025-10-15T23:17:14.65Z" }, + { url = "https://files.pythonhosted.org/packages/7e/bf/80fbf45253ea585a1e492a6a17efcb93467701fa79e71550a430c5e60df0/cryptography-46.0.3-cp311-abi3-win_amd64.whl", hash = "sha256:a9a3008438615669153eb86b26b61e09993921ebdd75385ddd748702c5adfddb", size = 3514451, upload-time = "2025-10-15T23:17:16.142Z" }, + { url = "https://files.pythonhosted.org/packages/2e/af/9b302da4c87b0beb9db4e756386a7c6c5b8003cd0e742277888d352ae91d/cryptography-46.0.3-cp311-abi3-win_arm64.whl", hash = "sha256:5d7f93296ee28f68447397bf5198428c9aeeab45705a55d53a6343455dcb2c3c", size = 2928007, upload-time = "2025-10-15T23:17:18.04Z" }, + { url = "https://files.pythonhosted.org/packages/f5/e2/a510aa736755bffa9d2f75029c229111a1d02f8ecd5de03078f4c18d91a3/cryptography-46.0.3-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:00a5e7e87938e5ff9ff5447ab086a5706a957137e6e433841e9d24f38a065217", size = 7158012, upload-time = "2025-10-15T23:17:19.982Z" }, + { url = "https://files.pythonhosted.org/packages/73/dc/9aa866fbdbb95b02e7f9d086f1fccfeebf8953509b87e3f28fff927ff8a0/cryptography-46.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c8daeb2d2174beb4575b77482320303f3d39b8e81153da4f0fb08eb5fe86a6c5", size = 4288728, upload-time = "2025-10-15T23:17:21.527Z" }, + { url = "https://files.pythonhosted.org/packages/c5/fd/bc1daf8230eaa075184cbbf5f8cd00ba9db4fd32d63fb83da4671b72ed8a/cryptography-46.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:39b6755623145ad5eff1dab323f4eae2a32a77a7abef2c5089a04a3d04366715", size = 4435078, upload-time = "2025-10-15T23:17:23.042Z" }, + { url = "https://files.pythonhosted.org/packages/82/98/d3bd5407ce4c60017f8ff9e63ffee4200ab3e23fe05b765cab805a7db008/cryptography-46.0.3-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:db391fa7c66df6762ee3f00c95a89e6d428f4d60e7abc8328f4fe155b5ac6e54", size = 4293460, upload-time = "2025-10-15T23:17:24.885Z" }, + { url = "https://files.pythonhosted.org/packages/26/e9/e23e7900983c2b8af7a08098db406cf989d7f09caea7897e347598d4cd5b/cryptography-46.0.3-cp314-cp314t-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:78a97cf6a8839a48c49271cdcbd5cf37ca2c1d6b7fdd86cc864f302b5e9bf459", size = 3995237, upload-time = "2025-10-15T23:17:26.449Z" }, + { url = "https://files.pythonhosted.org/packages/91/15/af68c509d4a138cfe299d0d7ddb14afba15233223ebd933b4bbdbc7155d3/cryptography-46.0.3-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:dfb781ff7eaa91a6f7fd41776ec37c5853c795d3b358d4896fdbb5df168af422", size = 4967344, upload-time = "2025-10-15T23:17:28.06Z" }, + { url = "https://files.pythonhosted.org/packages/ca/e3/8643d077c53868b681af077edf6b3cb58288b5423610f21c62aadcbe99f4/cryptography-46.0.3-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:6f61efb26e76c45c4a227835ddeae96d83624fb0d29eb5df5b96e14ed1a0afb7", size = 4466564, upload-time = "2025-10-15T23:17:29.665Z" }, + { url = "https://files.pythonhosted.org/packages/0e/43/c1e8726fa59c236ff477ff2b5dc071e54b21e5a1e51aa2cee1676f1c986f/cryptography-46.0.3-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:23b1a8f26e43f47ceb6d6a43115f33a5a37d57df4ea0ca295b780ae8546e8044", size = 4292415, upload-time = "2025-10-15T23:17:31.686Z" }, + { url = "https://files.pythonhosted.org/packages/42/f9/2f8fefdb1aee8a8e3256a0568cffc4e6d517b256a2fe97a029b3f1b9fe7e/cryptography-46.0.3-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:b419ae593c86b87014b9be7396b385491ad7f320bde96826d0dd174459e54665", size = 4931457, upload-time = "2025-10-15T23:17:33.478Z" }, + { url = "https://files.pythonhosted.org/packages/79/30/9b54127a9a778ccd6d27c3da7563e9f2d341826075ceab89ae3b41bf5be2/cryptography-46.0.3-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:50fc3343ac490c6b08c0cf0d704e881d0d660be923fd3076db3e932007e726e3", size = 4466074, upload-time = "2025-10-15T23:17:35.158Z" }, + { url = "https://files.pythonhosted.org/packages/ac/68/b4f4a10928e26c941b1b6a179143af9f4d27d88fe84a6a3c53592d2e76bf/cryptography-46.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:22d7e97932f511d6b0b04f2bfd818d73dcd5928db509460aaf48384778eb6d20", size = 4420569, upload-time = "2025-10-15T23:17:37.188Z" }, + { url = "https://files.pythonhosted.org/packages/a3/49/3746dab4c0d1979888f125226357d3262a6dd40e114ac29e3d2abdf1ec55/cryptography-46.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:d55f3dffadd674514ad19451161118fd010988540cee43d8bc20675e775925de", size = 4681941, upload-time = "2025-10-15T23:17:39.236Z" }, + { url = "https://files.pythonhosted.org/packages/fd/30/27654c1dbaf7e4a3531fa1fc77986d04aefa4d6d78259a62c9dc13d7ad36/cryptography-46.0.3-cp314-cp314t-win32.whl", hash = "sha256:8a6e050cb6164d3f830453754094c086ff2d0b2f3a897a1d9820f6139a1f0914", size = 3022339, upload-time = "2025-10-15T23:17:40.888Z" }, + { url = "https://files.pythonhosted.org/packages/f6/30/640f34ccd4d2a1bc88367b54b926b781b5a018d65f404d409aba76a84b1c/cryptography-46.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:760f83faa07f8b64e9c33fc963d790a2edb24efb479e3520c14a45741cd9b2db", size = 3494315, upload-time = "2025-10-15T23:17:42.769Z" }, + { url = "https://files.pythonhosted.org/packages/ba/8b/88cc7e3bd0a8e7b861f26981f7b820e1f46aa9d26cc482d0feba0ecb4919/cryptography-46.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:516ea134e703e9fe26bcd1277a4b59ad30586ea90c365a87781d7887a646fe21", size = 2919331, upload-time = "2025-10-15T23:17:44.468Z" }, + { url = "https://files.pythonhosted.org/packages/fd/23/45fe7f376a7df8daf6da3556603b36f53475a99ce4faacb6ba2cf3d82021/cryptography-46.0.3-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:cb3d760a6117f621261d662bccc8ef5bc32ca673e037c83fbe565324f5c46936", size = 7218248, upload-time = "2025-10-15T23:17:46.294Z" }, + { url = "https://files.pythonhosted.org/packages/27/32/b68d27471372737054cbd34c84981f9edbc24fe67ca225d389799614e27f/cryptography-46.0.3-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:4b7387121ac7d15e550f5cb4a43aef2559ed759c35df7336c402bb8275ac9683", size = 4294089, upload-time = "2025-10-15T23:17:48.269Z" }, + { url = "https://files.pythonhosted.org/packages/26/42/fa8389d4478368743e24e61eea78846a0006caffaf72ea24a15159215a14/cryptography-46.0.3-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:15ab9b093e8f09daab0f2159bb7e47532596075139dd74365da52ecc9cb46c5d", size = 4440029, upload-time = "2025-10-15T23:17:49.837Z" }, + { url = "https://files.pythonhosted.org/packages/5f/eb/f483db0ec5ac040824f269e93dd2bd8a21ecd1027e77ad7bdf6914f2fd80/cryptography-46.0.3-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:46acf53b40ea38f9c6c229599a4a13f0d46a6c3fa9ef19fc1a124d62e338dfa0", size = 4297222, upload-time = "2025-10-15T23:17:51.357Z" }, + { url = "https://files.pythonhosted.org/packages/fd/cf/da9502c4e1912cb1da3807ea3618a6829bee8207456fbbeebc361ec38ba3/cryptography-46.0.3-cp38-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:10ca84c4668d066a9878890047f03546f3ae0a6b8b39b697457b7757aaf18dbc", size = 4012280, upload-time = "2025-10-15T23:17:52.964Z" }, + { url = "https://files.pythonhosted.org/packages/6b/8f/9adb86b93330e0df8b3dcf03eae67c33ba89958fc2e03862ef1ac2b42465/cryptography-46.0.3-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:36e627112085bb3b81b19fed209c05ce2a52ee8b15d161b7c643a7d5a88491f3", size = 4978958, upload-time = "2025-10-15T23:17:54.965Z" }, + { url = "https://files.pythonhosted.org/packages/d1/a0/5fa77988289c34bdb9f913f5606ecc9ada1adb5ae870bd0d1054a7021cc4/cryptography-46.0.3-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:1000713389b75c449a6e979ffc7dcc8ac90b437048766cef052d4d30b8220971", size = 4473714, upload-time = "2025-10-15T23:17:56.754Z" }, + { url = "https://files.pythonhosted.org/packages/14/e5/fc82d72a58d41c393697aa18c9abe5ae1214ff6f2a5c18ac470f92777895/cryptography-46.0.3-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:b02cf04496f6576afffef5ddd04a0cb7d49cf6be16a9059d793a30b035f6b6ac", size = 4296970, upload-time = "2025-10-15T23:17:58.588Z" }, + { url = "https://files.pythonhosted.org/packages/78/06/5663ed35438d0b09056973994f1aec467492b33bd31da36e468b01ec1097/cryptography-46.0.3-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:71e842ec9bc7abf543b47cf86b9a743baa95f4677d22baa4c7d5c69e49e9bc04", size = 4940236, upload-time = "2025-10-15T23:18:00.897Z" }, + { url = "https://files.pythonhosted.org/packages/fc/59/873633f3f2dcd8a053b8dd1d38f783043b5fce589c0f6988bf55ef57e43e/cryptography-46.0.3-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:402b58fc32614f00980b66d6e56a5b4118e6cb362ae8f3fda141ba4689bd4506", size = 4472642, upload-time = "2025-10-15T23:18:02.749Z" }, + { url = "https://files.pythonhosted.org/packages/3d/39/8e71f3930e40f6877737d6f69248cf74d4e34b886a3967d32f919cc50d3b/cryptography-46.0.3-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:ef639cb3372f69ec44915fafcd6698b6cc78fbe0c2ea41be867f6ed612811963", size = 4423126, upload-time = "2025-10-15T23:18:04.85Z" }, + { url = "https://files.pythonhosted.org/packages/cd/c7/f65027c2810e14c3e7268353b1681932b87e5a48e65505d8cc17c99e36ae/cryptography-46.0.3-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:3b51b8ca4f1c6453d8829e1eb7299499ca7f313900dd4d89a24b8b87c0a780d4", size = 4686573, upload-time = "2025-10-15T23:18:06.908Z" }, + { url = "https://files.pythonhosted.org/packages/0a/6e/1c8331ddf91ca4730ab3086a0f1be19c65510a33b5a441cb334e7a2d2560/cryptography-46.0.3-cp38-abi3-win32.whl", hash = "sha256:6276eb85ef938dc035d59b87c8a7dc559a232f954962520137529d77b18ff1df", size = 3036695, upload-time = "2025-10-15T23:18:08.672Z" }, + { url = "https://files.pythonhosted.org/packages/90/45/b0d691df20633eff80955a0fc7695ff9051ffce8b69741444bd9ed7bd0db/cryptography-46.0.3-cp38-abi3-win_amd64.whl", hash = "sha256:416260257577718c05135c55958b674000baef9a1c7d9e8f306ec60d71db850f", size = 3501720, upload-time = "2025-10-15T23:18:10.632Z" }, + { url = "https://files.pythonhosted.org/packages/e8/cb/2da4cc83f5edb9c3257d09e1e7ab7b23f049c7962cae8d842bbef0a9cec9/cryptography-46.0.3-cp38-abi3-win_arm64.whl", hash = "sha256:d89c3468de4cdc4f08a57e214384d0471911a3830fcdaf7a8cc587e42a866372", size = 2918740, upload-time = "2025-10-15T23:18:12.277Z" }, ] [[package]] @@ -502,9 +486,9 @@ dependencies = [ { name = "rich" }, { name = "rich-rst" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/40/99/e1b75193ee23bd10a05a3b90c065d419b1c8c18f61cae6b8218c7158f792/cyclopts-4.4.1.tar.gz", hash = "sha256:368a404926b46a49dc328a33ccd7e55ba879296a28e64a42afe2f6667704cecf", size = 159245 } +sdist = { url = "https://files.pythonhosted.org/packages/40/99/e1b75193ee23bd10a05a3b90c065d419b1c8c18f61cae6b8218c7158f792/cyclopts-4.4.1.tar.gz", hash = "sha256:368a404926b46a49dc328a33ccd7e55ba879296a28e64a42afe2f6667704cecf", size = 159245, upload-time = "2025-12-21T13:59:02.266Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/8d/05/8efadba80e1296526e69c1dceba8b0f0bc3756e8d69f6ed9b0e647cf3169/cyclopts-4.4.1-py3-none-any.whl", hash = "sha256:67500e9fde90f335fddbf9c452d2e7c4f58209dffe52e7abb1e272796a963bde", size = 196726 }, + { url = "https://files.pythonhosted.org/packages/8d/05/8efadba80e1296526e69c1dceba8b0f0bc3756e8d69f6ed9b0e647cf3169/cyclopts-4.4.1-py3-none-any.whl", hash = "sha256:67500e9fde90f335fddbf9c452d2e7c4f58209dffe52e7abb1e272796a963bde", size = 196726, upload-time = "2025-12-21T13:59:03.127Z" }, ] [[package]] @@ -519,63 +503,63 @@ dependencies = [ { name = "typer-slim" }, { name = "websockets" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/1a/46/1159b06dfd1dd0407e188174a8e42f796daa8c4393654e8f956048c8574e/dbos-2.7.0.tar.gz", hash = "sha256:0ad5a9bf33be2c403946603c3d980f59dd4ca2858d393c912305e471575fd978", size = 219458 } +sdist = { url = "https://files.pythonhosted.org/packages/1a/46/1159b06dfd1dd0407e188174a8e42f796daa8c4393654e8f956048c8574e/dbos-2.7.0.tar.gz", hash = "sha256:0ad5a9bf33be2c403946603c3d980f59dd4ca2858d393c912305e471575fd978", size = 219458, upload-time = "2025-12-11T15:58:25.112Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/49/57/80c23cc65a6af2c740096b1a15544dd299e8fd4ab942846ad8c7938f9b9d/dbos-2.7.0-py3-none-any.whl", hash = "sha256:70cd823de89e6cb99031ddf02c2750ed4ef9dfc207ad3a36fd7f2e3e507c5195", size = 139958 }, + { url = "https://files.pythonhosted.org/packages/49/57/80c23cc65a6af2c740096b1a15544dd299e8fd4ab942846ad8c7938f9b9d/dbos-2.7.0-py3-none-any.whl", hash = "sha256:70cd823de89e6cb99031ddf02c2750ed4ef9dfc207ad3a36fd7f2e3e507c5195", size = 139958, upload-time = "2025-12-11T15:58:23.153Z" }, ] [[package]] name = "diskcache" version = "5.6.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/3f/21/1c1ffc1a039ddcc459db43cc108658f32c57d271d7289a2794e401d0fdb6/diskcache-5.6.3.tar.gz", hash = "sha256:2c3a3fa2743d8535d832ec61c2054a1641f41775aa7c556758a109941e33e4fc", size = 67916 } +sdist = { url = "https://files.pythonhosted.org/packages/3f/21/1c1ffc1a039ddcc459db43cc108658f32c57d271d7289a2794e401d0fdb6/diskcache-5.6.3.tar.gz", hash = "sha256:2c3a3fa2743d8535d832ec61c2054a1641f41775aa7c556758a109941e33e4fc", size = 67916, upload-time = "2023-08-31T06:12:00.316Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/3f/27/4570e78fc0bf5ea0ca45eb1de3818a23787af9b390c0b0a0033a1b8236f9/diskcache-5.6.3-py3-none-any.whl", hash = "sha256:5e31b2d5fbad117cc363ebaf6b689474db18a1f6438bc82358b024abd4c2ca19", size = 45550 }, + { url = "https://files.pythonhosted.org/packages/3f/27/4570e78fc0bf5ea0ca45eb1de3818a23787af9b390c0b0a0033a1b8236f9/diskcache-5.6.3-py3-none-any.whl", hash = "sha256:5e31b2d5fbad117cc363ebaf6b689474db18a1f6438bc82358b024abd4c2ca19", size = 45550, upload-time = "2023-08-31T06:11:58.822Z" }, ] [[package]] name = "distro" version = "1.9.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/fc/f8/98eea607f65de6527f8a2e8885fc8015d3e6f5775df186e443e0964a11c3/distro-1.9.0.tar.gz", hash = "sha256:2fa77c6fd8940f116ee1d6b94a2f90b13b5ea8d019b98bc8bafdcabcdd9bdbed", size = 60722 } +sdist = { url = "https://files.pythonhosted.org/packages/fc/f8/98eea607f65de6527f8a2e8885fc8015d3e6f5775df186e443e0964a11c3/distro-1.9.0.tar.gz", hash = "sha256:2fa77c6fd8940f116ee1d6b94a2f90b13b5ea8d019b98bc8bafdcabcdd9bdbed", size = 60722, upload-time = "2023-12-24T09:54:32.31Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/12/b3/231ffd4ab1fc9d679809f356cebee130ac7daa00d6d6f3206dd4fd137e9e/distro-1.9.0-py3-none-any.whl", hash = "sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2", size = 20277 }, + { url = "https://files.pythonhosted.org/packages/12/b3/231ffd4ab1fc9d679809f356cebee130ac7daa00d6d6f3206dd4fd137e9e/distro-1.9.0-py3-none-any.whl", hash = "sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2", size = 20277, upload-time = "2023-12-24T09:54:30.421Z" }, ] [[package]] name = "dnspython" version = "2.8.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/8c/8b/57666417c0f90f08bcafa776861060426765fdb422eb10212086fb811d26/dnspython-2.8.0.tar.gz", hash = "sha256:181d3c6996452cb1189c4046c61599b84a5a86e099562ffde77d26984ff26d0f", size = 368251 } +sdist = { url = "https://files.pythonhosted.org/packages/8c/8b/57666417c0f90f08bcafa776861060426765fdb422eb10212086fb811d26/dnspython-2.8.0.tar.gz", hash = "sha256:181d3c6996452cb1189c4046c61599b84a5a86e099562ffde77d26984ff26d0f", size = 368251, upload-time = "2025-09-07T18:58:00.022Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ba/5a/18ad964b0086c6e62e2e7500f7edc89e3faa45033c71c1893d34eed2b2de/dnspython-2.8.0-py3-none-any.whl", hash = "sha256:01d9bbc4a2d76bf0db7c1f729812ded6d912bd318d3b1cf81d30c0f845dbf3af", size = 331094 }, + { url = "https://files.pythonhosted.org/packages/ba/5a/18ad964b0086c6e62e2e7500f7edc89e3faa45033c71c1893d34eed2b2de/dnspython-2.8.0-py3-none-any.whl", hash = "sha256:01d9bbc4a2d76bf0db7c1f729812ded6d912bd318d3b1cf81d30c0f845dbf3af", size = 331094, upload-time = "2025-09-07T18:57:58.071Z" }, ] [[package]] name = "docstring-parser" version = "0.17.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b2/9d/c3b43da9515bd270df0f80548d9944e389870713cc1fe2b8fb35fe2bcefd/docstring_parser-0.17.0.tar.gz", hash = "sha256:583de4a309722b3315439bb31d64ba3eebada841f2e2cee23b99df001434c912", size = 27442 } +sdist = { url = "https://files.pythonhosted.org/packages/b2/9d/c3b43da9515bd270df0f80548d9944e389870713cc1fe2b8fb35fe2bcefd/docstring_parser-0.17.0.tar.gz", hash = "sha256:583de4a309722b3315439bb31d64ba3eebada841f2e2cee23b99df001434c912", size = 27442, upload-time = "2025-07-21T07:35:01.868Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/55/e2/2537ebcff11c1ee1ff17d8d0b6f4db75873e3b0fb32c2d4a2ee31ecb310a/docstring_parser-0.17.0-py3-none-any.whl", hash = "sha256:cf2569abd23dce8099b300f9b4fa8191e9582dda731fd533daf54c4551658708", size = 36896 }, + { url = "https://files.pythonhosted.org/packages/55/e2/2537ebcff11c1ee1ff17d8d0b6f4db75873e3b0fb32c2d4a2ee31ecb310a/docstring_parser-0.17.0-py3-none-any.whl", hash = "sha256:cf2569abd23dce8099b300f9b4fa8191e9582dda731fd533daf54c4551658708", size = 36896, upload-time = "2025-07-21T07:35:00.684Z" }, ] [[package]] name = "docutils" version = "0.22.4" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ae/b6/03bb70946330e88ffec97aefd3ea75ba575cb2e762061e0e62a213befee8/docutils-0.22.4.tar.gz", hash = "sha256:4db53b1fde9abecbb74d91230d32ab626d94f6badfc575d6db9194a49df29968", size = 2291750 } +sdist = { url = "https://files.pythonhosted.org/packages/ae/b6/03bb70946330e88ffec97aefd3ea75ba575cb2e762061e0e62a213befee8/docutils-0.22.4.tar.gz", hash = "sha256:4db53b1fde9abecbb74d91230d32ab626d94f6badfc575d6db9194a49df29968", size = 2291750, upload-time = "2025-12-18T19:00:26.443Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/02/10/5da547df7a391dcde17f59520a231527b8571e6f46fc8efb02ccb370ab12/docutils-0.22.4-py3-none-any.whl", hash = "sha256:d0013f540772d1420576855455d050a2180186c91c15779301ac2ccb3eeb68de", size = 633196 }, + { url = "https://files.pythonhosted.org/packages/02/10/5da547df7a391dcde17f59520a231527b8571e6f46fc8efb02ccb370ab12/docutils-0.22.4-py3-none-any.whl", hash = "sha256:d0013f540772d1420576855455d050a2180186c91c15779301ac2ccb3eeb68de", size = 633196, upload-time = "2025-12-18T19:00:18.077Z" }, ] [[package]] name = "durationpy" version = "0.10" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/9d/a4/e44218c2b394e31a6dd0d6b095c4e1f32d0be54c2a4b250032d717647bab/durationpy-0.10.tar.gz", hash = "sha256:1fa6893409a6e739c9c72334fc65cca1f355dbdd93405d30f726deb5bde42fba", size = 3335 } +sdist = { url = "https://files.pythonhosted.org/packages/9d/a4/e44218c2b394e31a6dd0d6b095c4e1f32d0be54c2a4b250032d717647bab/durationpy-0.10.tar.gz", hash = "sha256:1fa6893409a6e739c9c72334fc65cca1f355dbdd93405d30f726deb5bde42fba", size = 3335, upload-time = "2025-05-17T13:52:37.26Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b0/0d/9feae160378a3553fa9a339b0e9c1a048e147a4127210e286ef18b730f03/durationpy-0.10-py3-none-any.whl", hash = "sha256:3b41e1b601234296b4fb368338fdcd3e13e0b4fb5b67345948f4f2bf9868b286", size = 3922 }, + { url = "https://files.pythonhosted.org/packages/b0/0d/9feae160378a3553fa9a339b0e9c1a048e147a4127210e286ef18b730f03/durationpy-0.10-py3-none-any.whl", hash = "sha256:3b41e1b601234296b4fb368338fdcd3e13e0b4fb5b67345948f4f2bf9868b286", size = 3922, upload-time = "2025-05-17T13:52:36.463Z" }, ] [[package]] @@ -586,36 +570,36 @@ dependencies = [ { name = "dnspython" }, { name = "idna" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/f5/22/900cb125c76b7aaa450ce02fd727f452243f2e91a61af068b40adba60ea9/email_validator-2.3.0.tar.gz", hash = "sha256:9fc05c37f2f6cf439ff414f8fc46d917929974a82244c20eb10231ba60c54426", size = 51238 } +sdist = { url = "https://files.pythonhosted.org/packages/f5/22/900cb125c76b7aaa450ce02fd727f452243f2e91a61af068b40adba60ea9/email_validator-2.3.0.tar.gz", hash = "sha256:9fc05c37f2f6cf439ff414f8fc46d917929974a82244c20eb10231ba60c54426", size = 51238, upload-time = "2025-08-26T13:09:06.831Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/de/15/545e2b6cf2e3be84bc1ed85613edd75b8aea69807a71c26f4ca6a9258e82/email_validator-2.3.0-py3-none-any.whl", hash = "sha256:80f13f623413e6b197ae73bb10bf4eb0908faf509ad8362c5edeb0be7fd450b4", size = 35604 }, + { url = "https://files.pythonhosted.org/packages/de/15/545e2b6cf2e3be84bc1ed85613edd75b8aea69807a71c26f4ca6a9258e82/email_validator-2.3.0-py3-none-any.whl", hash = "sha256:80f13f623413e6b197ae73bb10bf4eb0908faf509ad8362c5edeb0be7fd450b4", size = 35604, upload-time = "2025-08-26T13:09:05.858Z" }, ] [[package]] name = "eval-type-backport" version = "0.3.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/fb/a3/cafafb4558fd638aadfe4121dc6cefb8d743368c085acb2f521df0f3d9d7/eval_type_backport-0.3.1.tar.gz", hash = "sha256:57e993f7b5b69d271e37482e62f74e76a0276c82490cf8e4f0dffeb6b332d5ed", size = 9445 } +sdist = { url = "https://files.pythonhosted.org/packages/fb/a3/cafafb4558fd638aadfe4121dc6cefb8d743368c085acb2f521df0f3d9d7/eval_type_backport-0.3.1.tar.gz", hash = "sha256:57e993f7b5b69d271e37482e62f74e76a0276c82490cf8e4f0dffeb6b332d5ed", size = 9445, upload-time = "2025-12-02T11:51:42.987Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/cf/22/fdc2e30d43ff853720042fa15baa3e6122722be1a7950a98233ebb55cd71/eval_type_backport-0.3.1-py3-none-any.whl", hash = "sha256:279ab641905e9f11129f56a8a78f493518515b83402b860f6f06dd7c011fdfa8", size = 6063 }, + { url = "https://files.pythonhosted.org/packages/cf/22/fdc2e30d43ff853720042fa15baa3e6122722be1a7950a98233ebb55cd71/eval_type_backport-0.3.1-py3-none-any.whl", hash = "sha256:279ab641905e9f11129f56a8a78f493518515b83402b860f6f06dd7c011fdfa8", size = 6063, upload-time = "2025-12-02T11:51:41.665Z" }, ] [[package]] name = "exceptiongroup" version = "1.3.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371 } +sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740 }, + { url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740, upload-time = "2025-11-21T23:01:53.443Z" }, ] [[package]] name = "executing" version = "2.2.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/cc/28/c14e053b6762b1044f34a13aab6859bbf40456d37d23aa286ac24cfd9a5d/executing-2.2.1.tar.gz", hash = "sha256:3632cc370565f6648cc328b32435bd120a1e4ebb20c77e3fdde9a13cd1e533c4", size = 1129488 } +sdist = { url = "https://files.pythonhosted.org/packages/cc/28/c14e053b6762b1044f34a13aab6859bbf40456d37d23aa286ac24cfd9a5d/executing-2.2.1.tar.gz", hash = "sha256:3632cc370565f6648cc328b32435bd120a1e4ebb20c77e3fdde9a13cd1e533c4", size = 1129488, upload-time = "2025-09-01T09:48:10.866Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c1/ea/53f2148663b321f21b5a606bd5f191517cf40b7072c0497d3c92c4a13b1e/executing-2.2.1-py2.py3-none-any.whl", hash = "sha256:760643d3452b4d777d295bb167ccc74c64a81df23fb5e08eff250c425a4b2017", size = 28317 }, + { url = "https://files.pythonhosted.org/packages/c1/ea/53f2148663b321f21b5a606bd5f191517cf40b7072c0497d3c92c4a13b1e/executing-2.2.1-py2.py3-none-any.whl", hash = "sha256:760643d3452b4d777d295bb167ccc74c64a81df23fb5e08eff250c425a4b2017", size = 28317, upload-time = "2025-09-01T09:48:08.5Z" }, ] [[package]] @@ -626,9 +610,9 @@ dependencies = [ { name = "redis" }, { name = "sortedcontainers" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/5f/f9/57464119936414d60697fcbd32f38909bb5688b616ae13de6e98384433e0/fakeredis-2.33.0.tar.gz", hash = "sha256:d7bc9a69d21df108a6451bbffee23b3eba432c21a654afc7ff2d295428ec5770", size = 175187 } +sdist = { url = "https://files.pythonhosted.org/packages/5f/f9/57464119936414d60697fcbd32f38909bb5688b616ae13de6e98384433e0/fakeredis-2.33.0.tar.gz", hash = "sha256:d7bc9a69d21df108a6451bbffee23b3eba432c21a654afc7ff2d295428ec5770", size = 175187, upload-time = "2025-12-16T19:45:52.269Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/6e/78/a850fed8aeef96d4a99043c90b818b2ed5419cd5b24a4049fd7cfb9f1471/fakeredis-2.33.0-py3-none-any.whl", hash = "sha256:de535f3f9ccde1c56672ab2fdd6a8efbc4f2619fc2f1acc87b8737177d71c965", size = 119605 }, + { url = "https://files.pythonhosted.org/packages/6e/78/a850fed8aeef96d4a99043c90b818b2ed5419cd5b24a4049fd7cfb9f1471/fakeredis-2.33.0-py3-none-any.whl", hash = "sha256:de535f3f9ccde1c56672ab2fdd6a8efbc4f2619fc2f1acc87b8737177d71c965", size = 119605, upload-time = "2025-12-16T19:45:51.08Z" }, ] [package.optional-dependencies] @@ -646,38 +630,38 @@ dependencies = [ { name = "starlette" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/17/71/2df15009fb4bdd522a069d2fbca6007c6c5487fce5cb965be00fc335f1d1/fastapi-0.125.0.tar.gz", hash = "sha256:16b532691a33e2c5dee1dac32feb31dc6eb41a3dd4ff29a95f9487cb21c054c0", size = 370550 } +sdist = { url = "https://files.pythonhosted.org/packages/17/71/2df15009fb4bdd522a069d2fbca6007c6c5487fce5cb965be00fc335f1d1/fastapi-0.125.0.tar.gz", hash = "sha256:16b532691a33e2c5dee1dac32feb31dc6eb41a3dd4ff29a95f9487cb21c054c0", size = 370550, upload-time = "2025-12-17T21:41:44.15Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/34/2f/ff2fcc98f500713368d8b650e1bbc4a0b3ebcdd3e050dcdaad5f5a13fd7e/fastapi-0.125.0-py3-none-any.whl", hash = "sha256:2570ec4f3aecf5cca8f0428aed2398b774fcdfee6c2116f86e80513f2f86a7a1", size = 112888 }, + { url = "https://files.pythonhosted.org/packages/34/2f/ff2fcc98f500713368d8b650e1bbc4a0b3ebcdd3e050dcdaad5f5a13fd7e/fastapi-0.125.0-py3-none-any.whl", hash = "sha256:2570ec4f3aecf5cca8f0428aed2398b774fcdfee6c2116f86e80513f2f86a7a1", size = 112888, upload-time = "2025-12-17T21:41:41.286Z" }, ] [[package]] name = "fastavro" version = "1.12.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/65/8b/fa2d3287fd2267be6261d0177c6809a7fa12c5600ddb33490c8dc29e77b2/fastavro-1.12.1.tar.gz", hash = "sha256:2f285be49e45bc047ab2f6bed040bb349da85db3f3c87880e4b92595ea093b2b", size = 1025661 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/bb/57/26d5efef9182392d5ac9f253953c856ccb66e4c549fd3176a1e94efb05c9/fastavro-1.12.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:78df838351e4dff9edd10a1c41d1324131ffecbadefb9c297d612ef5363c049a", size = 1000599 }, - { url = "https://files.pythonhosted.org/packages/33/cb/8ab55b21d018178eb126007a56bde14fd01c0afc11d20b5f2624fe01e698/fastavro-1.12.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:780476c23175d2ae457c52f45b9ffa9d504593499a36cd3c1929662bf5b7b14b", size = 3335933 }, - { url = "https://files.pythonhosted.org/packages/fe/03/9c94ec9bf873eb1ffb0aa694f4e71940154e6e9728ddfdc46046d7e8ced4/fastavro-1.12.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0714b285160fcd515eb0455540f40dd6dac93bdeacdb03f24e8eac3d8aa51f8d", size = 3402066 }, - { url = "https://files.pythonhosted.org/packages/75/c8/cb472347c5a584ccb8777a649ebb28278fccea39d005fc7df19996f41df8/fastavro-1.12.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a8bc2dcec5843d499f2489bfe0747999108f78c5b29295d877379f1972a3d41a", size = 3240038 }, - { url = "https://files.pythonhosted.org/packages/e1/77/569ce9474c40304b3a09e109494e020462b83e405545b78069ddba5f614e/fastavro-1.12.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:3b1921ac35f3d89090a5816b626cf46e67dbecf3f054131f84d56b4e70496f45", size = 3369398 }, - { url = "https://files.pythonhosted.org/packages/4a/1f/9589e35e9ea68035385db7bdbf500d36b8891db474063fb1ccc8215ee37c/fastavro-1.12.1-cp313-cp313-win_amd64.whl", hash = "sha256:5aa777b8ee595b50aa084104cd70670bf25a7bbb9fd8bb5d07524b0785ee1699", size = 444220 }, - { url = "https://files.pythonhosted.org/packages/6c/d2/78435fe737df94bd8db2234b2100f5453737cffd29adee2504a2b013de84/fastavro-1.12.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:c3d67c47f177e486640404a56f2f50b165fe892cc343ac3a34673b80cc7f1dd6", size = 1086611 }, - { url = "https://files.pythonhosted.org/packages/b6/be/428f99b10157230ddac77ec8cc167005b29e2bd5cbe228345192bb645f30/fastavro-1.12.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5217f773492bac43dae15ff2931432bce2d7a80be7039685a78d3fab7df910bd", size = 3541001 }, - { url = "https://files.pythonhosted.org/packages/16/08/a2eea4f20b85897740efe44887e1ac08f30dfa4bfc3de8962bdcbb21a5a1/fastavro-1.12.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:469fecb25cba07f2e1bfa4c8d008477cd6b5b34a59d48715e1b1a73f6160097d", size = 3432217 }, - { url = "https://files.pythonhosted.org/packages/87/bb/b4c620b9eb6e9838c7f7e4b7be0762834443adf9daeb252a214e9ad3178c/fastavro-1.12.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:d71c8aa841ef65cfab709a22bb887955f42934bced3ddb571e98fdbdade4c609", size = 3366742 }, - { url = "https://files.pythonhosted.org/packages/3d/d1/e69534ccdd5368350646fea7d93be39e5f77c614cca825c990bd9ca58f67/fastavro-1.12.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:b81fc04e85dfccf7c028e0580c606e33aa8472370b767ef058aae2c674a90746", size = 3383743 }, - { url = "https://files.pythonhosted.org/packages/58/54/b7b4a0c3fb5fcba38128542da1b26c4e6d69933c923f493548bdfd63ab6a/fastavro-1.12.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:9445da127751ba65975d8e4bdabf36bfcfdad70fc35b2d988e3950cce0ec0e7c", size = 1001377 }, - { url = "https://files.pythonhosted.org/packages/1e/4f/0e589089c7df0d8f57d7e5293fdc34efec9a3b758a0d4d0c99a7937e2492/fastavro-1.12.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ed924233272719b5d5a6a0b4d80ef3345fc7e84fc7a382b6232192a9112d38a6", size = 3320401 }, - { url = "https://files.pythonhosted.org/packages/f9/19/260110d56194ae29d7e423a336fccea8bcd103196d00f0b364b732bdb84e/fastavro-1.12.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3616e2f0e1c9265e92954fa099db79c6e7817356d3ff34f4bcc92699ae99697c", size = 3350894 }, - { url = "https://files.pythonhosted.org/packages/d0/96/58b0411e8be9694d5972bee3167d6c1fd1fdfdf7ce253c1a19a327208f4f/fastavro-1.12.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:cb0337b42fd3c047fcf0e9b7597bd6ad25868de719f29da81eabb6343f08d399", size = 3229644 }, - { url = "https://files.pythonhosted.org/packages/5b/db/38660660eac82c30471d9101f45b3acfdcbadfe42d8f7cdb129459a45050/fastavro-1.12.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:64961ab15b74b7c168717bbece5660e0f3d457837c3cc9d9145181d011199fa7", size = 3329704 }, - { url = "https://files.pythonhosted.org/packages/9d/a9/1672910f458ecb30b596c9e59e41b7c00309b602a0494341451e92e62747/fastavro-1.12.1-cp314-cp314-win_amd64.whl", hash = "sha256:792356d320f6e757e89f7ac9c22f481e546c886454a6709247f43c0dd7058004", size = 452911 }, - { url = "https://files.pythonhosted.org/packages/dc/8d/2e15d0938ded1891b33eff252e8500605508b799c2e57188a933f0bd744c/fastavro-1.12.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:120aaf82ac19d60a1016afe410935fe94728752d9c2d684e267e5b7f0e70f6d9", size = 3541999 }, - { url = "https://files.pythonhosted.org/packages/a7/1c/6dfd082a205be4510543221b734b1191299e6a1810c452b6bc76dfa6968e/fastavro-1.12.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b6a3462934b20a74f9ece1daa49c2e4e749bd9a35fa2657b53bf62898fba80f5", size = 3433972 }, - { url = "https://files.pythonhosted.org/packages/24/90/9de694625a1a4b727b1ad0958d220cab25a9b6cf7f16a5c7faa9ea7b2261/fastavro-1.12.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:1f81011d54dd47b12437b51dd93a70a9aa17b61307abf26542fc3c13efbc6c51", size = 3368752 }, - { url = "https://files.pythonhosted.org/packages/fa/93/b44f67589e4d439913dab6720f7e3507b0fa8b8e56d06f6fc875ced26afb/fastavro-1.12.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:43ded16b3f4a9f1a42f5970c2aa618acb23ea59c4fcaa06680bdf470b255e5a8", size = 3386636 }, +sdist = { url = "https://files.pythonhosted.org/packages/65/8b/fa2d3287fd2267be6261d0177c6809a7fa12c5600ddb33490c8dc29e77b2/fastavro-1.12.1.tar.gz", hash = "sha256:2f285be49e45bc047ab2f6bed040bb349da85db3f3c87880e4b92595ea093b2b", size = 1025661, upload-time = "2025-10-10T15:40:55.41Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bb/57/26d5efef9182392d5ac9f253953c856ccb66e4c549fd3176a1e94efb05c9/fastavro-1.12.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:78df838351e4dff9edd10a1c41d1324131ffecbadefb9c297d612ef5363c049a", size = 1000599, upload-time = "2025-10-10T15:41:36.554Z" }, + { url = "https://files.pythonhosted.org/packages/33/cb/8ab55b21d018178eb126007a56bde14fd01c0afc11d20b5f2624fe01e698/fastavro-1.12.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:780476c23175d2ae457c52f45b9ffa9d504593499a36cd3c1929662bf5b7b14b", size = 3335933, upload-time = "2025-10-10T15:41:39.07Z" }, + { url = "https://files.pythonhosted.org/packages/fe/03/9c94ec9bf873eb1ffb0aa694f4e71940154e6e9728ddfdc46046d7e8ced4/fastavro-1.12.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0714b285160fcd515eb0455540f40dd6dac93bdeacdb03f24e8eac3d8aa51f8d", size = 3402066, upload-time = "2025-10-10T15:41:41.608Z" }, + { url = "https://files.pythonhosted.org/packages/75/c8/cb472347c5a584ccb8777a649ebb28278fccea39d005fc7df19996f41df8/fastavro-1.12.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a8bc2dcec5843d499f2489bfe0747999108f78c5b29295d877379f1972a3d41a", size = 3240038, upload-time = "2025-10-10T15:41:43.743Z" }, + { url = "https://files.pythonhosted.org/packages/e1/77/569ce9474c40304b3a09e109494e020462b83e405545b78069ddba5f614e/fastavro-1.12.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:3b1921ac35f3d89090a5816b626cf46e67dbecf3f054131f84d56b4e70496f45", size = 3369398, upload-time = "2025-10-10T15:41:45.719Z" }, + { url = "https://files.pythonhosted.org/packages/4a/1f/9589e35e9ea68035385db7bdbf500d36b8891db474063fb1ccc8215ee37c/fastavro-1.12.1-cp313-cp313-win_amd64.whl", hash = "sha256:5aa777b8ee595b50aa084104cd70670bf25a7bbb9fd8bb5d07524b0785ee1699", size = 444220, upload-time = "2025-10-10T15:41:47.39Z" }, + { url = "https://files.pythonhosted.org/packages/6c/d2/78435fe737df94bd8db2234b2100f5453737cffd29adee2504a2b013de84/fastavro-1.12.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:c3d67c47f177e486640404a56f2f50b165fe892cc343ac3a34673b80cc7f1dd6", size = 1086611, upload-time = "2025-10-10T15:41:48.818Z" }, + { url = "https://files.pythonhosted.org/packages/b6/be/428f99b10157230ddac77ec8cc167005b29e2bd5cbe228345192bb645f30/fastavro-1.12.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5217f773492bac43dae15ff2931432bce2d7a80be7039685a78d3fab7df910bd", size = 3541001, upload-time = "2025-10-10T15:41:50.871Z" }, + { url = "https://files.pythonhosted.org/packages/16/08/a2eea4f20b85897740efe44887e1ac08f30dfa4bfc3de8962bdcbb21a5a1/fastavro-1.12.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:469fecb25cba07f2e1bfa4c8d008477cd6b5b34a59d48715e1b1a73f6160097d", size = 3432217, upload-time = "2025-10-10T15:41:53.149Z" }, + { url = "https://files.pythonhosted.org/packages/87/bb/b4c620b9eb6e9838c7f7e4b7be0762834443adf9daeb252a214e9ad3178c/fastavro-1.12.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:d71c8aa841ef65cfab709a22bb887955f42934bced3ddb571e98fdbdade4c609", size = 3366742, upload-time = "2025-10-10T15:41:55.237Z" }, + { url = "https://files.pythonhosted.org/packages/3d/d1/e69534ccdd5368350646fea7d93be39e5f77c614cca825c990bd9ca58f67/fastavro-1.12.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:b81fc04e85dfccf7c028e0580c606e33aa8472370b767ef058aae2c674a90746", size = 3383743, upload-time = "2025-10-10T15:41:57.68Z" }, + { url = "https://files.pythonhosted.org/packages/58/54/b7b4a0c3fb5fcba38128542da1b26c4e6d69933c923f493548bdfd63ab6a/fastavro-1.12.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:9445da127751ba65975d8e4bdabf36bfcfdad70fc35b2d988e3950cce0ec0e7c", size = 1001377, upload-time = "2025-10-10T15:41:59.241Z" }, + { url = "https://files.pythonhosted.org/packages/1e/4f/0e589089c7df0d8f57d7e5293fdc34efec9a3b758a0d4d0c99a7937e2492/fastavro-1.12.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ed924233272719b5d5a6a0b4d80ef3345fc7e84fc7a382b6232192a9112d38a6", size = 3320401, upload-time = "2025-10-10T15:42:01.682Z" }, + { url = "https://files.pythonhosted.org/packages/f9/19/260110d56194ae29d7e423a336fccea8bcd103196d00f0b364b732bdb84e/fastavro-1.12.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3616e2f0e1c9265e92954fa099db79c6e7817356d3ff34f4bcc92699ae99697c", size = 3350894, upload-time = "2025-10-10T15:42:04.073Z" }, + { url = "https://files.pythonhosted.org/packages/d0/96/58b0411e8be9694d5972bee3167d6c1fd1fdfdf7ce253c1a19a327208f4f/fastavro-1.12.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:cb0337b42fd3c047fcf0e9b7597bd6ad25868de719f29da81eabb6343f08d399", size = 3229644, upload-time = "2025-10-10T15:42:06.221Z" }, + { url = "https://files.pythonhosted.org/packages/5b/db/38660660eac82c30471d9101f45b3acfdcbadfe42d8f7cdb129459a45050/fastavro-1.12.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:64961ab15b74b7c168717bbece5660e0f3d457837c3cc9d9145181d011199fa7", size = 3329704, upload-time = "2025-10-10T15:42:08.384Z" }, + { url = "https://files.pythonhosted.org/packages/9d/a9/1672910f458ecb30b596c9e59e41b7c00309b602a0494341451e92e62747/fastavro-1.12.1-cp314-cp314-win_amd64.whl", hash = "sha256:792356d320f6e757e89f7ac9c22f481e546c886454a6709247f43c0dd7058004", size = 452911, upload-time = "2025-10-10T15:42:09.795Z" }, + { url = "https://files.pythonhosted.org/packages/dc/8d/2e15d0938ded1891b33eff252e8500605508b799c2e57188a933f0bd744c/fastavro-1.12.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:120aaf82ac19d60a1016afe410935fe94728752d9c2d684e267e5b7f0e70f6d9", size = 3541999, upload-time = "2025-10-10T15:42:11.794Z" }, + { url = "https://files.pythonhosted.org/packages/a7/1c/6dfd082a205be4510543221b734b1191299e6a1810c452b6bc76dfa6968e/fastavro-1.12.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b6a3462934b20a74f9ece1daa49c2e4e749bd9a35fa2657b53bf62898fba80f5", size = 3433972, upload-time = "2025-10-10T15:42:14.485Z" }, + { url = "https://files.pythonhosted.org/packages/24/90/9de694625a1a4b727b1ad0958d220cab25a9b6cf7f16a5c7faa9ea7b2261/fastavro-1.12.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:1f81011d54dd47b12437b51dd93a70a9aa17b61307abf26542fc3c13efbc6c51", size = 3368752, upload-time = "2025-10-10T15:42:16.618Z" }, + { url = "https://files.pythonhosted.org/packages/fa/93/b44f67589e4d439913dab6720f7e3507b0fa8b8e56d06f6fc875ced26afb/fastavro-1.12.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:43ded16b3f4a9f1a42f5970c2aa618acb23ea59c4fcaa06680bdf470b255e5a8", size = 3386636, upload-time = "2025-10-10T15:42:18.974Z" }, ] [[package]] @@ -702,100 +686,100 @@ dependencies = [ { name = "uvicorn" }, { name = "websockets" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/9e/50/d38e4371bdc34e709f4731b1e882cb7bc50e51c1a224859d4cd381b3a79b/fastmcp-2.14.1.tar.gz", hash = "sha256:132725cbf77b68fa3c3d165eff0cfa47e40c1479457419e6a2cfda65bd84c8d6", size = 8263331 } +sdist = { url = "https://files.pythonhosted.org/packages/9e/50/d38e4371bdc34e709f4731b1e882cb7bc50e51c1a224859d4cd381b3a79b/fastmcp-2.14.1.tar.gz", hash = "sha256:132725cbf77b68fa3c3d165eff0cfa47e40c1479457419e6a2cfda65bd84c8d6", size = 8263331, upload-time = "2025-12-15T02:26:27.102Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/1d/82/72401d09dc27c27fdf72ad6c2fe331e553e3c3646e01b5ff16473191033d/fastmcp-2.14.1-py3-none-any.whl", hash = "sha256:fb3e365cc1d52573ab89caeba9944dd4b056149097be169bce428e011f0a57e5", size = 412176 }, + { url = "https://files.pythonhosted.org/packages/1d/82/72401d09dc27c27fdf72ad6c2fe331e553e3c3646e01b5ff16473191033d/fastmcp-2.14.1-py3-none-any.whl", hash = "sha256:fb3e365cc1d52573ab89caeba9944dd4b056149097be169bce428e011f0a57e5", size = 412176, upload-time = "2025-12-15T02:26:25.356Z" }, ] [[package]] name = "filelock" version = "3.20.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a7/23/ce7a1126827cedeb958fc043d61745754464eb56c5937c35bbf2b8e26f34/filelock-3.20.1.tar.gz", hash = "sha256:b8360948b351b80f420878d8516519a2204b07aefcdcfd24912a5d33127f188c", size = 19476 } +sdist = { url = "https://files.pythonhosted.org/packages/a7/23/ce7a1126827cedeb958fc043d61745754464eb56c5937c35bbf2b8e26f34/filelock-3.20.1.tar.gz", hash = "sha256:b8360948b351b80f420878d8516519a2204b07aefcdcfd24912a5d33127f188c", size = 19476, upload-time = "2025-12-15T23:54:28.027Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e3/7f/a1a97644e39e7316d850784c642093c99df1290a460df4ede27659056834/filelock-3.20.1-py3-none-any.whl", hash = "sha256:15d9e9a67306188a44baa72f569d2bfd803076269365fdea0934385da4dc361a", size = 16666 }, + { url = "https://files.pythonhosted.org/packages/e3/7f/a1a97644e39e7316d850784c642093c99df1290a460df4ede27659056834/filelock-3.20.1-py3-none-any.whl", hash = "sha256:15d9e9a67306188a44baa72f569d2bfd803076269365fdea0934385da4dc361a", size = 16666, upload-time = "2025-12-15T23:54:26.874Z" }, ] [[package]] name = "frozenlist" version = "1.8.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/2d/f5/c831fac6cc817d26fd54c7eaccd04ef7e0288806943f7cc5bbf69f3ac1f0/frozenlist-1.8.0.tar.gz", hash = "sha256:3ede829ed8d842f6cd48fc7081d7a41001a56f1f38603f9d49bf3020d59a31ad", size = 45875 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/2d/40/0832c31a37d60f60ed79e9dfb5a92e1e2af4f40a16a29abcc7992af9edff/frozenlist-1.8.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8d92f1a84bb12d9e56f818b3a746f3efba93c1b63c8387a73dde655e1e42282a", size = 85717 }, - { url = "https://files.pythonhosted.org/packages/30/ba/b0b3de23f40bc55a7057bd38434e25c34fa48e17f20ee273bbde5e0650f3/frozenlist-1.8.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:96153e77a591c8adc2ee805756c61f59fef4cf4073a9275ee86fe8cba41241f7", size = 49651 }, - { url = "https://files.pythonhosted.org/packages/0c/ab/6e5080ee374f875296c4243c381bbdef97a9ac39c6e3ce1d5f7d42cb78d6/frozenlist-1.8.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f21f00a91358803399890ab167098c131ec2ddd5f8f5fd5fe9c9f2c6fcd91e40", size = 49417 }, - { url = "https://files.pythonhosted.org/packages/d5/4e/e4691508f9477ce67da2015d8c00acd751e6287739123113a9fca6f1604e/frozenlist-1.8.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fb30f9626572a76dfe4293c7194a09fb1fe93ba94c7d4f720dfae3b646b45027", size = 234391 }, - { url = "https://files.pythonhosted.org/packages/40/76/c202df58e3acdf12969a7895fd6f3bc016c642e6726aa63bd3025e0fc71c/frozenlist-1.8.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eaa352d7047a31d87dafcacbabe89df0aa506abb5b1b85a2fb91bc3faa02d822", size = 233048 }, - { url = "https://files.pythonhosted.org/packages/f9/c0/8746afb90f17b73ca5979c7a3958116e105ff796e718575175319b5bb4ce/frozenlist-1.8.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:03ae967b4e297f58f8c774c7eabcce57fe3c2434817d4385c50661845a058121", size = 226549 }, - { url = "https://files.pythonhosted.org/packages/7e/eb/4c7eefc718ff72f9b6c4893291abaae5fbc0c82226a32dcd8ef4f7a5dbef/frozenlist-1.8.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f6292f1de555ffcc675941d65fffffb0a5bcd992905015f85d0592201793e0e5", size = 239833 }, - { url = "https://files.pythonhosted.org/packages/c2/4e/e5c02187cf704224f8b21bee886f3d713ca379535f16893233b9d672ea71/frozenlist-1.8.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:29548f9b5b5e3460ce7378144c3010363d8035cea44bc0bf02d57f5a685e084e", size = 245363 }, - { url = "https://files.pythonhosted.org/packages/1f/96/cb85ec608464472e82ad37a17f844889c36100eed57bea094518bf270692/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ec3cc8c5d4084591b4237c0a272cc4f50a5b03396a47d9caaf76f5d7b38a4f11", size = 229314 }, - { url = "https://files.pythonhosted.org/packages/5d/6f/4ae69c550e4cee66b57887daeebe006fe985917c01d0fff9caab9883f6d0/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:517279f58009d0b1f2e7c1b130b377a349405da3f7621ed6bfae50b10adf20c1", size = 243365 }, - { url = "https://files.pythonhosted.org/packages/7a/58/afd56de246cf11780a40a2c28dc7cbabbf06337cc8ddb1c780a2d97e88d8/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:db1e72ede2d0d7ccb213f218df6a078a9c09a7de257c2fe8fcef16d5925230b1", size = 237763 }, - { url = "https://files.pythonhosted.org/packages/cb/36/cdfaf6ed42e2644740d4a10452d8e97fa1c062e2a8006e4b09f1b5fd7d63/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:b4dec9482a65c54a5044486847b8a66bf10c9cb4926d42927ec4e8fd5db7fed8", size = 240110 }, - { url = "https://files.pythonhosted.org/packages/03/a8/9ea226fbefad669f11b52e864c55f0bd57d3c8d7eb07e9f2e9a0b39502e1/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:21900c48ae04d13d416f0e1e0c4d81f7931f73a9dfa0b7a8746fb2fe7dd970ed", size = 233717 }, - { url = "https://files.pythonhosted.org/packages/1e/0b/1b5531611e83ba7d13ccc9988967ea1b51186af64c42b7a7af465dcc9568/frozenlist-1.8.0-cp313-cp313-win32.whl", hash = "sha256:8b7b94a067d1c504ee0b16def57ad5738701e4ba10cec90529f13fa03c833496", size = 39628 }, - { url = "https://files.pythonhosted.org/packages/d8/cf/174c91dbc9cc49bc7b7aab74d8b734e974d1faa8f191c74af9b7e80848e6/frozenlist-1.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:878be833caa6a3821caf85eb39c5ba92d28e85df26d57afb06b35b2efd937231", size = 43882 }, - { url = "https://files.pythonhosted.org/packages/c1/17/502cd212cbfa96eb1388614fe39a3fc9ab87dbbe042b66f97acb57474834/frozenlist-1.8.0-cp313-cp313-win_arm64.whl", hash = "sha256:44389d135b3ff43ba8cc89ff7f51f5a0bb6b63d829c8300f79a2fe4fe61bcc62", size = 39676 }, - { url = "https://files.pythonhosted.org/packages/d2/5c/3bbfaa920dfab09e76946a5d2833a7cbdf7b9b4a91c714666ac4855b88b4/frozenlist-1.8.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:e25ac20a2ef37e91c1b39938b591457666a0fa835c7783c3a8f33ea42870db94", size = 89235 }, - { url = "https://files.pythonhosted.org/packages/d2/d6/f03961ef72166cec1687e84e8925838442b615bd0b8854b54923ce5b7b8a/frozenlist-1.8.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:07cdca25a91a4386d2e76ad992916a85038a9b97561bf7a3fd12d5d9ce31870c", size = 50742 }, - { url = "https://files.pythonhosted.org/packages/1e/bb/a6d12b7ba4c3337667d0e421f7181c82dda448ce4e7ad7ecd249a16fa806/frozenlist-1.8.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4e0c11f2cc6717e0a741f84a527c52616140741cd812a50422f83dc31749fb52", size = 51725 }, - { url = "https://files.pythonhosted.org/packages/bc/71/d1fed0ffe2c2ccd70b43714c6cab0f4188f09f8a67a7914a6b46ee30f274/frozenlist-1.8.0-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b3210649ee28062ea6099cfda39e147fa1bc039583c8ee4481cb7811e2448c51", size = 284533 }, - { url = "https://files.pythonhosted.org/packages/c9/1f/fb1685a7b009d89f9bf78a42d94461bc06581f6e718c39344754a5d9bada/frozenlist-1.8.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:581ef5194c48035a7de2aefc72ac6539823bb71508189e5de01d60c9dcd5fa65", size = 292506 }, - { url = "https://files.pythonhosted.org/packages/e6/3b/b991fe1612703f7e0d05c0cf734c1b77aaf7c7d321df4572e8d36e7048c8/frozenlist-1.8.0-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3ef2d026f16a2b1866e1d86fc4e1291e1ed8a387b2c333809419a2f8b3a77b82", size = 274161 }, - { url = "https://files.pythonhosted.org/packages/ca/ec/c5c618767bcdf66e88945ec0157d7f6c4a1322f1473392319b7a2501ded7/frozenlist-1.8.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5500ef82073f599ac84d888e3a8c1f77ac831183244bfd7f11eaa0289fb30714", size = 294676 }, - { url = "https://files.pythonhosted.org/packages/7c/ce/3934758637d8f8a88d11f0585d6495ef54b2044ed6ec84492a91fa3b27aa/frozenlist-1.8.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:50066c3997d0091c411a66e710f4e11752251e6d2d73d70d8d5d4c76442a199d", size = 300638 }, - { url = "https://files.pythonhosted.org/packages/fc/4f/a7e4d0d467298f42de4b41cbc7ddaf19d3cfeabaf9ff97c20c6c7ee409f9/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:5c1c8e78426e59b3f8005e9b19f6ff46e5845895adbde20ece9218319eca6506", size = 283067 }, - { url = "https://files.pythonhosted.org/packages/dc/48/c7b163063d55a83772b268e6d1affb960771b0e203b632cfe09522d67ea5/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:eefdba20de0d938cec6a89bd4d70f346a03108a19b9df4248d3cf0d88f1b0f51", size = 292101 }, - { url = "https://files.pythonhosted.org/packages/9f/d0/2366d3c4ecdc2fd391e0afa6e11500bfba0ea772764d631bbf82f0136c9d/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:cf253e0e1c3ceb4aaff6df637ce033ff6535fb8c70a764a8f46aafd3d6ab798e", size = 289901 }, - { url = "https://files.pythonhosted.org/packages/b8/94/daff920e82c1b70e3618a2ac39fbc01ae3e2ff6124e80739ce5d71c9b920/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:032efa2674356903cd0261c4317a561a6850f3ac864a63fc1583147fb05a79b0", size = 289395 }, - { url = "https://files.pythonhosted.org/packages/e3/20/bba307ab4235a09fdcd3cc5508dbabd17c4634a1af4b96e0f69bfe551ebd/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6da155091429aeba16851ecb10a9104a108bcd32f6c1642867eadaee401c1c41", size = 283659 }, - { url = "https://files.pythonhosted.org/packages/fd/00/04ca1c3a7a124b6de4f8a9a17cc2fcad138b4608e7a3fc5877804b8715d7/frozenlist-1.8.0-cp313-cp313t-win32.whl", hash = "sha256:0f96534f8bfebc1a394209427d0f8a63d343c9779cda6fc25e8e121b5fd8555b", size = 43492 }, - { url = "https://files.pythonhosted.org/packages/59/5e/c69f733a86a94ab10f68e496dc6b7e8bc078ebb415281d5698313e3af3a1/frozenlist-1.8.0-cp313-cp313t-win_amd64.whl", hash = "sha256:5d63a068f978fc69421fb0e6eb91a9603187527c86b7cd3f534a5b77a592b888", size = 48034 }, - { url = "https://files.pythonhosted.org/packages/16/6c/be9d79775d8abe79b05fa6d23da99ad6e7763a1d080fbae7290b286093fd/frozenlist-1.8.0-cp313-cp313t-win_arm64.whl", hash = "sha256:bf0a7e10b077bf5fb9380ad3ae8ce20ef919a6ad93b4552896419ac7e1d8e042", size = 41749 }, - { url = "https://files.pythonhosted.org/packages/f1/c8/85da824b7e7b9b6e7f7705b2ecaf9591ba6f79c1177f324c2735e41d36a2/frozenlist-1.8.0-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:cee686f1f4cadeb2136007ddedd0aaf928ab95216e7691c63e50a8ec066336d0", size = 86127 }, - { url = "https://files.pythonhosted.org/packages/8e/e8/a1185e236ec66c20afd72399522f142c3724c785789255202d27ae992818/frozenlist-1.8.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:119fb2a1bd47307e899c2fac7f28e85b9a543864df47aa7ec9d3c1b4545f096f", size = 49698 }, - { url = "https://files.pythonhosted.org/packages/a1/93/72b1736d68f03fda5fdf0f2180fb6caaae3894f1b854d006ac61ecc727ee/frozenlist-1.8.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4970ece02dbc8c3a92fcc5228e36a3e933a01a999f7094ff7c23fbd2beeaa67c", size = 49749 }, - { url = "https://files.pythonhosted.org/packages/a7/b2/fabede9fafd976b991e9f1b9c8c873ed86f202889b864756f240ce6dd855/frozenlist-1.8.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:cba69cb73723c3f329622e34bdbf5ce1f80c21c290ff04256cff1cd3c2036ed2", size = 231298 }, - { url = "https://files.pythonhosted.org/packages/3a/3b/d9b1e0b0eed36e70477ffb8360c49c85c8ca8ef9700a4e6711f39a6e8b45/frozenlist-1.8.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:778a11b15673f6f1df23d9586f83c4846c471a8af693a22e066508b77d201ec8", size = 232015 }, - { url = "https://files.pythonhosted.org/packages/dc/94/be719d2766c1138148564a3960fc2c06eb688da592bdc25adcf856101be7/frozenlist-1.8.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0325024fe97f94c41c08872db482cf8ac4800d80e79222c6b0b7b162d5b13686", size = 225038 }, - { url = "https://files.pythonhosted.org/packages/e4/09/6712b6c5465f083f52f50cf74167b92d4ea2f50e46a9eea0523d658454ae/frozenlist-1.8.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:97260ff46b207a82a7567b581ab4190bd4dfa09f4db8a8b49d1a958f6aa4940e", size = 240130 }, - { url = "https://files.pythonhosted.org/packages/f8/d4/cd065cdcf21550b54f3ce6a22e143ac9e4836ca42a0de1022da8498eac89/frozenlist-1.8.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:54b2077180eb7f83dd52c40b2750d0a9f175e06a42e3213ce047219de902717a", size = 242845 }, - { url = "https://files.pythonhosted.org/packages/62/c3/f57a5c8c70cd1ead3d5d5f776f89d33110b1addae0ab010ad774d9a44fb9/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:2f05983daecab868a31e1da44462873306d3cbfd76d1f0b5b69c473d21dbb128", size = 229131 }, - { url = "https://files.pythonhosted.org/packages/6c/52/232476fe9cb64f0742f3fde2b7d26c1dac18b6d62071c74d4ded55e0ef94/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:33f48f51a446114bc5d251fb2954ab0164d5be02ad3382abcbfe07e2531d650f", size = 240542 }, - { url = "https://files.pythonhosted.org/packages/5f/85/07bf3f5d0fb5414aee5f47d33c6f5c77bfe49aac680bfece33d4fdf6a246/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:154e55ec0655291b5dd1b8731c637ecdb50975a2ae70c606d100750a540082f7", size = 237308 }, - { url = "https://files.pythonhosted.org/packages/11/99/ae3a33d5befd41ac0ca2cc7fd3aa707c9c324de2e89db0e0f45db9a64c26/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:4314debad13beb564b708b4a496020e5306c7333fa9a3ab90374169a20ffab30", size = 238210 }, - { url = "https://files.pythonhosted.org/packages/b2/60/b1d2da22f4970e7a155f0adde9b1435712ece01b3cd45ba63702aea33938/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:073f8bf8becba60aa931eb3bc420b217bb7d5b8f4750e6f8b3be7f3da85d38b7", size = 231972 }, - { url = "https://files.pythonhosted.org/packages/3f/ab/945b2f32de889993b9c9133216c068b7fcf257d8595a0ac420ac8677cab0/frozenlist-1.8.0-cp314-cp314-win32.whl", hash = "sha256:bac9c42ba2ac65ddc115d930c78d24ab8d4f465fd3fc473cdedfccadb9429806", size = 40536 }, - { url = "https://files.pythonhosted.org/packages/59/ad/9caa9b9c836d9ad6f067157a531ac48b7d36499f5036d4141ce78c230b1b/frozenlist-1.8.0-cp314-cp314-win_amd64.whl", hash = "sha256:3e0761f4d1a44f1d1a47996511752cf3dcec5bbdd9cc2b4fe595caf97754b7a0", size = 44330 }, - { url = "https://files.pythonhosted.org/packages/82/13/e6950121764f2676f43534c555249f57030150260aee9dcf7d64efda11dd/frozenlist-1.8.0-cp314-cp314-win_arm64.whl", hash = "sha256:d1eaff1d00c7751b7c6662e9c5ba6eb2c17a2306ba5e2a37f24ddf3cc953402b", size = 40627 }, - { url = "https://files.pythonhosted.org/packages/c0/c7/43200656ecc4e02d3f8bc248df68256cd9572b3f0017f0a0c4e93440ae23/frozenlist-1.8.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:d3bb933317c52d7ea5004a1c442eef86f426886fba134ef8cf4226ea6ee1821d", size = 89238 }, - { url = "https://files.pythonhosted.org/packages/d1/29/55c5f0689b9c0fb765055629f472c0de484dcaf0acee2f7707266ae3583c/frozenlist-1.8.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:8009897cdef112072f93a0efdce29cd819e717fd2f649ee3016efd3cd885a7ed", size = 50738 }, - { url = "https://files.pythonhosted.org/packages/ba/7d/b7282a445956506fa11da8c2db7d276adcbf2b17d8bb8407a47685263f90/frozenlist-1.8.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2c5dcbbc55383e5883246d11fd179782a9d07a986c40f49abe89ddf865913930", size = 51739 }, - { url = "https://files.pythonhosted.org/packages/62/1c/3d8622e60d0b767a5510d1d3cf21065b9db874696a51ea6d7a43180a259c/frozenlist-1.8.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:39ecbc32f1390387d2aa4f5a995e465e9e2f79ba3adcac92d68e3e0afae6657c", size = 284186 }, - { url = "https://files.pythonhosted.org/packages/2d/14/aa36d5f85a89679a85a1d44cd7a6657e0b1c75f61e7cad987b203d2daca8/frozenlist-1.8.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:92db2bf818d5cc8d9c1f1fc56b897662e24ea5adb36ad1f1d82875bd64e03c24", size = 292196 }, - { url = "https://files.pythonhosted.org/packages/05/23/6bde59eb55abd407d34f77d39a5126fb7b4f109a3f611d3929f14b700c66/frozenlist-1.8.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2dc43a022e555de94c3b68a4ef0b11c4f747d12c024a520c7101709a2144fb37", size = 273830 }, - { url = "https://files.pythonhosted.org/packages/d2/3f/22cff331bfad7a8afa616289000ba793347fcd7bc275f3b28ecea2a27909/frozenlist-1.8.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cb89a7f2de3602cfed448095bab3f178399646ab7c61454315089787df07733a", size = 294289 }, - { url = "https://files.pythonhosted.org/packages/a4/89/5b057c799de4838b6c69aa82b79705f2027615e01be996d2486a69ca99c4/frozenlist-1.8.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:33139dc858c580ea50e7e60a1b0ea003efa1fd42e6ec7fdbad78fff65fad2fd2", size = 300318 }, - { url = "https://files.pythonhosted.org/packages/30/de/2c22ab3eb2a8af6d69dc799e48455813bab3690c760de58e1bf43b36da3e/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:168c0969a329b416119507ba30b9ea13688fafffac1b7822802537569a1cb0ef", size = 282814 }, - { url = "https://files.pythonhosted.org/packages/59/f7/970141a6a8dbd7f556d94977858cfb36fa9b66e0892c6dd780d2219d8cd8/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:28bd570e8e189d7f7b001966435f9dac6718324b5be2990ac496cf1ea9ddb7fe", size = 291762 }, - { url = "https://files.pythonhosted.org/packages/c1/15/ca1adae83a719f82df9116d66f5bb28bb95557b3951903d39135620ef157/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:b2a095d45c5d46e5e79ba1e5b9cb787f541a8dee0433836cea4b96a2c439dcd8", size = 289470 }, - { url = "https://files.pythonhosted.org/packages/ac/83/dca6dc53bf657d371fbc88ddeb21b79891e747189c5de990b9dfff2ccba1/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:eab8145831a0d56ec9c4139b6c3e594c7a83c2c8be25d5bcf2d86136a532287a", size = 289042 }, - { url = "https://files.pythonhosted.org/packages/96/52/abddd34ca99be142f354398700536c5bd315880ed0a213812bc491cff5e4/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:974b28cf63cc99dfb2188d8d222bc6843656188164848c4f679e63dae4b0708e", size = 283148 }, - { url = "https://files.pythonhosted.org/packages/af/d3/76bd4ed4317e7119c2b7f57c3f6934aba26d277acc6309f873341640e21f/frozenlist-1.8.0-cp314-cp314t-win32.whl", hash = "sha256:342c97bf697ac5480c0a7ec73cd700ecfa5a8a40ac923bd035484616efecc2df", size = 44676 }, - { url = "https://files.pythonhosted.org/packages/89/76/c615883b7b521ead2944bb3480398cbb07e12b7b4e4d073d3752eb721558/frozenlist-1.8.0-cp314-cp314t-win_amd64.whl", hash = "sha256:06be8f67f39c8b1dc671f5d83aaefd3358ae5cdcf8314552c57e7ed3e6475bdd", size = 49451 }, - { url = "https://files.pythonhosted.org/packages/e0/a3/5982da14e113d07b325230f95060e2169f5311b1017ea8af2a29b374c289/frozenlist-1.8.0-cp314-cp314t-win_arm64.whl", hash = "sha256:102e6314ca4da683dca92e3b1355490fed5f313b768500084fbe6371fddfdb79", size = 42507 }, - { url = "https://files.pythonhosted.org/packages/9a/9a/e35b4a917281c0b8419d4207f4334c8e8c5dbf4f3f5f9ada73958d937dcc/frozenlist-1.8.0-py3-none-any.whl", hash = "sha256:0c18a16eab41e82c295618a77502e17b195883241c563b00f0aa5106fc4eaa0d", size = 13409 }, +sdist = { url = "https://files.pythonhosted.org/packages/2d/f5/c831fac6cc817d26fd54c7eaccd04ef7e0288806943f7cc5bbf69f3ac1f0/frozenlist-1.8.0.tar.gz", hash = "sha256:3ede829ed8d842f6cd48fc7081d7a41001a56f1f38603f9d49bf3020d59a31ad", size = 45875, upload-time = "2025-10-06T05:38:17.865Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2d/40/0832c31a37d60f60ed79e9dfb5a92e1e2af4f40a16a29abcc7992af9edff/frozenlist-1.8.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8d92f1a84bb12d9e56f818b3a746f3efba93c1b63c8387a73dde655e1e42282a", size = 85717, upload-time = "2025-10-06T05:36:27.341Z" }, + { url = "https://files.pythonhosted.org/packages/30/ba/b0b3de23f40bc55a7057bd38434e25c34fa48e17f20ee273bbde5e0650f3/frozenlist-1.8.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:96153e77a591c8adc2ee805756c61f59fef4cf4073a9275ee86fe8cba41241f7", size = 49651, upload-time = "2025-10-06T05:36:28.855Z" }, + { url = "https://files.pythonhosted.org/packages/0c/ab/6e5080ee374f875296c4243c381bbdef97a9ac39c6e3ce1d5f7d42cb78d6/frozenlist-1.8.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f21f00a91358803399890ab167098c131ec2ddd5f8f5fd5fe9c9f2c6fcd91e40", size = 49417, upload-time = "2025-10-06T05:36:29.877Z" }, + { url = "https://files.pythonhosted.org/packages/d5/4e/e4691508f9477ce67da2015d8c00acd751e6287739123113a9fca6f1604e/frozenlist-1.8.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fb30f9626572a76dfe4293c7194a09fb1fe93ba94c7d4f720dfae3b646b45027", size = 234391, upload-time = "2025-10-06T05:36:31.301Z" }, + { url = "https://files.pythonhosted.org/packages/40/76/c202df58e3acdf12969a7895fd6f3bc016c642e6726aa63bd3025e0fc71c/frozenlist-1.8.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eaa352d7047a31d87dafcacbabe89df0aa506abb5b1b85a2fb91bc3faa02d822", size = 233048, upload-time = "2025-10-06T05:36:32.531Z" }, + { url = "https://files.pythonhosted.org/packages/f9/c0/8746afb90f17b73ca5979c7a3958116e105ff796e718575175319b5bb4ce/frozenlist-1.8.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:03ae967b4e297f58f8c774c7eabcce57fe3c2434817d4385c50661845a058121", size = 226549, upload-time = "2025-10-06T05:36:33.706Z" }, + { url = "https://files.pythonhosted.org/packages/7e/eb/4c7eefc718ff72f9b6c4893291abaae5fbc0c82226a32dcd8ef4f7a5dbef/frozenlist-1.8.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f6292f1de555ffcc675941d65fffffb0a5bcd992905015f85d0592201793e0e5", size = 239833, upload-time = "2025-10-06T05:36:34.947Z" }, + { url = "https://files.pythonhosted.org/packages/c2/4e/e5c02187cf704224f8b21bee886f3d713ca379535f16893233b9d672ea71/frozenlist-1.8.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:29548f9b5b5e3460ce7378144c3010363d8035cea44bc0bf02d57f5a685e084e", size = 245363, upload-time = "2025-10-06T05:36:36.534Z" }, + { url = "https://files.pythonhosted.org/packages/1f/96/cb85ec608464472e82ad37a17f844889c36100eed57bea094518bf270692/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ec3cc8c5d4084591b4237c0a272cc4f50a5b03396a47d9caaf76f5d7b38a4f11", size = 229314, upload-time = "2025-10-06T05:36:38.582Z" }, + { url = "https://files.pythonhosted.org/packages/5d/6f/4ae69c550e4cee66b57887daeebe006fe985917c01d0fff9caab9883f6d0/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:517279f58009d0b1f2e7c1b130b377a349405da3f7621ed6bfae50b10adf20c1", size = 243365, upload-time = "2025-10-06T05:36:40.152Z" }, + { url = "https://files.pythonhosted.org/packages/7a/58/afd56de246cf11780a40a2c28dc7cbabbf06337cc8ddb1c780a2d97e88d8/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:db1e72ede2d0d7ccb213f218df6a078a9c09a7de257c2fe8fcef16d5925230b1", size = 237763, upload-time = "2025-10-06T05:36:41.355Z" }, + { url = "https://files.pythonhosted.org/packages/cb/36/cdfaf6ed42e2644740d4a10452d8e97fa1c062e2a8006e4b09f1b5fd7d63/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:b4dec9482a65c54a5044486847b8a66bf10c9cb4926d42927ec4e8fd5db7fed8", size = 240110, upload-time = "2025-10-06T05:36:42.716Z" }, + { url = "https://files.pythonhosted.org/packages/03/a8/9ea226fbefad669f11b52e864c55f0bd57d3c8d7eb07e9f2e9a0b39502e1/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:21900c48ae04d13d416f0e1e0c4d81f7931f73a9dfa0b7a8746fb2fe7dd970ed", size = 233717, upload-time = "2025-10-06T05:36:44.251Z" }, + { url = "https://files.pythonhosted.org/packages/1e/0b/1b5531611e83ba7d13ccc9988967ea1b51186af64c42b7a7af465dcc9568/frozenlist-1.8.0-cp313-cp313-win32.whl", hash = "sha256:8b7b94a067d1c504ee0b16def57ad5738701e4ba10cec90529f13fa03c833496", size = 39628, upload-time = "2025-10-06T05:36:45.423Z" }, + { url = "https://files.pythonhosted.org/packages/d8/cf/174c91dbc9cc49bc7b7aab74d8b734e974d1faa8f191c74af9b7e80848e6/frozenlist-1.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:878be833caa6a3821caf85eb39c5ba92d28e85df26d57afb06b35b2efd937231", size = 43882, upload-time = "2025-10-06T05:36:46.796Z" }, + { url = "https://files.pythonhosted.org/packages/c1/17/502cd212cbfa96eb1388614fe39a3fc9ab87dbbe042b66f97acb57474834/frozenlist-1.8.0-cp313-cp313-win_arm64.whl", hash = "sha256:44389d135b3ff43ba8cc89ff7f51f5a0bb6b63d829c8300f79a2fe4fe61bcc62", size = 39676, upload-time = "2025-10-06T05:36:47.8Z" }, + { url = "https://files.pythonhosted.org/packages/d2/5c/3bbfaa920dfab09e76946a5d2833a7cbdf7b9b4a91c714666ac4855b88b4/frozenlist-1.8.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:e25ac20a2ef37e91c1b39938b591457666a0fa835c7783c3a8f33ea42870db94", size = 89235, upload-time = "2025-10-06T05:36:48.78Z" }, + { url = "https://files.pythonhosted.org/packages/d2/d6/f03961ef72166cec1687e84e8925838442b615bd0b8854b54923ce5b7b8a/frozenlist-1.8.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:07cdca25a91a4386d2e76ad992916a85038a9b97561bf7a3fd12d5d9ce31870c", size = 50742, upload-time = "2025-10-06T05:36:49.837Z" }, + { url = "https://files.pythonhosted.org/packages/1e/bb/a6d12b7ba4c3337667d0e421f7181c82dda448ce4e7ad7ecd249a16fa806/frozenlist-1.8.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4e0c11f2cc6717e0a741f84a527c52616140741cd812a50422f83dc31749fb52", size = 51725, upload-time = "2025-10-06T05:36:50.851Z" }, + { url = "https://files.pythonhosted.org/packages/bc/71/d1fed0ffe2c2ccd70b43714c6cab0f4188f09f8a67a7914a6b46ee30f274/frozenlist-1.8.0-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b3210649ee28062ea6099cfda39e147fa1bc039583c8ee4481cb7811e2448c51", size = 284533, upload-time = "2025-10-06T05:36:51.898Z" }, + { url = "https://files.pythonhosted.org/packages/c9/1f/fb1685a7b009d89f9bf78a42d94461bc06581f6e718c39344754a5d9bada/frozenlist-1.8.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:581ef5194c48035a7de2aefc72ac6539823bb71508189e5de01d60c9dcd5fa65", size = 292506, upload-time = "2025-10-06T05:36:53.101Z" }, + { url = "https://files.pythonhosted.org/packages/e6/3b/b991fe1612703f7e0d05c0cf734c1b77aaf7c7d321df4572e8d36e7048c8/frozenlist-1.8.0-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3ef2d026f16a2b1866e1d86fc4e1291e1ed8a387b2c333809419a2f8b3a77b82", size = 274161, upload-time = "2025-10-06T05:36:54.309Z" }, + { url = "https://files.pythonhosted.org/packages/ca/ec/c5c618767bcdf66e88945ec0157d7f6c4a1322f1473392319b7a2501ded7/frozenlist-1.8.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5500ef82073f599ac84d888e3a8c1f77ac831183244bfd7f11eaa0289fb30714", size = 294676, upload-time = "2025-10-06T05:36:55.566Z" }, + { url = "https://files.pythonhosted.org/packages/7c/ce/3934758637d8f8a88d11f0585d6495ef54b2044ed6ec84492a91fa3b27aa/frozenlist-1.8.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:50066c3997d0091c411a66e710f4e11752251e6d2d73d70d8d5d4c76442a199d", size = 300638, upload-time = "2025-10-06T05:36:56.758Z" }, + { url = "https://files.pythonhosted.org/packages/fc/4f/a7e4d0d467298f42de4b41cbc7ddaf19d3cfeabaf9ff97c20c6c7ee409f9/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:5c1c8e78426e59b3f8005e9b19f6ff46e5845895adbde20ece9218319eca6506", size = 283067, upload-time = "2025-10-06T05:36:57.965Z" }, + { url = "https://files.pythonhosted.org/packages/dc/48/c7b163063d55a83772b268e6d1affb960771b0e203b632cfe09522d67ea5/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:eefdba20de0d938cec6a89bd4d70f346a03108a19b9df4248d3cf0d88f1b0f51", size = 292101, upload-time = "2025-10-06T05:36:59.237Z" }, + { url = "https://files.pythonhosted.org/packages/9f/d0/2366d3c4ecdc2fd391e0afa6e11500bfba0ea772764d631bbf82f0136c9d/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:cf253e0e1c3ceb4aaff6df637ce033ff6535fb8c70a764a8f46aafd3d6ab798e", size = 289901, upload-time = "2025-10-06T05:37:00.811Z" }, + { url = "https://files.pythonhosted.org/packages/b8/94/daff920e82c1b70e3618a2ac39fbc01ae3e2ff6124e80739ce5d71c9b920/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:032efa2674356903cd0261c4317a561a6850f3ac864a63fc1583147fb05a79b0", size = 289395, upload-time = "2025-10-06T05:37:02.115Z" }, + { url = "https://files.pythonhosted.org/packages/e3/20/bba307ab4235a09fdcd3cc5508dbabd17c4634a1af4b96e0f69bfe551ebd/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6da155091429aeba16851ecb10a9104a108bcd32f6c1642867eadaee401c1c41", size = 283659, upload-time = "2025-10-06T05:37:03.711Z" }, + { url = "https://files.pythonhosted.org/packages/fd/00/04ca1c3a7a124b6de4f8a9a17cc2fcad138b4608e7a3fc5877804b8715d7/frozenlist-1.8.0-cp313-cp313t-win32.whl", hash = "sha256:0f96534f8bfebc1a394209427d0f8a63d343c9779cda6fc25e8e121b5fd8555b", size = 43492, upload-time = "2025-10-06T05:37:04.915Z" }, + { url = "https://files.pythonhosted.org/packages/59/5e/c69f733a86a94ab10f68e496dc6b7e8bc078ebb415281d5698313e3af3a1/frozenlist-1.8.0-cp313-cp313t-win_amd64.whl", hash = "sha256:5d63a068f978fc69421fb0e6eb91a9603187527c86b7cd3f534a5b77a592b888", size = 48034, upload-time = "2025-10-06T05:37:06.343Z" }, + { url = "https://files.pythonhosted.org/packages/16/6c/be9d79775d8abe79b05fa6d23da99ad6e7763a1d080fbae7290b286093fd/frozenlist-1.8.0-cp313-cp313t-win_arm64.whl", hash = "sha256:bf0a7e10b077bf5fb9380ad3ae8ce20ef919a6ad93b4552896419ac7e1d8e042", size = 41749, upload-time = "2025-10-06T05:37:07.431Z" }, + { url = "https://files.pythonhosted.org/packages/f1/c8/85da824b7e7b9b6e7f7705b2ecaf9591ba6f79c1177f324c2735e41d36a2/frozenlist-1.8.0-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:cee686f1f4cadeb2136007ddedd0aaf928ab95216e7691c63e50a8ec066336d0", size = 86127, upload-time = "2025-10-06T05:37:08.438Z" }, + { url = "https://files.pythonhosted.org/packages/8e/e8/a1185e236ec66c20afd72399522f142c3724c785789255202d27ae992818/frozenlist-1.8.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:119fb2a1bd47307e899c2fac7f28e85b9a543864df47aa7ec9d3c1b4545f096f", size = 49698, upload-time = "2025-10-06T05:37:09.48Z" }, + { url = "https://files.pythonhosted.org/packages/a1/93/72b1736d68f03fda5fdf0f2180fb6caaae3894f1b854d006ac61ecc727ee/frozenlist-1.8.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4970ece02dbc8c3a92fcc5228e36a3e933a01a999f7094ff7c23fbd2beeaa67c", size = 49749, upload-time = "2025-10-06T05:37:10.569Z" }, + { url = "https://files.pythonhosted.org/packages/a7/b2/fabede9fafd976b991e9f1b9c8c873ed86f202889b864756f240ce6dd855/frozenlist-1.8.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:cba69cb73723c3f329622e34bdbf5ce1f80c21c290ff04256cff1cd3c2036ed2", size = 231298, upload-time = "2025-10-06T05:37:11.993Z" }, + { url = "https://files.pythonhosted.org/packages/3a/3b/d9b1e0b0eed36e70477ffb8360c49c85c8ca8ef9700a4e6711f39a6e8b45/frozenlist-1.8.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:778a11b15673f6f1df23d9586f83c4846c471a8af693a22e066508b77d201ec8", size = 232015, upload-time = "2025-10-06T05:37:13.194Z" }, + { url = "https://files.pythonhosted.org/packages/dc/94/be719d2766c1138148564a3960fc2c06eb688da592bdc25adcf856101be7/frozenlist-1.8.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0325024fe97f94c41c08872db482cf8ac4800d80e79222c6b0b7b162d5b13686", size = 225038, upload-time = "2025-10-06T05:37:14.577Z" }, + { url = "https://files.pythonhosted.org/packages/e4/09/6712b6c5465f083f52f50cf74167b92d4ea2f50e46a9eea0523d658454ae/frozenlist-1.8.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:97260ff46b207a82a7567b581ab4190bd4dfa09f4db8a8b49d1a958f6aa4940e", size = 240130, upload-time = "2025-10-06T05:37:15.781Z" }, + { url = "https://files.pythonhosted.org/packages/f8/d4/cd065cdcf21550b54f3ce6a22e143ac9e4836ca42a0de1022da8498eac89/frozenlist-1.8.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:54b2077180eb7f83dd52c40b2750d0a9f175e06a42e3213ce047219de902717a", size = 242845, upload-time = "2025-10-06T05:37:17.037Z" }, + { url = "https://files.pythonhosted.org/packages/62/c3/f57a5c8c70cd1ead3d5d5f776f89d33110b1addae0ab010ad774d9a44fb9/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:2f05983daecab868a31e1da44462873306d3cbfd76d1f0b5b69c473d21dbb128", size = 229131, upload-time = "2025-10-06T05:37:18.221Z" }, + { url = "https://files.pythonhosted.org/packages/6c/52/232476fe9cb64f0742f3fde2b7d26c1dac18b6d62071c74d4ded55e0ef94/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:33f48f51a446114bc5d251fb2954ab0164d5be02ad3382abcbfe07e2531d650f", size = 240542, upload-time = "2025-10-06T05:37:19.771Z" }, + { url = "https://files.pythonhosted.org/packages/5f/85/07bf3f5d0fb5414aee5f47d33c6f5c77bfe49aac680bfece33d4fdf6a246/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:154e55ec0655291b5dd1b8731c637ecdb50975a2ae70c606d100750a540082f7", size = 237308, upload-time = "2025-10-06T05:37:20.969Z" }, + { url = "https://files.pythonhosted.org/packages/11/99/ae3a33d5befd41ac0ca2cc7fd3aa707c9c324de2e89db0e0f45db9a64c26/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:4314debad13beb564b708b4a496020e5306c7333fa9a3ab90374169a20ffab30", size = 238210, upload-time = "2025-10-06T05:37:22.252Z" }, + { url = "https://files.pythonhosted.org/packages/b2/60/b1d2da22f4970e7a155f0adde9b1435712ece01b3cd45ba63702aea33938/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:073f8bf8becba60aa931eb3bc420b217bb7d5b8f4750e6f8b3be7f3da85d38b7", size = 231972, upload-time = "2025-10-06T05:37:23.5Z" }, + { url = "https://files.pythonhosted.org/packages/3f/ab/945b2f32de889993b9c9133216c068b7fcf257d8595a0ac420ac8677cab0/frozenlist-1.8.0-cp314-cp314-win32.whl", hash = "sha256:bac9c42ba2ac65ddc115d930c78d24ab8d4f465fd3fc473cdedfccadb9429806", size = 40536, upload-time = "2025-10-06T05:37:25.581Z" }, + { url = "https://files.pythonhosted.org/packages/59/ad/9caa9b9c836d9ad6f067157a531ac48b7d36499f5036d4141ce78c230b1b/frozenlist-1.8.0-cp314-cp314-win_amd64.whl", hash = "sha256:3e0761f4d1a44f1d1a47996511752cf3dcec5bbdd9cc2b4fe595caf97754b7a0", size = 44330, upload-time = "2025-10-06T05:37:26.928Z" }, + { url = "https://files.pythonhosted.org/packages/82/13/e6950121764f2676f43534c555249f57030150260aee9dcf7d64efda11dd/frozenlist-1.8.0-cp314-cp314-win_arm64.whl", hash = "sha256:d1eaff1d00c7751b7c6662e9c5ba6eb2c17a2306ba5e2a37f24ddf3cc953402b", size = 40627, upload-time = "2025-10-06T05:37:28.075Z" }, + { url = "https://files.pythonhosted.org/packages/c0/c7/43200656ecc4e02d3f8bc248df68256cd9572b3f0017f0a0c4e93440ae23/frozenlist-1.8.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:d3bb933317c52d7ea5004a1c442eef86f426886fba134ef8cf4226ea6ee1821d", size = 89238, upload-time = "2025-10-06T05:37:29.373Z" }, + { url = "https://files.pythonhosted.org/packages/d1/29/55c5f0689b9c0fb765055629f472c0de484dcaf0acee2f7707266ae3583c/frozenlist-1.8.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:8009897cdef112072f93a0efdce29cd819e717fd2f649ee3016efd3cd885a7ed", size = 50738, upload-time = "2025-10-06T05:37:30.792Z" }, + { url = "https://files.pythonhosted.org/packages/ba/7d/b7282a445956506fa11da8c2db7d276adcbf2b17d8bb8407a47685263f90/frozenlist-1.8.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2c5dcbbc55383e5883246d11fd179782a9d07a986c40f49abe89ddf865913930", size = 51739, upload-time = "2025-10-06T05:37:32.127Z" }, + { url = "https://files.pythonhosted.org/packages/62/1c/3d8622e60d0b767a5510d1d3cf21065b9db874696a51ea6d7a43180a259c/frozenlist-1.8.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:39ecbc32f1390387d2aa4f5a995e465e9e2f79ba3adcac92d68e3e0afae6657c", size = 284186, upload-time = "2025-10-06T05:37:33.21Z" }, + { url = "https://files.pythonhosted.org/packages/2d/14/aa36d5f85a89679a85a1d44cd7a6657e0b1c75f61e7cad987b203d2daca8/frozenlist-1.8.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:92db2bf818d5cc8d9c1f1fc56b897662e24ea5adb36ad1f1d82875bd64e03c24", size = 292196, upload-time = "2025-10-06T05:37:36.107Z" }, + { url = "https://files.pythonhosted.org/packages/05/23/6bde59eb55abd407d34f77d39a5126fb7b4f109a3f611d3929f14b700c66/frozenlist-1.8.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2dc43a022e555de94c3b68a4ef0b11c4f747d12c024a520c7101709a2144fb37", size = 273830, upload-time = "2025-10-06T05:37:37.663Z" }, + { url = "https://files.pythonhosted.org/packages/d2/3f/22cff331bfad7a8afa616289000ba793347fcd7bc275f3b28ecea2a27909/frozenlist-1.8.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cb89a7f2de3602cfed448095bab3f178399646ab7c61454315089787df07733a", size = 294289, upload-time = "2025-10-06T05:37:39.261Z" }, + { url = "https://files.pythonhosted.org/packages/a4/89/5b057c799de4838b6c69aa82b79705f2027615e01be996d2486a69ca99c4/frozenlist-1.8.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:33139dc858c580ea50e7e60a1b0ea003efa1fd42e6ec7fdbad78fff65fad2fd2", size = 300318, upload-time = "2025-10-06T05:37:43.213Z" }, + { url = "https://files.pythonhosted.org/packages/30/de/2c22ab3eb2a8af6d69dc799e48455813bab3690c760de58e1bf43b36da3e/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:168c0969a329b416119507ba30b9ea13688fafffac1b7822802537569a1cb0ef", size = 282814, upload-time = "2025-10-06T05:37:45.337Z" }, + { url = "https://files.pythonhosted.org/packages/59/f7/970141a6a8dbd7f556d94977858cfb36fa9b66e0892c6dd780d2219d8cd8/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:28bd570e8e189d7f7b001966435f9dac6718324b5be2990ac496cf1ea9ddb7fe", size = 291762, upload-time = "2025-10-06T05:37:46.657Z" }, + { url = "https://files.pythonhosted.org/packages/c1/15/ca1adae83a719f82df9116d66f5bb28bb95557b3951903d39135620ef157/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:b2a095d45c5d46e5e79ba1e5b9cb787f541a8dee0433836cea4b96a2c439dcd8", size = 289470, upload-time = "2025-10-06T05:37:47.946Z" }, + { url = "https://files.pythonhosted.org/packages/ac/83/dca6dc53bf657d371fbc88ddeb21b79891e747189c5de990b9dfff2ccba1/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:eab8145831a0d56ec9c4139b6c3e594c7a83c2c8be25d5bcf2d86136a532287a", size = 289042, upload-time = "2025-10-06T05:37:49.499Z" }, + { url = "https://files.pythonhosted.org/packages/96/52/abddd34ca99be142f354398700536c5bd315880ed0a213812bc491cff5e4/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:974b28cf63cc99dfb2188d8d222bc6843656188164848c4f679e63dae4b0708e", size = 283148, upload-time = "2025-10-06T05:37:50.745Z" }, + { url = "https://files.pythonhosted.org/packages/af/d3/76bd4ed4317e7119c2b7f57c3f6934aba26d277acc6309f873341640e21f/frozenlist-1.8.0-cp314-cp314t-win32.whl", hash = "sha256:342c97bf697ac5480c0a7ec73cd700ecfa5a8a40ac923bd035484616efecc2df", size = 44676, upload-time = "2025-10-06T05:37:52.222Z" }, + { url = "https://files.pythonhosted.org/packages/89/76/c615883b7b521ead2944bb3480398cbb07e12b7b4e4d073d3752eb721558/frozenlist-1.8.0-cp314-cp314t-win_amd64.whl", hash = "sha256:06be8f67f39c8b1dc671f5d83aaefd3358ae5cdcf8314552c57e7ed3e6475bdd", size = 49451, upload-time = "2025-10-06T05:37:53.425Z" }, + { url = "https://files.pythonhosted.org/packages/e0/a3/5982da14e113d07b325230f95060e2169f5311b1017ea8af2a29b374c289/frozenlist-1.8.0-cp314-cp314t-win_arm64.whl", hash = "sha256:102e6314ca4da683dca92e3b1355490fed5f313b768500084fbe6371fddfdb79", size = 42507, upload-time = "2025-10-06T05:37:54.513Z" }, + { url = "https://files.pythonhosted.org/packages/9a/9a/e35b4a917281c0b8419d4207f4334c8e8c5dbf4f3f5f9ada73958d937dcc/frozenlist-1.8.0-py3-none-any.whl", hash = "sha256:0c18a16eab41e82c295618a77502e17b195883241c563b00f0aa5106fc4eaa0d", size = 13409, upload-time = "2025-10-06T05:38:16.721Z" }, ] [[package]] name = "fsspec" version = "2025.12.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b6/27/954057b0d1f53f086f681755207dda6de6c660ce133c829158e8e8fe7895/fsspec-2025.12.0.tar.gz", hash = "sha256:c505de011584597b1060ff778bb664c1bc022e87921b0e4f10cc9c44f9635973", size = 309748 } +sdist = { url = "https://files.pythonhosted.org/packages/b6/27/954057b0d1f53f086f681755207dda6de6c660ce133c829158e8e8fe7895/fsspec-2025.12.0.tar.gz", hash = "sha256:c505de011584597b1060ff778bb664c1bc022e87921b0e4f10cc9c44f9635973", size = 309748, upload-time = "2025-12-03T15:23:42.687Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/51/c7/b64cae5dba3a1b138d7123ec36bb5ccd39d39939f18454407e5468f4763f/fsspec-2025.12.0-py3-none-any.whl", hash = "sha256:8bf1fe301b7d8acfa6e8571e3b1c3d158f909666642431cc78a1b7b4dbc5ec5b", size = 201422 }, + { url = "https://files.pythonhosted.org/packages/51/c7/b64cae5dba3a1b138d7123ec36bb5ccd39d39939f18454407e5468f4763f/fsspec-2025.12.0-py3-none-any.whl", hash = "sha256:8bf1fe301b7d8acfa6e8571e3b1c3d158f909666642431cc78a1b7b4dbc5ec5b", size = 201422, upload-time = "2025-12-03T15:23:41.434Z" }, ] [[package]] @@ -806,9 +790,9 @@ dependencies = [ { name = "httpx" }, { name = "pydantic" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/0d/aa/81f76b90f8d1a7dcd9297bd8bf664927ae2a1efe40fe5d1a8856dc721359/genai_prices-0.0.49.tar.gz", hash = "sha256:a7f98f1537e6f89ed54f1cd8f560806e187033dcb42554fbecd4d635567120c5", size = 57852 } +sdist = { url = "https://files.pythonhosted.org/packages/0d/aa/81f76b90f8d1a7dcd9297bd8bf664927ae2a1efe40fe5d1a8856dc721359/genai_prices-0.0.49.tar.gz", hash = "sha256:a7f98f1537e6f89ed54f1cd8f560806e187033dcb42554fbecd4d635567120c5", size = 57852, upload-time = "2025-12-17T10:47:29.345Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e4/1e/1d51238dd164dde10c4e3be6ad2d8f26dd34dd262117c277440e2b5dc7c0/genai_prices-0.0.49-py3-none-any.whl", hash = "sha256:dd3efbebcd865d89cd849793530729e7f7e1ca59d2b17a091ad1aa6aa76daf0d", size = 60433 }, + { url = "https://files.pythonhosted.org/packages/e4/1e/1d51238dd164dde10c4e3be6ad2d8f26dd34dd262117c277440e2b5dc7c0/genai_prices-0.0.49-py3-none-any.whl", hash = "sha256:dd3efbebcd865d89cd849793530729e7f7e1ca59d2b17a091ad1aa6aa76daf0d", size = 60433, upload-time = "2025-12-17T10:47:28.3Z" }, ] [[package]] @@ -822,9 +806,9 @@ dependencies = [ { name = "pydantic" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/55/3b/d8d4bd504b9d353f40d83f9c52d6e7b5efbc2b2c54979584e045cee87555/githubkit-0.14.1.tar.gz", hash = "sha256:7f5f9463c901c44871ffe0c1ddb1f2959f8cdbe015e0ca5ba0bfcaf1858651af", size = 2643130 } +sdist = { url = "https://files.pythonhosted.org/packages/55/3b/d8d4bd504b9d353f40d83f9c52d6e7b5efbc2b2c54979584e045cee87555/githubkit-0.14.1.tar.gz", hash = "sha256:7f5f9463c901c44871ffe0c1ddb1f2959f8cdbe015e0ca5ba0bfcaf1858651af", size = 2643130, upload-time = "2025-12-21T08:55:11.34Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/8f/d1/c2e6e05ea979d1ef1c0ab99cf21e3ae5728d7c2db16d4a1fea9f8f345d77/githubkit-0.14.1-py3-none-any.whl", hash = "sha256:e0055a287d86543ba64db65a44a828b93e21ff221bc12a77913349e4816f5195", size = 6446837 }, + { url = "https://files.pythonhosted.org/packages/8f/d1/c2e6e05ea979d1ef1c0ab99cf21e3ae5728d7c2db16d4a1fea9f8f345d77/githubkit-0.14.1-py3-none-any.whl", hash = "sha256:e0055a287d86543ba64db65a44a828b93e21ff221bc12a77913349e4816f5195", size = 6446837, upload-time = "2025-12-21T08:55:09.446Z" }, ] [[package]] @@ -836,9 +820,9 @@ dependencies = [ { name = "pyasn1-modules" }, { name = "rsa" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/e5/00/3c794502a8b892c404b2dea5b3650eb21bfc7069612fbfd15c7f17c1cb0d/google_auth-2.45.0.tar.gz", hash = "sha256:90d3f41b6b72ea72dd9811e765699ee491ab24139f34ebf1ca2b9cc0c38708f3", size = 320708 } +sdist = { url = "https://files.pythonhosted.org/packages/e5/00/3c794502a8b892c404b2dea5b3650eb21bfc7069612fbfd15c7f17c1cb0d/google_auth-2.45.0.tar.gz", hash = "sha256:90d3f41b6b72ea72dd9811e765699ee491ab24139f34ebf1ca2b9cc0c38708f3", size = 320708, upload-time = "2025-12-15T22:58:42.889Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c6/97/451d55e05487a5cd6279a01a7e34921858b16f7dc8aa38a2c684743cd2b3/google_auth-2.45.0-py2.py3-none-any.whl", hash = "sha256:82344e86dc00410ef5382d99be677c6043d72e502b625aa4f4afa0bdacca0f36", size = 233312 }, + { url = "https://files.pythonhosted.org/packages/c6/97/451d55e05487a5cd6279a01a7e34921858b16f7dc8aa38a2c684743cd2b3/google_auth-2.45.0-py2.py3-none-any.whl", hash = "sha256:82344e86dc00410ef5382d99be677c6043d72e502b625aa4f4afa0bdacca0f36", size = 233312, upload-time = "2025-12-15T22:58:40.777Z" }, ] [package.optional-dependencies] @@ -862,9 +846,9 @@ dependencies = [ { name = "typing-extensions" }, { name = "websockets" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/70/ad/d3ac5a102135bd3f1e4b1475ca65d2bd4bcc22eb2e9348ac40fe3fadb1d6/google_genai-1.56.0.tar.gz", hash = "sha256:0491af33c375f099777ae207d9621f044e27091fafad4c50e617eba32165e82f", size = 340451 } +sdist = { url = "https://files.pythonhosted.org/packages/70/ad/d3ac5a102135bd3f1e4b1475ca65d2bd4bcc22eb2e9348ac40fe3fadb1d6/google_genai-1.56.0.tar.gz", hash = "sha256:0491af33c375f099777ae207d9621f044e27091fafad4c50e617eba32165e82f", size = 340451, upload-time = "2025-12-17T12:35:05.412Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/84/93/94bc7a89ef4e7ed3666add55cd859d1483a22737251df659bf1aa46e9405/google_genai-1.56.0-py3-none-any.whl", hash = "sha256:9e6b11e0c105ead229368cb5849a480e4d0185519f8d9f538d61ecfcf193b052", size = 426563 }, + { url = "https://files.pythonhosted.org/packages/84/93/94bc7a89ef4e7ed3666add55cd859d1483a22737251df659bf1aa46e9405/google_genai-1.56.0-py3-none-any.whl", hash = "sha256:9e6b11e0c105ead229368cb5849a480e4d0185519f8d9f538d61ecfcf193b052", size = 426563, upload-time = "2025-12-17T12:35:03.717Z" }, ] [[package]] @@ -874,40 +858,37 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "protobuf" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/e5/7b/adfd75544c415c487b33061fe7ae526165241c1ea133f9a9125a56b39fd8/googleapis_common_protos-1.72.0.tar.gz", hash = "sha256:e55a601c1b32b52d7a3e65f43563e2aa61bcd737998ee672ac9b951cd49319f5", size = 147433 } +sdist = { url = "https://files.pythonhosted.org/packages/e5/7b/adfd75544c415c487b33061fe7ae526165241c1ea133f9a9125a56b39fd8/googleapis_common_protos-1.72.0.tar.gz", hash = "sha256:e55a601c1b32b52d7a3e65f43563e2aa61bcd737998ee672ac9b951cd49319f5", size = 147433, upload-time = "2025-11-06T18:29:24.087Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c4/ab/09169d5a4612a5f92490806649ac8d41e3ec9129c636754575b3553f4ea4/googleapis_common_protos-1.72.0-py3-none-any.whl", hash = "sha256:4299c5a82d5ae1a9702ada957347726b167f9f8d1fc352477702a1e851ff4038", size = 297515 }, + { url = "https://files.pythonhosted.org/packages/c4/ab/09169d5a4612a5f92490806649ac8d41e3ec9129c636754575b3553f4ea4/googleapis_common_protos-1.72.0-py3-none-any.whl", hash = "sha256:4299c5a82d5ae1a9702ada957347726b167f9f8d1fc352477702a1e851ff4038", size = 297515, upload-time = "2025-11-06T18:29:13.14Z" }, ] [[package]] name = "greenlet" version = "3.3.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/c7/e5/40dbda2736893e3e53d25838e0f19a2b417dfc122b9989c91918db30b5d3/greenlet-3.3.0.tar.gz", hash = "sha256:a82bb225a4e9e4d653dd2fb7b8b2d36e4fb25bc0165422a11e48b88e9e6f78fb", size = 190651 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/02/2f/28592176381b9ab2cafa12829ba7b472d177f3acc35d8fbcf3673d966fff/greenlet-3.3.0-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:a1e41a81c7e2825822f4e068c48cb2196002362619e2d70b148f20a831c00739", size = 275140 }, - { url = "https://files.pythonhosted.org/packages/2c/80/fbe937bf81e9fca98c981fe499e59a3f45df2a04da0baa5c2be0dca0d329/greenlet-3.3.0-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9f515a47d02da4d30caaa85b69474cec77b7929b2e936ff7fb853d42f4bf8808", size = 599219 }, - { url = "https://files.pythonhosted.org/packages/c2/ff/7c985128f0514271b8268476af89aee6866df5eec04ac17dcfbc676213df/greenlet-3.3.0-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7d2d9fd66bfadf230b385fdc90426fcd6eb64db54b40c495b72ac0feb5766c54", size = 610211 }, - { url = "https://files.pythonhosted.org/packages/79/07/c47a82d881319ec18a4510bb30463ed6891f2ad2c1901ed5ec23d3de351f/greenlet-3.3.0-cp313-cp313-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:30a6e28487a790417d036088b3bcb3f3ac7d8babaa7d0139edbaddebf3af9492", size = 624311 }, - { url = "https://files.pythonhosted.org/packages/fd/8e/424b8c6e78bd9837d14ff7df01a9829fc883ba2ab4ea787d4f848435f23f/greenlet-3.3.0-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:087ea5e004437321508a8d6f20efc4cfec5e3c30118e1417ea96ed1d93950527", size = 612833 }, - { url = "https://files.pythonhosted.org/packages/b5/ba/56699ff9b7c76ca12f1cdc27a886d0f81f2189c3455ff9f65246780f713d/greenlet-3.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ab97cf74045343f6c60a39913fa59710e4bd26a536ce7ab2397adf8b27e67c39", size = 1567256 }, - { url = "https://files.pythonhosted.org/packages/1e/37/f31136132967982d698c71a281a8901daf1a8fbab935dce7c0cf15f942cc/greenlet-3.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5375d2e23184629112ca1ea89a53389dddbffcf417dad40125713d88eb5f96e8", size = 1636483 }, - { url = "https://files.pythonhosted.org/packages/7e/71/ba21c3fb8c5dce83b8c01f458a42e99ffdb1963aeec08fff5a18588d8fd7/greenlet-3.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:9ee1942ea19550094033c35d25d20726e4f1c40d59545815e1128ac58d416d38", size = 301833 }, - { url = "https://files.pythonhosted.org/packages/d7/7c/f0a6d0ede2c7bf092d00bc83ad5bafb7e6ec9b4aab2fbdfa6f134dc73327/greenlet-3.3.0-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:60c2ef0f578afb3c8d92ea07ad327f9a062547137afe91f38408f08aacab667f", size = 275671 }, - { url = "https://files.pythonhosted.org/packages/44/06/dac639ae1a50f5969d82d2e3dd9767d30d6dbdbab0e1a54010c8fe90263c/greenlet-3.3.0-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0a5d554d0712ba1de0a6c94c640f7aeba3f85b3a6e1f2899c11c2c0428da9365", size = 646360 }, - { url = "https://files.pythonhosted.org/packages/e0/94/0fb76fe6c5369fba9bf98529ada6f4c3a1adf19e406a47332245ef0eb357/greenlet-3.3.0-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3a898b1e9c5f7307ebbde4102908e6cbfcb9ea16284a3abe15cab996bee8b9b3", size = 658160 }, - { url = "https://files.pythonhosted.org/packages/93/79/d2c70cae6e823fac36c3bbc9077962105052b7ef81db2f01ec3b9bf17e2b/greenlet-3.3.0-cp314-cp314-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:dcd2bdbd444ff340e8d6bdf54d2f206ccddbb3ccfdcd3c25bf4afaa7b8f0cf45", size = 671388 }, - { url = "https://files.pythonhosted.org/packages/b8/14/bab308fc2c1b5228c3224ec2bf928ce2e4d21d8046c161e44a2012b5203e/greenlet-3.3.0-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5773edda4dc00e173820722711d043799d3adb4f01731f40619e07ea2750b955", size = 660166 }, - { url = "https://files.pythonhosted.org/packages/4b/d2/91465d39164eaa0085177f61983d80ffe746c5a1860f009811d498e7259c/greenlet-3.3.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ac0549373982b36d5fd5d30beb8a7a33ee541ff98d2b502714a09f1169f31b55", size = 1615193 }, - { url = "https://files.pythonhosted.org/packages/42/1b/83d110a37044b92423084d52d5d5a3b3a73cafb51b547e6d7366ff62eff1/greenlet-3.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d198d2d977460358c3b3a4dc844f875d1adb33817f0613f663a656f463764ccc", size = 1683653 }, - { url = "https://files.pythonhosted.org/packages/7c/9a/9030e6f9aa8fd7808e9c31ba4c38f87c4f8ec324ee67431d181fe396d705/greenlet-3.3.0-cp314-cp314-win_amd64.whl", hash = "sha256:73f51dd0e0bdb596fb0417e475fa3c5e32d4c83638296e560086b8d7da7c4170", size = 305387 }, - { url = "https://files.pythonhosted.org/packages/a0/66/bd6317bc5932accf351fc19f177ffba53712a202f9df10587da8df257c7e/greenlet-3.3.0-cp314-cp314t-macosx_11_0_universal2.whl", hash = "sha256:d6ed6f85fae6cdfdb9ce04c9bf7a08d666cfcfb914e7d006f44f840b46741931", size = 282638 }, - { url = "https://files.pythonhosted.org/packages/30/cf/cc81cb030b40e738d6e69502ccbd0dd1bced0588e958f9e757945de24404/greenlet-3.3.0-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d9125050fcf24554e69c4cacb086b87b3b55dc395a8b3ebe6487b045b2614388", size = 651145 }, - { url = "https://files.pythonhosted.org/packages/9c/ea/1020037b5ecfe95ca7df8d8549959baceb8186031da83d5ecceff8b08cd2/greenlet-3.3.0-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:87e63ccfa13c0a0f6234ed0add552af24cc67dd886731f2261e46e241608bee3", size = 654236 }, - { url = "https://files.pythonhosted.org/packages/69/cc/1e4bae2e45ca2fa55299f4e85854606a78ecc37fead20d69322f96000504/greenlet-3.3.0-cp314-cp314t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2662433acbca297c9153a4023fe2161c8dcfdcc91f10433171cf7e7d94ba2221", size = 662506 }, - { url = "https://files.pythonhosted.org/packages/57/b9/f8025d71a6085c441a7eaff0fd928bbb275a6633773667023d19179fe815/greenlet-3.3.0-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3c6e9b9c1527a78520357de498b0e709fb9e2f49c3a513afd5a249007261911b", size = 653783 }, - { url = "https://files.pythonhosted.org/packages/f6/c7/876a8c7a7485d5d6b5c6821201d542ef28be645aa024cfe1145b35c120c1/greenlet-3.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:286d093f95ec98fdd92fcb955003b8a3d054b4e2cab3e2707a5039e7b50520fd", size = 1614857 }, - { url = "https://files.pythonhosted.org/packages/4f/dc/041be1dff9f23dac5f48a43323cd0789cb798342011c19a248d9c9335536/greenlet-3.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c10513330af5b8ae16f023e8ddbfb486ab355d04467c4679c5cfe4659975dd9", size = 1676034 }, +sdist = { url = "https://files.pythonhosted.org/packages/c7/e5/40dbda2736893e3e53d25838e0f19a2b417dfc122b9989c91918db30b5d3/greenlet-3.3.0.tar.gz", hash = "sha256:a82bb225a4e9e4d653dd2fb7b8b2d36e4fb25bc0165422a11e48b88e9e6f78fb", size = 190651, upload-time = "2025-12-04T14:49:44.05Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/02/2f/28592176381b9ab2cafa12829ba7b472d177f3acc35d8fbcf3673d966fff/greenlet-3.3.0-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:a1e41a81c7e2825822f4e068c48cb2196002362619e2d70b148f20a831c00739", size = 275140, upload-time = "2025-12-04T14:23:01.282Z" }, + { url = "https://files.pythonhosted.org/packages/2c/80/fbe937bf81e9fca98c981fe499e59a3f45df2a04da0baa5c2be0dca0d329/greenlet-3.3.0-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9f515a47d02da4d30caaa85b69474cec77b7929b2e936ff7fb853d42f4bf8808", size = 599219, upload-time = "2025-12-04T14:50:08.309Z" }, + { url = "https://files.pythonhosted.org/packages/c2/ff/7c985128f0514271b8268476af89aee6866df5eec04ac17dcfbc676213df/greenlet-3.3.0-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7d2d9fd66bfadf230b385fdc90426fcd6eb64db54b40c495b72ac0feb5766c54", size = 610211, upload-time = "2025-12-04T14:57:43.968Z" }, + { url = "https://files.pythonhosted.org/packages/fd/8e/424b8c6e78bd9837d14ff7df01a9829fc883ba2ab4ea787d4f848435f23f/greenlet-3.3.0-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:087ea5e004437321508a8d6f20efc4cfec5e3c30118e1417ea96ed1d93950527", size = 612833, upload-time = "2025-12-04T14:26:03.669Z" }, + { url = "https://files.pythonhosted.org/packages/b5/ba/56699ff9b7c76ca12f1cdc27a886d0f81f2189c3455ff9f65246780f713d/greenlet-3.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ab97cf74045343f6c60a39913fa59710e4bd26a536ce7ab2397adf8b27e67c39", size = 1567256, upload-time = "2025-12-04T15:04:25.276Z" }, + { url = "https://files.pythonhosted.org/packages/1e/37/f31136132967982d698c71a281a8901daf1a8fbab935dce7c0cf15f942cc/greenlet-3.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5375d2e23184629112ca1ea89a53389dddbffcf417dad40125713d88eb5f96e8", size = 1636483, upload-time = "2025-12-04T14:27:30.804Z" }, + { url = "https://files.pythonhosted.org/packages/7e/71/ba21c3fb8c5dce83b8c01f458a42e99ffdb1963aeec08fff5a18588d8fd7/greenlet-3.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:9ee1942ea19550094033c35d25d20726e4f1c40d59545815e1128ac58d416d38", size = 301833, upload-time = "2025-12-04T14:32:23.929Z" }, + { url = "https://files.pythonhosted.org/packages/d7/7c/f0a6d0ede2c7bf092d00bc83ad5bafb7e6ec9b4aab2fbdfa6f134dc73327/greenlet-3.3.0-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:60c2ef0f578afb3c8d92ea07ad327f9a062547137afe91f38408f08aacab667f", size = 275671, upload-time = "2025-12-04T14:23:05.267Z" }, + { url = "https://files.pythonhosted.org/packages/44/06/dac639ae1a50f5969d82d2e3dd9767d30d6dbdbab0e1a54010c8fe90263c/greenlet-3.3.0-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0a5d554d0712ba1de0a6c94c640f7aeba3f85b3a6e1f2899c11c2c0428da9365", size = 646360, upload-time = "2025-12-04T14:50:10.026Z" }, + { url = "https://files.pythonhosted.org/packages/e0/94/0fb76fe6c5369fba9bf98529ada6f4c3a1adf19e406a47332245ef0eb357/greenlet-3.3.0-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3a898b1e9c5f7307ebbde4102908e6cbfcb9ea16284a3abe15cab996bee8b9b3", size = 658160, upload-time = "2025-12-04T14:57:45.41Z" }, + { url = "https://files.pythonhosted.org/packages/b8/14/bab308fc2c1b5228c3224ec2bf928ce2e4d21d8046c161e44a2012b5203e/greenlet-3.3.0-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5773edda4dc00e173820722711d043799d3adb4f01731f40619e07ea2750b955", size = 660166, upload-time = "2025-12-04T14:26:05.099Z" }, + { url = "https://files.pythonhosted.org/packages/4b/d2/91465d39164eaa0085177f61983d80ffe746c5a1860f009811d498e7259c/greenlet-3.3.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ac0549373982b36d5fd5d30beb8a7a33ee541ff98d2b502714a09f1169f31b55", size = 1615193, upload-time = "2025-12-04T15:04:27.041Z" }, + { url = "https://files.pythonhosted.org/packages/42/1b/83d110a37044b92423084d52d5d5a3b3a73cafb51b547e6d7366ff62eff1/greenlet-3.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d198d2d977460358c3b3a4dc844f875d1adb33817f0613f663a656f463764ccc", size = 1683653, upload-time = "2025-12-04T14:27:32.366Z" }, + { url = "https://files.pythonhosted.org/packages/7c/9a/9030e6f9aa8fd7808e9c31ba4c38f87c4f8ec324ee67431d181fe396d705/greenlet-3.3.0-cp314-cp314-win_amd64.whl", hash = "sha256:73f51dd0e0bdb596fb0417e475fa3c5e32d4c83638296e560086b8d7da7c4170", size = 305387, upload-time = "2025-12-04T14:26:51.063Z" }, + { url = "https://files.pythonhosted.org/packages/a0/66/bd6317bc5932accf351fc19f177ffba53712a202f9df10587da8df257c7e/greenlet-3.3.0-cp314-cp314t-macosx_11_0_universal2.whl", hash = "sha256:d6ed6f85fae6cdfdb9ce04c9bf7a08d666cfcfb914e7d006f44f840b46741931", size = 282638, upload-time = "2025-12-04T14:25:20.941Z" }, + { url = "https://files.pythonhosted.org/packages/30/cf/cc81cb030b40e738d6e69502ccbd0dd1bced0588e958f9e757945de24404/greenlet-3.3.0-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d9125050fcf24554e69c4cacb086b87b3b55dc395a8b3ebe6487b045b2614388", size = 651145, upload-time = "2025-12-04T14:50:11.039Z" }, + { url = "https://files.pythonhosted.org/packages/9c/ea/1020037b5ecfe95ca7df8d8549959baceb8186031da83d5ecceff8b08cd2/greenlet-3.3.0-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:87e63ccfa13c0a0f6234ed0add552af24cc67dd886731f2261e46e241608bee3", size = 654236, upload-time = "2025-12-04T14:57:47.007Z" }, + { url = "https://files.pythonhosted.org/packages/57/b9/f8025d71a6085c441a7eaff0fd928bbb275a6633773667023d19179fe815/greenlet-3.3.0-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3c6e9b9c1527a78520357de498b0e709fb9e2f49c3a513afd5a249007261911b", size = 653783, upload-time = "2025-12-04T14:26:06.225Z" }, + { url = "https://files.pythonhosted.org/packages/f6/c7/876a8c7a7485d5d6b5c6821201d542ef28be645aa024cfe1145b35c120c1/greenlet-3.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:286d093f95ec98fdd92fcb955003b8a3d054b4e2cab3e2707a5039e7b50520fd", size = 1614857, upload-time = "2025-12-04T15:04:28.484Z" }, + { url = "https://files.pythonhosted.org/packages/4f/dc/041be1dff9f23dac5f48a43323cd0789cb798342011c19a248d9c9335536/greenlet-3.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c10513330af5b8ae16f023e8ddbfb486ab355d04467c4679c5cfe4659975dd9", size = 1676034, upload-time = "2025-12-04T14:27:33.531Z" }, ] [[package]] @@ -917,9 +898,9 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/0d/0c/3a471b6e31951dce2360477420d0a8d1e00dea6cf33b70f3e8c3ab6e28e1/griffe-1.15.0.tar.gz", hash = "sha256:7726e3afd6f298fbc3696e67958803e7ac843c1cfe59734b6251a40cdbfb5eea", size = 424112 } +sdist = { url = "https://files.pythonhosted.org/packages/0d/0c/3a471b6e31951dce2360477420d0a8d1e00dea6cf33b70f3e8c3ab6e28e1/griffe-1.15.0.tar.gz", hash = "sha256:7726e3afd6f298fbc3696e67958803e7ac843c1cfe59734b6251a40cdbfb5eea", size = 424112, upload-time = "2025-11-10T15:03:15.52Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/9c/83/3b1d03d36f224edded98e9affd0467630fc09d766c0e56fb1498cbb04a9b/griffe-1.15.0-py3-none-any.whl", hash = "sha256:6f6762661949411031f5fcda9593f586e6ce8340f0ba88921a0f2ef7a81eb9a3", size = 150705 }, + { url = "https://files.pythonhosted.org/packages/9c/83/3b1d03d36f224edded98e9affd0467630fc09d766c0e56fb1498cbb04a9b/griffe-1.15.0-py3-none-any.whl", hash = "sha256:6f6762661949411031f5fcda9593f586e6ce8340f0ba88921a0f2ef7a81eb9a3", size = 150705, upload-time = "2025-11-10T15:03:13.549Z" }, ] [[package]] @@ -934,47 +915,47 @@ dependencies = [ { name = "sniffio" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/3f/12/f4099a141677fcd2ed79dcc1fcec431e60c52e0e90c9c5d935f0ffaf8c0e/groq-1.0.0.tar.gz", hash = "sha256:66cb7bb729e6eb644daac7ce8efe945e99e4eb33657f733ee6f13059ef0c25a9", size = 146068 } +sdist = { url = "https://files.pythonhosted.org/packages/3f/12/f4099a141677fcd2ed79dcc1fcec431e60c52e0e90c9c5d935f0ffaf8c0e/groq-1.0.0.tar.gz", hash = "sha256:66cb7bb729e6eb644daac7ce8efe945e99e4eb33657f733ee6f13059ef0c25a9", size = 146068, upload-time = "2025-12-17T23:34:23.115Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/4a/88/3175759d2ef30406ea721f4d837bfa1ba4339fde3b81ba8c5640a96ed231/groq-1.0.0-py3-none-any.whl", hash = "sha256:6e22bf92ffad988f01d2d4df7729add66b8fd5dbfb2154b5bbf3af245b72c731", size = 138292 }, + { url = "https://files.pythonhosted.org/packages/4a/88/3175759d2ef30406ea721f4d837bfa1ba4339fde3b81ba8c5640a96ed231/groq-1.0.0-py3-none-any.whl", hash = "sha256:6e22bf92ffad988f01d2d4df7729add66b8fd5dbfb2154b5bbf3af245b72c731", size = 138292, upload-time = "2025-12-17T23:34:21.957Z" }, ] [[package]] name = "h11" version = "0.16.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250 } +sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515 }, + { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, ] [[package]] name = "hf-xet" version = "1.2.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/5e/6e/0f11bacf08a67f7fb5ee09740f2ca54163863b07b70d579356e9222ce5d8/hf_xet-1.2.0.tar.gz", hash = "sha256:a8c27070ca547293b6890c4bf389f713f80e8c478631432962bb7f4bc0bd7d7f", size = 506020 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/9e/a5/85ef910a0aa034a2abcfadc360ab5ac6f6bc4e9112349bd40ca97551cff0/hf_xet-1.2.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:ceeefcd1b7aed4956ae8499e2199607765fbd1c60510752003b6cc0b8413b649", size = 2861870 }, - { url = "https://files.pythonhosted.org/packages/ea/40/e2e0a7eb9a51fe8828ba2d47fe22a7e74914ea8a0db68a18c3aa7449c767/hf_xet-1.2.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:b70218dd548e9840224df5638fdc94bd033552963cfa97f9170829381179c813", size = 2717584 }, - { url = "https://files.pythonhosted.org/packages/a5/7d/daf7f8bc4594fdd59a8a596f9e3886133fdc68e675292218a5e4c1b7e834/hf_xet-1.2.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7d40b18769bb9a8bc82a9ede575ce1a44c75eb80e7375a01d76259089529b5dc", size = 3315004 }, - { url = "https://files.pythonhosted.org/packages/b1/ba/45ea2f605fbf6d81c8b21e4d970b168b18a53515923010c312c06cd83164/hf_xet-1.2.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:cd3a6027d59cfb60177c12d6424e31f4b5ff13d8e3a1247b3a584bf8977e6df5", size = 3222636 }, - { url = "https://files.pythonhosted.org/packages/4a/1d/04513e3cab8f29ab8c109d309ddd21a2705afab9d52f2ba1151e0c14f086/hf_xet-1.2.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6de1fc44f58f6dd937956c8d304d8c2dea264c80680bcfa61ca4a15e7b76780f", size = 3408448 }, - { url = "https://files.pythonhosted.org/packages/f0/7c/60a2756d7feec7387db3a1176c632357632fbe7849fce576c5559d4520c7/hf_xet-1.2.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f182f264ed2acd566c514e45da9f2119110e48a87a327ca271027904c70c5832", size = 3503401 }, - { url = "https://files.pythonhosted.org/packages/4e/64/48fffbd67fb418ab07451e4ce641a70de1c40c10a13e25325e24858ebe5a/hf_xet-1.2.0-cp313-cp313t-win_amd64.whl", hash = "sha256:293a7a3787e5c95d7be1857358a9130694a9c6021de3f27fa233f37267174382", size = 2900866 }, - { url = "https://files.pythonhosted.org/packages/e2/51/f7e2caae42f80af886db414d4e9885fac959330509089f97cccb339c6b87/hf_xet-1.2.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:10bfab528b968c70e062607f663e21e34e2bba349e8038db546646875495179e", size = 2861861 }, - { url = "https://files.pythonhosted.org/packages/6e/1d/a641a88b69994f9371bd347f1dd35e5d1e2e2460a2e350c8d5165fc62005/hf_xet-1.2.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2a212e842647b02eb6a911187dc878e79c4aa0aa397e88dd3b26761676e8c1f8", size = 2717699 }, - { url = "https://files.pythonhosted.org/packages/df/e0/e5e9bba7d15f0318955f7ec3f4af13f92e773fbb368c0b8008a5acbcb12f/hf_xet-1.2.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:30e06daccb3a7d4c065f34fc26c14c74f4653069bb2b194e7f18f17cbe9939c0", size = 3314885 }, - { url = "https://files.pythonhosted.org/packages/21/90/b7fe5ff6f2b7b8cbdf1bd56145f863c90a5807d9758a549bf3d916aa4dec/hf_xet-1.2.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:29c8fc913a529ec0a91867ce3d119ac1aac966e098cf49501800c870328cc090", size = 3221550 }, - { url = "https://files.pythonhosted.org/packages/6f/cb/73f276f0a7ce46cc6a6ec7d6c7d61cbfe5f2e107123d9bbd0193c355f106/hf_xet-1.2.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e159cbfcfbb29f920db2c09ed8b660eb894640d284f102ada929b6e3dc410a", size = 3408010 }, - { url = "https://files.pythonhosted.org/packages/b8/1e/d642a12caa78171f4be64f7cd9c40e3ca5279d055d0873188a58c0f5fbb9/hf_xet-1.2.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:9c91d5ae931510107f148874e9e2de8a16052b6f1b3ca3c1b12f15ccb491390f", size = 3503264 }, - { url = "https://files.pythonhosted.org/packages/17/b5/33764714923fa1ff922770f7ed18c2daae034d21ae6e10dbf4347c854154/hf_xet-1.2.0-cp314-cp314t-win_amd64.whl", hash = "sha256:210d577732b519ac6ede149d2f2f34049d44e8622bf14eb3d63bbcd2d4b332dc", size = 2901071 }, - { url = "https://files.pythonhosted.org/packages/96/2d/22338486473df5923a9ab7107d375dbef9173c338ebef5098ef593d2b560/hf_xet-1.2.0-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:46740d4ac024a7ca9b22bebf77460ff43332868b661186a8e46c227fdae01848", size = 2866099 }, - { url = "https://files.pythonhosted.org/packages/7f/8c/c5becfa53234299bc2210ba314eaaae36c2875e0045809b82e40a9544f0c/hf_xet-1.2.0-cp37-abi3-macosx_11_0_arm64.whl", hash = "sha256:27df617a076420d8845bea087f59303da8be17ed7ec0cd7ee3b9b9f579dff0e4", size = 2722178 }, - { url = "https://files.pythonhosted.org/packages/9a/92/cf3ab0b652b082e66876d08da57fcc6fa2f0e6c70dfbbafbd470bb73eb47/hf_xet-1.2.0-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3651fd5bfe0281951b988c0facbe726aa5e347b103a675f49a3fa8144c7968fd", size = 3320214 }, - { url = "https://files.pythonhosted.org/packages/46/92/3f7ec4a1b6a65bf45b059b6d4a5d38988f63e193056de2f420137e3c3244/hf_xet-1.2.0-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:d06fa97c8562fb3ee7a378dd9b51e343bc5bc8190254202c9771029152f5e08c", size = 3229054 }, - { url = "https://files.pythonhosted.org/packages/0b/dd/7ac658d54b9fb7999a0ccb07ad863b413cbaf5cf172f48ebcd9497ec7263/hf_xet-1.2.0-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:4c1428c9ae73ec0939410ec73023c4f842927f39db09b063b9482dac5a3bb737", size = 3413812 }, - { url = "https://files.pythonhosted.org/packages/92/68/89ac4e5b12a9ff6286a12174c8538a5930e2ed662091dd2572bbe0a18c8a/hf_xet-1.2.0-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a55558084c16b09b5ed32ab9ed38421e2d87cf3f1f89815764d1177081b99865", size = 3508920 }, - { url = "https://files.pythonhosted.org/packages/cb/44/870d44b30e1dcfb6a65932e3e1506c103a8a5aea9103c337e7a53180322c/hf_xet-1.2.0-cp37-abi3-win_amd64.whl", hash = "sha256:e6584a52253f72c9f52f9e549d5895ca7a471608495c4ecaa6cc73dba2b24d69", size = 2905735 }, +sdist = { url = "https://files.pythonhosted.org/packages/5e/6e/0f11bacf08a67f7fb5ee09740f2ca54163863b07b70d579356e9222ce5d8/hf_xet-1.2.0.tar.gz", hash = "sha256:a8c27070ca547293b6890c4bf389f713f80e8c478631432962bb7f4bc0bd7d7f", size = 506020, upload-time = "2025-10-24T19:04:32.129Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9e/a5/85ef910a0aa034a2abcfadc360ab5ac6f6bc4e9112349bd40ca97551cff0/hf_xet-1.2.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:ceeefcd1b7aed4956ae8499e2199607765fbd1c60510752003b6cc0b8413b649", size = 2861870, upload-time = "2025-10-24T19:04:11.422Z" }, + { url = "https://files.pythonhosted.org/packages/ea/40/e2e0a7eb9a51fe8828ba2d47fe22a7e74914ea8a0db68a18c3aa7449c767/hf_xet-1.2.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:b70218dd548e9840224df5638fdc94bd033552963cfa97f9170829381179c813", size = 2717584, upload-time = "2025-10-24T19:04:09.586Z" }, + { url = "https://files.pythonhosted.org/packages/a5/7d/daf7f8bc4594fdd59a8a596f9e3886133fdc68e675292218a5e4c1b7e834/hf_xet-1.2.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7d40b18769bb9a8bc82a9ede575ce1a44c75eb80e7375a01d76259089529b5dc", size = 3315004, upload-time = "2025-10-24T19:04:00.314Z" }, + { url = "https://files.pythonhosted.org/packages/b1/ba/45ea2f605fbf6d81c8b21e4d970b168b18a53515923010c312c06cd83164/hf_xet-1.2.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:cd3a6027d59cfb60177c12d6424e31f4b5ff13d8e3a1247b3a584bf8977e6df5", size = 3222636, upload-time = "2025-10-24T19:03:58.111Z" }, + { url = "https://files.pythonhosted.org/packages/4a/1d/04513e3cab8f29ab8c109d309ddd21a2705afab9d52f2ba1151e0c14f086/hf_xet-1.2.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6de1fc44f58f6dd937956c8d304d8c2dea264c80680bcfa61ca4a15e7b76780f", size = 3408448, upload-time = "2025-10-24T19:04:20.951Z" }, + { url = "https://files.pythonhosted.org/packages/f0/7c/60a2756d7feec7387db3a1176c632357632fbe7849fce576c5559d4520c7/hf_xet-1.2.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f182f264ed2acd566c514e45da9f2119110e48a87a327ca271027904c70c5832", size = 3503401, upload-time = "2025-10-24T19:04:22.549Z" }, + { url = "https://files.pythonhosted.org/packages/4e/64/48fffbd67fb418ab07451e4ce641a70de1c40c10a13e25325e24858ebe5a/hf_xet-1.2.0-cp313-cp313t-win_amd64.whl", hash = "sha256:293a7a3787e5c95d7be1857358a9130694a9c6021de3f27fa233f37267174382", size = 2900866, upload-time = "2025-10-24T19:04:33.461Z" }, + { url = "https://files.pythonhosted.org/packages/e2/51/f7e2caae42f80af886db414d4e9885fac959330509089f97cccb339c6b87/hf_xet-1.2.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:10bfab528b968c70e062607f663e21e34e2bba349e8038db546646875495179e", size = 2861861, upload-time = "2025-10-24T19:04:19.01Z" }, + { url = "https://files.pythonhosted.org/packages/6e/1d/a641a88b69994f9371bd347f1dd35e5d1e2e2460a2e350c8d5165fc62005/hf_xet-1.2.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2a212e842647b02eb6a911187dc878e79c4aa0aa397e88dd3b26761676e8c1f8", size = 2717699, upload-time = "2025-10-24T19:04:17.306Z" }, + { url = "https://files.pythonhosted.org/packages/df/e0/e5e9bba7d15f0318955f7ec3f4af13f92e773fbb368c0b8008a5acbcb12f/hf_xet-1.2.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:30e06daccb3a7d4c065f34fc26c14c74f4653069bb2b194e7f18f17cbe9939c0", size = 3314885, upload-time = "2025-10-24T19:04:07.642Z" }, + { url = "https://files.pythonhosted.org/packages/21/90/b7fe5ff6f2b7b8cbdf1bd56145f863c90a5807d9758a549bf3d916aa4dec/hf_xet-1.2.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:29c8fc913a529ec0a91867ce3d119ac1aac966e098cf49501800c870328cc090", size = 3221550, upload-time = "2025-10-24T19:04:05.55Z" }, + { url = "https://files.pythonhosted.org/packages/6f/cb/73f276f0a7ce46cc6a6ec7d6c7d61cbfe5f2e107123d9bbd0193c355f106/hf_xet-1.2.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e159cbfcfbb29f920db2c09ed8b660eb894640d284f102ada929b6e3dc410a", size = 3408010, upload-time = "2025-10-24T19:04:28.598Z" }, + { url = "https://files.pythonhosted.org/packages/b8/1e/d642a12caa78171f4be64f7cd9c40e3ca5279d055d0873188a58c0f5fbb9/hf_xet-1.2.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:9c91d5ae931510107f148874e9e2de8a16052b6f1b3ca3c1b12f15ccb491390f", size = 3503264, upload-time = "2025-10-24T19:04:30.397Z" }, + { url = "https://files.pythonhosted.org/packages/17/b5/33764714923fa1ff922770f7ed18c2daae034d21ae6e10dbf4347c854154/hf_xet-1.2.0-cp314-cp314t-win_amd64.whl", hash = "sha256:210d577732b519ac6ede149d2f2f34049d44e8622bf14eb3d63bbcd2d4b332dc", size = 2901071, upload-time = "2025-10-24T19:04:37.463Z" }, + { url = "https://files.pythonhosted.org/packages/96/2d/22338486473df5923a9ab7107d375dbef9173c338ebef5098ef593d2b560/hf_xet-1.2.0-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:46740d4ac024a7ca9b22bebf77460ff43332868b661186a8e46c227fdae01848", size = 2866099, upload-time = "2025-10-24T19:04:15.366Z" }, + { url = "https://files.pythonhosted.org/packages/7f/8c/c5becfa53234299bc2210ba314eaaae36c2875e0045809b82e40a9544f0c/hf_xet-1.2.0-cp37-abi3-macosx_11_0_arm64.whl", hash = "sha256:27df617a076420d8845bea087f59303da8be17ed7ec0cd7ee3b9b9f579dff0e4", size = 2722178, upload-time = "2025-10-24T19:04:13.695Z" }, + { url = "https://files.pythonhosted.org/packages/9a/92/cf3ab0b652b082e66876d08da57fcc6fa2f0e6c70dfbbafbd470bb73eb47/hf_xet-1.2.0-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3651fd5bfe0281951b988c0facbe726aa5e347b103a675f49a3fa8144c7968fd", size = 3320214, upload-time = "2025-10-24T19:04:03.596Z" }, + { url = "https://files.pythonhosted.org/packages/46/92/3f7ec4a1b6a65bf45b059b6d4a5d38988f63e193056de2f420137e3c3244/hf_xet-1.2.0-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:d06fa97c8562fb3ee7a378dd9b51e343bc5bc8190254202c9771029152f5e08c", size = 3229054, upload-time = "2025-10-24T19:04:01.949Z" }, + { url = "https://files.pythonhosted.org/packages/0b/dd/7ac658d54b9fb7999a0ccb07ad863b413cbaf5cf172f48ebcd9497ec7263/hf_xet-1.2.0-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:4c1428c9ae73ec0939410ec73023c4f842927f39db09b063b9482dac5a3bb737", size = 3413812, upload-time = "2025-10-24T19:04:24.585Z" }, + { url = "https://files.pythonhosted.org/packages/92/68/89ac4e5b12a9ff6286a12174c8538a5930e2ed662091dd2572bbe0a18c8a/hf_xet-1.2.0-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a55558084c16b09b5ed32ab9ed38421e2d87cf3f1f89815764d1177081b99865", size = 3508920, upload-time = "2025-10-24T19:04:26.927Z" }, + { url = "https://files.pythonhosted.org/packages/cb/44/870d44b30e1dcfb6a65932e3e1506c103a8a5aea9103c337e7a53180322c/hf_xet-1.2.0-cp37-abi3-win_amd64.whl", hash = "sha256:e6584a52253f72c9f52f9e549d5895ca7a471608495c4ecaa6cc73dba2b24d69", size = 2905735, upload-time = "2025-10-24T19:04:35.928Z" }, ] [[package]] @@ -988,9 +969,9 @@ dependencies = [ { name = "msgpack" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/e5/64/a104ccac48f123f853254483617b16e0efc1649bd7e35bcdc5a5a5ef0ae2/hishel-0.1.5.tar.gz", hash = "sha256:9d40c682cd94fd6e1394fb05713ae20a75ed8aeba6f5272380444039ce6257f2", size = 75468 } +sdist = { url = "https://files.pythonhosted.org/packages/e5/64/a104ccac48f123f853254483617b16e0efc1649bd7e35bcdc5a5a5ef0ae2/hishel-0.1.5.tar.gz", hash = "sha256:9d40c682cd94fd6e1394fb05713ae20a75ed8aeba6f5272380444039ce6257f2", size = 75468, upload-time = "2025-10-18T13:32:41.854Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/70/83/4f8b77839e62114bb034375ee8e08cfb6af1164754b925b271d3f1ec06ee/hishel-0.1.5-py3-none-any.whl", hash = "sha256:0bfbe9a2b9342090eba82ba6de88258092e1c4c7b730cd4cb4b570e4b40e44a7", size = 92486 }, + { url = "https://files.pythonhosted.org/packages/70/83/4f8b77839e62114bb034375ee8e08cfb6af1164754b925b271d3f1ec06ee/hishel-0.1.5-py3-none-any.whl", hash = "sha256:0bfbe9a2b9342090eba82ba6de88258092e1c4c7b730cd4cb4b570e4b40e44a7", size = 92486, upload-time = "2025-10-18T13:32:40.333Z" }, ] [[package]] @@ -1001,31 +982,31 @@ dependencies = [ { name = "certifi" }, { name = "h11" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484 } +sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784 }, + { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" }, ] [[package]] name = "httptools" version = "0.7.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b5/46/120a669232c7bdedb9d52d4aeae7e6c7dfe151e99dc70802e2fc7a5e1993/httptools-0.7.1.tar.gz", hash = "sha256:abd72556974f8e7c74a259655924a717a2365b236c882c3f6f8a45fe94703ac9", size = 258961 } +sdist = { url = "https://files.pythonhosted.org/packages/b5/46/120a669232c7bdedb9d52d4aeae7e6c7dfe151e99dc70802e2fc7a5e1993/httptools-0.7.1.tar.gz", hash = "sha256:abd72556974f8e7c74a259655924a717a2365b236c882c3f6f8a45fe94703ac9", size = 258961, upload-time = "2025-10-10T03:55:08.559Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/09/8f/c77b1fcbfd262d422f12da02feb0d218fa228d52485b77b953832105bb90/httptools-0.7.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:6babce6cfa2a99545c60bfef8bee0cc0545413cb0018f617c8059a30ad985de3", size = 202889 }, - { url = "https://files.pythonhosted.org/packages/0a/1a/22887f53602feaa066354867bc49a68fc295c2293433177ee90870a7d517/httptools-0.7.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:601b7628de7504077dd3dcb3791c6b8694bbd967148a6d1f01806509254fb1ca", size = 108180 }, - { url = "https://files.pythonhosted.org/packages/32/6a/6aaa91937f0010d288d3d124ca2946d48d60c3a5ee7ca62afe870e3ea011/httptools-0.7.1-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:04c6c0e6c5fb0739c5b8a9eb046d298650a0ff38cf42537fc372b28dc7e4472c", size = 478596 }, - { url = "https://files.pythonhosted.org/packages/6d/70/023d7ce117993107be88d2cbca566a7c1323ccbaf0af7eabf2064fe356f6/httptools-0.7.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:69d4f9705c405ae3ee83d6a12283dc9feba8cc6aaec671b412917e644ab4fa66", size = 473268 }, - { url = "https://files.pythonhosted.org/packages/32/4d/9dd616c38da088e3f436e9a616e1d0cc66544b8cdac405cc4e81c8679fc7/httptools-0.7.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:44c8f4347d4b31269c8a9205d8a5ee2df5322b09bbbd30f8f862185bb6b05346", size = 455517 }, - { url = "https://files.pythonhosted.org/packages/1d/3a/a6c595c310b7df958e739aae88724e24f9246a514d909547778d776799be/httptools-0.7.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:465275d76db4d554918aba40bf1cbebe324670f3dfc979eaffaa5d108e2ed650", size = 458337 }, - { url = "https://files.pythonhosted.org/packages/fd/82/88e8d6d2c51edc1cc391b6e044c6c435b6aebe97b1abc33db1b0b24cd582/httptools-0.7.1-cp313-cp313-win_amd64.whl", hash = "sha256:322d00c2068d125bd570f7bf78b2d367dad02b919d8581d7476d8b75b294e3e6", size = 85743 }, - { url = "https://files.pythonhosted.org/packages/34/50/9d095fcbb6de2d523e027a2f304d4551855c2f46e0b82befd718b8b20056/httptools-0.7.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:c08fe65728b8d70b6923ce31e3956f859d5e1e8548e6f22ec520a962c6757270", size = 203619 }, - { url = "https://files.pythonhosted.org/packages/07/f0/89720dc5139ae54b03f861b5e2c55a37dba9a5da7d51e1e824a1f343627f/httptools-0.7.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:7aea2e3c3953521c3c51106ee11487a910d45586e351202474d45472db7d72d3", size = 108714 }, - { url = "https://files.pythonhosted.org/packages/b3/cb/eea88506f191fb552c11787c23f9a405f4c7b0c5799bf73f2249cd4f5228/httptools-0.7.1-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0e68b8582f4ea9166be62926077a3334064d422cf08ab87d8b74664f8e9058e1", size = 472909 }, - { url = "https://files.pythonhosted.org/packages/e0/4a/a548bdfae6369c0d078bab5769f7b66f17f1bfaa6fa28f81d6be6959066b/httptools-0.7.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:df091cf961a3be783d6aebae963cc9b71e00d57fa6f149025075217bc6a55a7b", size = 470831 }, - { url = "https://files.pythonhosted.org/packages/4d/31/14df99e1c43bd132eec921c2e7e11cda7852f65619bc0fc5bdc2d0cb126c/httptools-0.7.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f084813239e1eb403ddacd06a30de3d3e09a9b76e7894dcda2b22f8a726e9c60", size = 452631 }, - { url = "https://files.pythonhosted.org/packages/22/d2/b7e131f7be8d854d48cb6d048113c30f9a46dca0c9a8b08fcb3fcd588cdc/httptools-0.7.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7347714368fb2b335e9063bc2b96f2f87a9ceffcd9758ac295f8bbcd3ffbc0ca", size = 452910 }, - { url = "https://files.pythonhosted.org/packages/53/cf/878f3b91e4e6e011eff6d1fa9ca39f7eb17d19c9d7971b04873734112f30/httptools-0.7.1-cp314-cp314-win_amd64.whl", hash = "sha256:cfabda2a5bb85aa2a904ce06d974a3f30fb36cc63d7feaddec05d2050acede96", size = 88205 }, + { url = "https://files.pythonhosted.org/packages/09/8f/c77b1fcbfd262d422f12da02feb0d218fa228d52485b77b953832105bb90/httptools-0.7.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:6babce6cfa2a99545c60bfef8bee0cc0545413cb0018f617c8059a30ad985de3", size = 202889, upload-time = "2025-10-10T03:54:47.089Z" }, + { url = "https://files.pythonhosted.org/packages/0a/1a/22887f53602feaa066354867bc49a68fc295c2293433177ee90870a7d517/httptools-0.7.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:601b7628de7504077dd3dcb3791c6b8694bbd967148a6d1f01806509254fb1ca", size = 108180, upload-time = "2025-10-10T03:54:48.052Z" }, + { url = "https://files.pythonhosted.org/packages/32/6a/6aaa91937f0010d288d3d124ca2946d48d60c3a5ee7ca62afe870e3ea011/httptools-0.7.1-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:04c6c0e6c5fb0739c5b8a9eb046d298650a0ff38cf42537fc372b28dc7e4472c", size = 478596, upload-time = "2025-10-10T03:54:48.919Z" }, + { url = "https://files.pythonhosted.org/packages/6d/70/023d7ce117993107be88d2cbca566a7c1323ccbaf0af7eabf2064fe356f6/httptools-0.7.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:69d4f9705c405ae3ee83d6a12283dc9feba8cc6aaec671b412917e644ab4fa66", size = 473268, upload-time = "2025-10-10T03:54:49.993Z" }, + { url = "https://files.pythonhosted.org/packages/32/4d/9dd616c38da088e3f436e9a616e1d0cc66544b8cdac405cc4e81c8679fc7/httptools-0.7.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:44c8f4347d4b31269c8a9205d8a5ee2df5322b09bbbd30f8f862185bb6b05346", size = 455517, upload-time = "2025-10-10T03:54:51.066Z" }, + { url = "https://files.pythonhosted.org/packages/1d/3a/a6c595c310b7df958e739aae88724e24f9246a514d909547778d776799be/httptools-0.7.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:465275d76db4d554918aba40bf1cbebe324670f3dfc979eaffaa5d108e2ed650", size = 458337, upload-time = "2025-10-10T03:54:52.196Z" }, + { url = "https://files.pythonhosted.org/packages/fd/82/88e8d6d2c51edc1cc391b6e044c6c435b6aebe97b1abc33db1b0b24cd582/httptools-0.7.1-cp313-cp313-win_amd64.whl", hash = "sha256:322d00c2068d125bd570f7bf78b2d367dad02b919d8581d7476d8b75b294e3e6", size = 85743, upload-time = "2025-10-10T03:54:53.448Z" }, + { url = "https://files.pythonhosted.org/packages/34/50/9d095fcbb6de2d523e027a2f304d4551855c2f46e0b82befd718b8b20056/httptools-0.7.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:c08fe65728b8d70b6923ce31e3956f859d5e1e8548e6f22ec520a962c6757270", size = 203619, upload-time = "2025-10-10T03:54:54.321Z" }, + { url = "https://files.pythonhosted.org/packages/07/f0/89720dc5139ae54b03f861b5e2c55a37dba9a5da7d51e1e824a1f343627f/httptools-0.7.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:7aea2e3c3953521c3c51106ee11487a910d45586e351202474d45472db7d72d3", size = 108714, upload-time = "2025-10-10T03:54:55.163Z" }, + { url = "https://files.pythonhosted.org/packages/b3/cb/eea88506f191fb552c11787c23f9a405f4c7b0c5799bf73f2249cd4f5228/httptools-0.7.1-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0e68b8582f4ea9166be62926077a3334064d422cf08ab87d8b74664f8e9058e1", size = 472909, upload-time = "2025-10-10T03:54:56.056Z" }, + { url = "https://files.pythonhosted.org/packages/e0/4a/a548bdfae6369c0d078bab5769f7b66f17f1bfaa6fa28f81d6be6959066b/httptools-0.7.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:df091cf961a3be783d6aebae963cc9b71e00d57fa6f149025075217bc6a55a7b", size = 470831, upload-time = "2025-10-10T03:54:57.219Z" }, + { url = "https://files.pythonhosted.org/packages/4d/31/14df99e1c43bd132eec921c2e7e11cda7852f65619bc0fc5bdc2d0cb126c/httptools-0.7.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f084813239e1eb403ddacd06a30de3d3e09a9b76e7894dcda2b22f8a726e9c60", size = 452631, upload-time = "2025-10-10T03:54:58.219Z" }, + { url = "https://files.pythonhosted.org/packages/22/d2/b7e131f7be8d854d48cb6d048113c30f9a46dca0c9a8b08fcb3fcd588cdc/httptools-0.7.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7347714368fb2b335e9063bc2b96f2f87a9ceffcd9758ac295f8bbcd3ffbc0ca", size = 452910, upload-time = "2025-10-10T03:54:59.366Z" }, + { url = "https://files.pythonhosted.org/packages/53/cf/878f3b91e4e6e011eff6d1fa9ca39f7eb17d19c9d7971b04873734112f30/httptools-0.7.1-cp314-cp314-win_amd64.whl", hash = "sha256:cfabda2a5bb85aa2a904ce06d974a3f30fb36cc63d7feaddec05d2050acede96", size = 88205, upload-time = "2025-10-10T03:55:00.389Z" }, ] [[package]] @@ -1038,18 +1019,18 @@ dependencies = [ { name = "httpcore" }, { name = "idna" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406 } +sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517 }, + { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, ] [[package]] name = "httpx-sse" version = "0.4.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/0f/4c/751061ffa58615a32c31b2d82e8482be8dd4a89154f003147acee90f2be9/httpx_sse-0.4.3.tar.gz", hash = "sha256:9b1ed0127459a66014aec3c56bebd93da3c1bc8bb6618c8082039a44889a755d", size = 15943 } +sdist = { url = "https://files.pythonhosted.org/packages/0f/4c/751061ffa58615a32c31b2d82e8482be8dd4a89154f003147acee90f2be9/httpx_sse-0.4.3.tar.gz", hash = "sha256:9b1ed0127459a66014aec3c56bebd93da3c1bc8bb6618c8082039a44889a755d", size = 15943, upload-time = "2025-10-10T21:48:22.271Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d2/fd/6668e5aec43ab844de6fc74927e155a3b37bf40d7c3790e49fc0406b6578/httpx_sse-0.4.3-py3-none-any.whl", hash = "sha256:0ac1c9fe3c0afad2e0ebb25a934a59f4c7823b60792691f779fad2c5568830fc", size = 8960 }, + { url = "https://files.pythonhosted.org/packages/d2/fd/6668e5aec43ab844de6fc74927e155a3b37bf40d7c3790e49fc0406b6578/httpx_sse-0.4.3-py3-none-any.whl", hash = "sha256:0ac1c9fe3c0afad2e0ebb25a934a59f4c7823b60792691f779fad2c5568830fc", size = 8960, upload-time = "2025-10-10T21:48:21.158Z" }, ] [[package]] @@ -1066,9 +1047,9 @@ dependencies = [ { name = "tqdm" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/98/63/4910c5fa9128fdadf6a9c5ac138e8b1b6cee4ca44bf7915bbfbce4e355ee/huggingface_hub-0.36.0.tar.gz", hash = "sha256:47b3f0e2539c39bf5cde015d63b72ec49baff67b6931c3d97f3f84532e2b8d25", size = 463358 } +sdist = { url = "https://files.pythonhosted.org/packages/98/63/4910c5fa9128fdadf6a9c5ac138e8b1b6cee4ca44bf7915bbfbce4e355ee/huggingface_hub-0.36.0.tar.gz", hash = "sha256:47b3f0e2539c39bf5cde015d63b72ec49baff67b6931c3d97f3f84532e2b8d25", size = 463358, upload-time = "2025-10-23T12:12:01.413Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/cb/bd/1a875e0d592d447cbc02805fd3fe0f497714d6a2583f59d14fa9ebad96eb/huggingface_hub-0.36.0-py3-none-any.whl", hash = "sha256:7bcc9ad17d5b3f07b57c78e79d527102d08313caa278a641993acddcb894548d", size = 566094 }, + { url = "https://files.pythonhosted.org/packages/cb/bd/1a875e0d592d447cbc02805fd3fe0f497714d6a2583f59d14fa9ebad96eb/huggingface_hub-0.36.0-py3-none-any.whl", hash = "sha256:7bcc9ad17d5b3f07b57c78e79d527102d08313caa278a641993acddcb894548d", size = 566094, upload-time = "2025-10-23T12:11:59.557Z" }, ] [package.optional-dependencies] @@ -1080,9 +1061,9 @@ inference = [ name = "idna" version = "3.11" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582 } +sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008 }, + { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" }, ] [[package]] @@ -1092,18 +1073,18 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "zipp" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/f3/49/3b30cad09e7771a4982d9975a8cbf64f00d4a1ececb53297f1d9a7be1b10/importlib_metadata-8.7.1.tar.gz", hash = "sha256:49fef1ae6440c182052f407c8d34a68f72efc36db9ca90dc0113398f2fdde8bb", size = 57107 } +sdist = { url = "https://files.pythonhosted.org/packages/f3/49/3b30cad09e7771a4982d9975a8cbf64f00d4a1ececb53297f1d9a7be1b10/importlib_metadata-8.7.1.tar.gz", hash = "sha256:49fef1ae6440c182052f407c8d34a68f72efc36db9ca90dc0113398f2fdde8bb", size = 57107, upload-time = "2025-12-21T10:00:19.278Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/fa/5e/f8e9a1d23b9c20a551a8a02ea3637b4642e22c2626e3a13a9a29cdea99eb/importlib_metadata-8.7.1-py3-none-any.whl", hash = "sha256:5a1f80bf1daa489495071efbb095d75a634cf28a8bc299581244063b53176151", size = 27865 }, + { url = "https://files.pythonhosted.org/packages/fa/5e/f8e9a1d23b9c20a551a8a02ea3637b4642e22c2626e3a13a9a29cdea99eb/importlib_metadata-8.7.1-py3-none-any.whl", hash = "sha256:5a1f80bf1daa489495071efbb095d75a634cf28a8bc299581244063b53176151", size = 27865, upload-time = "2025-12-21T10:00:18.329Z" }, ] [[package]] name = "invoke" version = "2.2.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/de/bd/b461d3424a24c80490313fd77feeb666ca4f6a28c7e72713e3d9095719b4/invoke-2.2.1.tar.gz", hash = "sha256:515bf49b4a48932b79b024590348da22f39c4942dff991ad1fb8b8baea1be707", size = 304762 } +sdist = { url = "https://files.pythonhosted.org/packages/de/bd/b461d3424a24c80490313fd77feeb666ca4f6a28c7e72713e3d9095719b4/invoke-2.2.1.tar.gz", hash = "sha256:515bf49b4a48932b79b024590348da22f39c4942dff991ad1fb8b8baea1be707", size = 304762, upload-time = "2025-10-11T00:36:35.172Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/32/4b/b99e37f88336009971405cbb7630610322ed6fbfa31e1d7ab3fbf3049a2d/invoke-2.2.1-py3-none-any.whl", hash = "sha256:2413bc441b376e5cd3f55bb5d364f973ad8bdd7bf87e53c79de3c11bf3feecc8", size = 160287 }, + { url = "https://files.pythonhosted.org/packages/32/4b/b99e37f88336009971405cbb7630610322ed6fbfa31e1d7ab3fbf3049a2d/invoke-2.2.1-py3-none-any.whl", hash = "sha256:2413bc441b376e5cd3f55bb5d364f973ad8bdd7bf87e53c79de3c11bf3feecc8", size = 160287, upload-time = "2025-10-11T00:36:33.703Z" }, ] [[package]] @@ -1113,18 +1094,18 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "more-itertools" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/06/c0/ed4a27bc5571b99e3cff68f8a9fa5b56ff7df1c2251cc715a652ddd26402/jaraco.classes-3.4.0.tar.gz", hash = "sha256:47a024b51d0239c0dd8c8540c6c7f484be3b8fcf0b2d85c13825780d3b3f3acd", size = 11780 } +sdist = { url = "https://files.pythonhosted.org/packages/06/c0/ed4a27bc5571b99e3cff68f8a9fa5b56ff7df1c2251cc715a652ddd26402/jaraco.classes-3.4.0.tar.gz", hash = "sha256:47a024b51d0239c0dd8c8540c6c7f484be3b8fcf0b2d85c13825780d3b3f3acd", size = 11780, upload-time = "2024-03-31T07:27:36.643Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/7f/66/b15ce62552d84bbfcec9a4873ab79d993a1dd4edb922cbfccae192bd5b5f/jaraco.classes-3.4.0-py3-none-any.whl", hash = "sha256:f662826b6bed8cace05e7ff873ce0f9283b5c924470fe664fff1c2f00f581790", size = 6777 }, + { url = "https://files.pythonhosted.org/packages/7f/66/b15ce62552d84bbfcec9a4873ab79d993a1dd4edb922cbfccae192bd5b5f/jaraco.classes-3.4.0-py3-none-any.whl", hash = "sha256:f662826b6bed8cace05e7ff873ce0f9283b5c924470fe664fff1c2f00f581790", size = 6777, upload-time = "2024-03-31T07:27:34.792Z" }, ] [[package]] name = "jaraco-context" version = "6.0.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/8d/7d/41acf8e22d791bde812cb6c2c36128bb932ed8ae066bcb5e39cb198e8253/jaraco_context-6.0.2.tar.gz", hash = "sha256:953ae8dddb57b1d791bf72ea1009b32088840a7dd19b9ba16443f62be919ee57", size = 14994 } +sdist = { url = "https://files.pythonhosted.org/packages/8d/7d/41acf8e22d791bde812cb6c2c36128bb932ed8ae066bcb5e39cb198e8253/jaraco_context-6.0.2.tar.gz", hash = "sha256:953ae8dddb57b1d791bf72ea1009b32088840a7dd19b9ba16443f62be919ee57", size = 14994, upload-time = "2025-12-24T19:21:35.784Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c7/0c/1e0096ced9c55f9c6c6655446798df74165780375d3f5ab5f33751e087ae/jaraco_context-6.0.2-py3-none-any.whl", hash = "sha256:55fc21af4b4f9ca94aa643b6ee7fe13b1e4c01abf3aeb98ca4ad9c80b741c786", size = 6988 }, + { url = "https://files.pythonhosted.org/packages/c7/0c/1e0096ced9c55f9c6c6655446798df74165780375d3f5ab5f33751e087ae/jaraco_context-6.0.2-py3-none-any.whl", hash = "sha256:55fc21af4b4f9ca94aa643b6ee7fe13b1e4c01abf3aeb98ca4ad9c80b741c786", size = 6988, upload-time = "2025-12-24T19:21:34.557Z" }, ] [[package]] @@ -1134,78 +1115,78 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "more-itertools" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/0f/27/056e0638a86749374d6f57d0b0db39f29509cce9313cf91bdc0ac4d91084/jaraco_functools-4.4.0.tar.gz", hash = "sha256:da21933b0417b89515562656547a77b4931f98176eb173644c0d35032a33d6bb", size = 19943 } +sdist = { url = "https://files.pythonhosted.org/packages/0f/27/056e0638a86749374d6f57d0b0db39f29509cce9313cf91bdc0ac4d91084/jaraco_functools-4.4.0.tar.gz", hash = "sha256:da21933b0417b89515562656547a77b4931f98176eb173644c0d35032a33d6bb", size = 19943, upload-time = "2025-12-21T09:29:43.6Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/fd/c4/813bb09f0985cb21e959f21f2464169eca882656849adf727ac7bb7e1767/jaraco_functools-4.4.0-py3-none-any.whl", hash = "sha256:9eec1e36f45c818d9bf307c8948eb03b2b56cd44087b3cdc989abca1f20b9176", size = 10481 }, + { url = "https://files.pythonhosted.org/packages/fd/c4/813bb09f0985cb21e959f21f2464169eca882656849adf727ac7bb7e1767/jaraco_functools-4.4.0-py3-none-any.whl", hash = "sha256:9eec1e36f45c818d9bf307c8948eb03b2b56cd44087b3cdc989abca1f20b9176", size = 10481, upload-time = "2025-12-21T09:29:42.27Z" }, ] [[package]] name = "jeepney" version = "0.9.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/7b/6f/357efd7602486741aa73ffc0617fb310a29b588ed0fd69c2399acbb85b0c/jeepney-0.9.0.tar.gz", hash = "sha256:cf0e9e845622b81e4a28df94c40345400256ec608d0e55bb8a3feaa9163f5732", size = 106758 } +sdist = { url = "https://files.pythonhosted.org/packages/7b/6f/357efd7602486741aa73ffc0617fb310a29b588ed0fd69c2399acbb85b0c/jeepney-0.9.0.tar.gz", hash = "sha256:cf0e9e845622b81e4a28df94c40345400256ec608d0e55bb8a3feaa9163f5732", size = 106758, upload-time = "2025-02-27T18:51:01.684Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b2/a3/e137168c9c44d18eff0376253da9f1e9234d0239e0ee230d2fee6cea8e55/jeepney-0.9.0-py3-none-any.whl", hash = "sha256:97e5714520c16fc0a45695e5365a2e11b81ea79bba796e26f9f1d178cb182683", size = 49010 }, + { url = "https://files.pythonhosted.org/packages/b2/a3/e137168c9c44d18eff0376253da9f1e9234d0239e0ee230d2fee6cea8e55/jeepney-0.9.0-py3-none-any.whl", hash = "sha256:97e5714520c16fc0a45695e5365a2e11b81ea79bba796e26f9f1d178cb182683", size = 49010, upload-time = "2025-02-27T18:51:00.104Z" }, ] [[package]] name = "jiter" version = "0.12.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/45/9d/e0660989c1370e25848bb4c52d061c71837239738ad937e83edca174c273/jiter-0.12.0.tar.gz", hash = "sha256:64dfcd7d5c168b38d3f9f8bba7fc639edb3418abcc74f22fdbe6b8938293f30b", size = 168294 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/3d/a6/97209693b177716e22576ee1161674d1d58029eb178e01866a0422b69224/jiter-0.12.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:6cc49d5130a14b732e0612bc76ae8db3b49898732223ef8b7599aa8d9810683e", size = 313658 }, - { url = "https://files.pythonhosted.org/packages/06/4d/125c5c1537c7d8ee73ad3d530a442d6c619714b95027143f1b61c0b4dfe0/jiter-0.12.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:37f27a32ce36364d2fa4f7fdc507279db604d27d239ea2e044c8f148410defe1", size = 318605 }, - { url = "https://files.pythonhosted.org/packages/99/bf/a840b89847885064c41a5f52de6e312e91fa84a520848ee56c97e4fa0205/jiter-0.12.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bbc0944aa3d4b4773e348cda635252824a78f4ba44328e042ef1ff3f6080d1cf", size = 349803 }, - { url = "https://files.pythonhosted.org/packages/8a/88/e63441c28e0db50e305ae23e19c1d8fae012d78ed55365da392c1f34b09c/jiter-0.12.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:da25c62d4ee1ffbacb97fac6dfe4dcd6759ebdc9015991e92a6eae5816287f44", size = 365120 }, - { url = "https://files.pythonhosted.org/packages/0a/7c/49b02714af4343970eb8aca63396bc1c82fa01197dbb1e9b0d274b550d4e/jiter-0.12.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:048485c654b838140b007390b8182ba9774621103bd4d77c9c3f6f117474ba45", size = 479918 }, - { url = "https://files.pythonhosted.org/packages/69/ba/0a809817fdd5a1db80490b9150645f3aae16afad166960bcd562be194f3b/jiter-0.12.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:635e737fbb7315bef0037c19b88b799143d2d7d3507e61a76751025226b3ac87", size = 379008 }, - { url = "https://files.pythonhosted.org/packages/5f/c3/c9fc0232e736c8877d9e6d83d6eeb0ba4e90c6c073835cc2e8f73fdeef51/jiter-0.12.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e017c417b1ebda911bd13b1e40612704b1f5420e30695112efdbed8a4b389ed", size = 361785 }, - { url = "https://files.pythonhosted.org/packages/96/61/61f69b7e442e97ca6cd53086ddc1cf59fb830549bc72c0a293713a60c525/jiter-0.12.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:89b0bfb8b2bf2351fba36bb211ef8bfceba73ef58e7f0c68fb67b5a2795ca2f9", size = 386108 }, - { url = "https://files.pythonhosted.org/packages/e9/2e/76bb3332f28550c8f1eba3bf6e5efe211efda0ddbbaf24976bc7078d42a5/jiter-0.12.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:f5aa5427a629a824a543672778c9ce0c5e556550d1569bb6ea28a85015287626", size = 519937 }, - { url = "https://files.pythonhosted.org/packages/84/d6/fa96efa87dc8bff2094fb947f51f66368fa56d8d4fc9e77b25d7fbb23375/jiter-0.12.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ed53b3d6acbcb0fd0b90f20c7cb3b24c357fe82a3518934d4edfa8c6898e498c", size = 510853 }, - { url = "https://files.pythonhosted.org/packages/8a/28/93f67fdb4d5904a708119a6ab58a8f1ec226ff10a94a282e0215402a8462/jiter-0.12.0-cp313-cp313-win32.whl", hash = "sha256:4747de73d6b8c78f2e253a2787930f4fffc68da7fa319739f57437f95963c4de", size = 204699 }, - { url = "https://files.pythonhosted.org/packages/c4/1f/30b0eb087045a0abe2a5c9c0c0c8da110875a1d3be83afd4a9a4e548be3c/jiter-0.12.0-cp313-cp313-win_amd64.whl", hash = "sha256:e25012eb0c456fcc13354255d0338cd5397cce26c77b2832b3c4e2e255ea5d9a", size = 204258 }, - { url = "https://files.pythonhosted.org/packages/2c/f4/2b4daf99b96bce6fc47971890b14b2a36aef88d7beb9f057fafa032c6141/jiter-0.12.0-cp313-cp313-win_arm64.whl", hash = "sha256:c97b92c54fe6110138c872add030a1f99aea2401ddcdaa21edf74705a646dd60", size = 185503 }, - { url = "https://files.pythonhosted.org/packages/39/ca/67bb15a7061d6fe20b9b2a2fd783e296a1e0f93468252c093481a2f00efa/jiter-0.12.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:53839b35a38f56b8be26a7851a48b89bc47e5d88e900929df10ed93b95fea3d6", size = 317965 }, - { url = "https://files.pythonhosted.org/packages/18/af/1788031cd22e29c3b14bc6ca80b16a39a0b10e611367ffd480c06a259831/jiter-0.12.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:94f669548e55c91ab47fef8bddd9c954dab1938644e715ea49d7e117015110a4", size = 345831 }, - { url = "https://files.pythonhosted.org/packages/05/17/710bf8472d1dff0d3caf4ced6031060091c1320f84ee7d5dcbed1f352417/jiter-0.12.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:351d54f2b09a41600ffea43d081522d792e81dcfb915f6d2d242744c1cc48beb", size = 361272 }, - { url = "https://files.pythonhosted.org/packages/fb/f1/1dcc4618b59761fef92d10bcbb0b038b5160be653b003651566a185f1a5c/jiter-0.12.0-cp313-cp313t-win_amd64.whl", hash = "sha256:2a5e90604620f94bf62264e7c2c038704d38217b7465b863896c6d7c902b06c7", size = 204604 }, - { url = "https://files.pythonhosted.org/packages/d9/32/63cb1d9f1c5c6632a783c0052cde9ef7ba82688f7065e2f0d5f10a7e3edb/jiter-0.12.0-cp313-cp313t-win_arm64.whl", hash = "sha256:88ef757017e78d2860f96250f9393b7b577b06a956ad102c29c8237554380db3", size = 185628 }, - { url = "https://files.pythonhosted.org/packages/a8/99/45c9f0dbe4a1416b2b9a8a6d1236459540f43d7fb8883cff769a8db0612d/jiter-0.12.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:c46d927acd09c67a9fb1416df45c5a04c27e83aae969267e98fba35b74e99525", size = 312478 }, - { url = "https://files.pythonhosted.org/packages/4c/a7/54ae75613ba9e0f55fcb0bc5d1f807823b5167cc944e9333ff322e9f07dd/jiter-0.12.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:774ff60b27a84a85b27b88cd5583899c59940bcc126caca97eb2a9df6aa00c49", size = 318706 }, - { url = "https://files.pythonhosted.org/packages/59/31/2aa241ad2c10774baf6c37f8b8e1f39c07db358f1329f4eb40eba179c2a2/jiter-0.12.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c5433fab222fb072237df3f637d01b81f040a07dcac1cb4a5c75c7aa9ed0bef1", size = 351894 }, - { url = "https://files.pythonhosted.org/packages/54/4f/0f2759522719133a9042781b18cc94e335b6d290f5e2d3e6899d6af933e3/jiter-0.12.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f8c593c6e71c07866ec6bfb790e202a833eeec885022296aff6b9e0b92d6a70e", size = 365714 }, - { url = "https://files.pythonhosted.org/packages/dc/6f/806b895f476582c62a2f52c453151edd8a0fde5411b0497baaa41018e878/jiter-0.12.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:90d32894d4c6877a87ae00c6b915b609406819dce8bc0d4e962e4de2784e567e", size = 478989 }, - { url = "https://files.pythonhosted.org/packages/86/6c/012d894dc6e1033acd8db2b8346add33e413ec1c7c002598915278a37f79/jiter-0.12.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:798e46eed9eb10c3adbbacbd3bdb5ecd4cf7064e453d00dbef08802dae6937ff", size = 378615 }, - { url = "https://files.pythonhosted.org/packages/87/30/d718d599f6700163e28e2c71c0bbaf6dace692e7df2592fd793ac9276717/jiter-0.12.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b3f1368f0a6719ea80013a4eb90ba72e75d7ea67cfc7846db2ca504f3df0169a", size = 364745 }, - { url = "https://files.pythonhosted.org/packages/8f/85/315b45ce4b6ddc7d7fceca24068543b02bdc8782942f4ee49d652e2cc89f/jiter-0.12.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:65f04a9d0b4406f7e51279710b27484af411896246200e461d80d3ba0caa901a", size = 386502 }, - { url = "https://files.pythonhosted.org/packages/74/0b/ce0434fb40c5b24b368fe81b17074d2840748b4952256bab451b72290a49/jiter-0.12.0-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:fd990541982a24281d12b67a335e44f117e4c6cbad3c3b75c7dea68bf4ce3a67", size = 519845 }, - { url = "https://files.pythonhosted.org/packages/e8/a3/7a7a4488ba052767846b9c916d208b3ed114e3eb670ee984e4c565b9cf0d/jiter-0.12.0-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:b111b0e9152fa7df870ecaebb0bd30240d9f7fff1f2003bcb4ed0f519941820b", size = 510701 }, - { url = "https://files.pythonhosted.org/packages/c3/16/052ffbf9d0467b70af24e30f91e0579e13ded0c17bb4a8eb2aed3cb60131/jiter-0.12.0-cp314-cp314-win32.whl", hash = "sha256:a78befb9cc0a45b5a5a0d537b06f8544c2ebb60d19d02c41ff15da28a9e22d42", size = 205029 }, - { url = "https://files.pythonhosted.org/packages/e4/18/3cf1f3f0ccc789f76b9a754bdb7a6977e5d1d671ee97a9e14f7eb728d80e/jiter-0.12.0-cp314-cp314-win_amd64.whl", hash = "sha256:e1fe01c082f6aafbe5c8faf0ff074f38dfb911d53f07ec333ca03f8f6226debf", size = 204960 }, - { url = "https://files.pythonhosted.org/packages/02/68/736821e52ecfdeeb0f024b8ab01b5a229f6b9293bbdb444c27efade50b0f/jiter-0.12.0-cp314-cp314-win_arm64.whl", hash = "sha256:d72f3b5a432a4c546ea4bedc84cce0c3404874f1d1676260b9c7f048a9855451", size = 185529 }, - { url = "https://files.pythonhosted.org/packages/30/61/12ed8ee7a643cce29ac97c2281f9ce3956eb76b037e88d290f4ed0d41480/jiter-0.12.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:e6ded41aeba3603f9728ed2b6196e4df875348ab97b28fc8afff115ed42ba7a7", size = 318974 }, - { url = "https://files.pythonhosted.org/packages/2d/c6/f3041ede6d0ed5e0e79ff0de4c8f14f401bbf196f2ef3971cdbe5fd08d1d/jiter-0.12.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a947920902420a6ada6ad51892082521978e9dd44a802663b001436e4b771684", size = 345932 }, - { url = "https://files.pythonhosted.org/packages/d5/5d/4d94835889edd01ad0e2dbfc05f7bdfaed46292e7b504a6ac7839aa00edb/jiter-0.12.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:add5e227e0554d3a52cf390a7635edaffdf4f8fce4fdbcef3cc2055bb396a30c", size = 367243 }, - { url = "https://files.pythonhosted.org/packages/fd/76/0051b0ac2816253a99d27baf3dda198663aff882fa6ea7deeb94046da24e/jiter-0.12.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3f9b1cda8fcb736250d7e8711d4580ebf004a46771432be0ae4796944b5dfa5d", size = 479315 }, - { url = "https://files.pythonhosted.org/packages/70/ae/83f793acd68e5cb24e483f44f482a1a15601848b9b6f199dacb970098f77/jiter-0.12.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:deeb12a2223fe0135c7ff1356a143d57f95bbf1f4a66584f1fc74df21d86b993", size = 380714 }, - { url = "https://files.pythonhosted.org/packages/b1/5e/4808a88338ad2c228b1126b93fcd8ba145e919e886fe910d578230dabe3b/jiter-0.12.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c596cc0f4cb574877550ce4ecd51f8037469146addd676d7c1a30ebe6391923f", size = 365168 }, - { url = "https://files.pythonhosted.org/packages/0c/d4/04619a9e8095b42aef436b5aeb4c0282b4ff1b27d1db1508df9f5dc82750/jiter-0.12.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5ab4c823b216a4aeab3fdbf579c5843165756bd9ad87cc6b1c65919c4715f783", size = 387893 }, - { url = "https://files.pythonhosted.org/packages/17/ea/d3c7e62e4546fdc39197fa4a4315a563a89b95b6d54c0d25373842a59cbe/jiter-0.12.0-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:e427eee51149edf962203ff8db75a7514ab89be5cb623fb9cea1f20b54f1107b", size = 520828 }, - { url = "https://files.pythonhosted.org/packages/cc/0b/c6d3562a03fd767e31cb119d9041ea7958c3c80cb3d753eafb19b3b18349/jiter-0.12.0-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:edb868841f84c111255ba5e80339d386d937ec1fdce419518ce1bd9370fac5b6", size = 511009 }, - { url = "https://files.pythonhosted.org/packages/aa/51/2cb4468b3448a8385ebcd15059d325c9ce67df4e2758d133ab9442b19834/jiter-0.12.0-cp314-cp314t-win32.whl", hash = "sha256:8bbcfe2791dfdb7c5e48baf646d37a6a3dcb5a97a032017741dea9f817dca183", size = 205110 }, - { url = "https://files.pythonhosted.org/packages/b2/c5/ae5ec83dec9c2d1af805fd5fe8f74ebded9c8670c5210ec7820ce0dbeb1e/jiter-0.12.0-cp314-cp314t-win_amd64.whl", hash = "sha256:2fa940963bf02e1d8226027ef461e36af472dea85d36054ff835aeed944dd873", size = 205223 }, - { url = "https://files.pythonhosted.org/packages/97/9a/3c5391907277f0e55195550cf3fa8e293ae9ee0c00fb402fec1e38c0c82f/jiter-0.12.0-cp314-cp314t-win_arm64.whl", hash = "sha256:506c9708dd29b27288f9f8f1140c3cb0e3d8ddb045956d7757b1fa0e0f39a473", size = 185564 }, +sdist = { url = "https://files.pythonhosted.org/packages/45/9d/e0660989c1370e25848bb4c52d061c71837239738ad937e83edca174c273/jiter-0.12.0.tar.gz", hash = "sha256:64dfcd7d5c168b38d3f9f8bba7fc639edb3418abcc74f22fdbe6b8938293f30b", size = 168294, upload-time = "2025-11-09T20:49:23.302Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3d/a6/97209693b177716e22576ee1161674d1d58029eb178e01866a0422b69224/jiter-0.12.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:6cc49d5130a14b732e0612bc76ae8db3b49898732223ef8b7599aa8d9810683e", size = 313658, upload-time = "2025-11-09T20:47:44.424Z" }, + { url = "https://files.pythonhosted.org/packages/06/4d/125c5c1537c7d8ee73ad3d530a442d6c619714b95027143f1b61c0b4dfe0/jiter-0.12.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:37f27a32ce36364d2fa4f7fdc507279db604d27d239ea2e044c8f148410defe1", size = 318605, upload-time = "2025-11-09T20:47:45.973Z" }, + { url = "https://files.pythonhosted.org/packages/99/bf/a840b89847885064c41a5f52de6e312e91fa84a520848ee56c97e4fa0205/jiter-0.12.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bbc0944aa3d4b4773e348cda635252824a78f4ba44328e042ef1ff3f6080d1cf", size = 349803, upload-time = "2025-11-09T20:47:47.535Z" }, + { url = "https://files.pythonhosted.org/packages/8a/88/e63441c28e0db50e305ae23e19c1d8fae012d78ed55365da392c1f34b09c/jiter-0.12.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:da25c62d4ee1ffbacb97fac6dfe4dcd6759ebdc9015991e92a6eae5816287f44", size = 365120, upload-time = "2025-11-09T20:47:49.284Z" }, + { url = "https://files.pythonhosted.org/packages/0a/7c/49b02714af4343970eb8aca63396bc1c82fa01197dbb1e9b0d274b550d4e/jiter-0.12.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:048485c654b838140b007390b8182ba9774621103bd4d77c9c3f6f117474ba45", size = 479918, upload-time = "2025-11-09T20:47:50.807Z" }, + { url = "https://files.pythonhosted.org/packages/69/ba/0a809817fdd5a1db80490b9150645f3aae16afad166960bcd562be194f3b/jiter-0.12.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:635e737fbb7315bef0037c19b88b799143d2d7d3507e61a76751025226b3ac87", size = 379008, upload-time = "2025-11-09T20:47:52.211Z" }, + { url = "https://files.pythonhosted.org/packages/5f/c3/c9fc0232e736c8877d9e6d83d6eeb0ba4e90c6c073835cc2e8f73fdeef51/jiter-0.12.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e017c417b1ebda911bd13b1e40612704b1f5420e30695112efdbed8a4b389ed", size = 361785, upload-time = "2025-11-09T20:47:53.512Z" }, + { url = "https://files.pythonhosted.org/packages/96/61/61f69b7e442e97ca6cd53086ddc1cf59fb830549bc72c0a293713a60c525/jiter-0.12.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:89b0bfb8b2bf2351fba36bb211ef8bfceba73ef58e7f0c68fb67b5a2795ca2f9", size = 386108, upload-time = "2025-11-09T20:47:54.893Z" }, + { url = "https://files.pythonhosted.org/packages/e9/2e/76bb3332f28550c8f1eba3bf6e5efe211efda0ddbbaf24976bc7078d42a5/jiter-0.12.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:f5aa5427a629a824a543672778c9ce0c5e556550d1569bb6ea28a85015287626", size = 519937, upload-time = "2025-11-09T20:47:56.253Z" }, + { url = "https://files.pythonhosted.org/packages/84/d6/fa96efa87dc8bff2094fb947f51f66368fa56d8d4fc9e77b25d7fbb23375/jiter-0.12.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ed53b3d6acbcb0fd0b90f20c7cb3b24c357fe82a3518934d4edfa8c6898e498c", size = 510853, upload-time = "2025-11-09T20:47:58.32Z" }, + { url = "https://files.pythonhosted.org/packages/8a/28/93f67fdb4d5904a708119a6ab58a8f1ec226ff10a94a282e0215402a8462/jiter-0.12.0-cp313-cp313-win32.whl", hash = "sha256:4747de73d6b8c78f2e253a2787930f4fffc68da7fa319739f57437f95963c4de", size = 204699, upload-time = "2025-11-09T20:47:59.686Z" }, + { url = "https://files.pythonhosted.org/packages/c4/1f/30b0eb087045a0abe2a5c9c0c0c8da110875a1d3be83afd4a9a4e548be3c/jiter-0.12.0-cp313-cp313-win_amd64.whl", hash = "sha256:e25012eb0c456fcc13354255d0338cd5397cce26c77b2832b3c4e2e255ea5d9a", size = 204258, upload-time = "2025-11-09T20:48:01.01Z" }, + { url = "https://files.pythonhosted.org/packages/2c/f4/2b4daf99b96bce6fc47971890b14b2a36aef88d7beb9f057fafa032c6141/jiter-0.12.0-cp313-cp313-win_arm64.whl", hash = "sha256:c97b92c54fe6110138c872add030a1f99aea2401ddcdaa21edf74705a646dd60", size = 185503, upload-time = "2025-11-09T20:48:02.35Z" }, + { url = "https://files.pythonhosted.org/packages/39/ca/67bb15a7061d6fe20b9b2a2fd783e296a1e0f93468252c093481a2f00efa/jiter-0.12.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:53839b35a38f56b8be26a7851a48b89bc47e5d88e900929df10ed93b95fea3d6", size = 317965, upload-time = "2025-11-09T20:48:03.783Z" }, + { url = "https://files.pythonhosted.org/packages/18/af/1788031cd22e29c3b14bc6ca80b16a39a0b10e611367ffd480c06a259831/jiter-0.12.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:94f669548e55c91ab47fef8bddd9c954dab1938644e715ea49d7e117015110a4", size = 345831, upload-time = "2025-11-09T20:48:05.55Z" }, + { url = "https://files.pythonhosted.org/packages/05/17/710bf8472d1dff0d3caf4ced6031060091c1320f84ee7d5dcbed1f352417/jiter-0.12.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:351d54f2b09a41600ffea43d081522d792e81dcfb915f6d2d242744c1cc48beb", size = 361272, upload-time = "2025-11-09T20:48:06.951Z" }, + { url = "https://files.pythonhosted.org/packages/fb/f1/1dcc4618b59761fef92d10bcbb0b038b5160be653b003651566a185f1a5c/jiter-0.12.0-cp313-cp313t-win_amd64.whl", hash = "sha256:2a5e90604620f94bf62264e7c2c038704d38217b7465b863896c6d7c902b06c7", size = 204604, upload-time = "2025-11-09T20:48:08.328Z" }, + { url = "https://files.pythonhosted.org/packages/d9/32/63cb1d9f1c5c6632a783c0052cde9ef7ba82688f7065e2f0d5f10a7e3edb/jiter-0.12.0-cp313-cp313t-win_arm64.whl", hash = "sha256:88ef757017e78d2860f96250f9393b7b577b06a956ad102c29c8237554380db3", size = 185628, upload-time = "2025-11-09T20:48:09.572Z" }, + { url = "https://files.pythonhosted.org/packages/a8/99/45c9f0dbe4a1416b2b9a8a6d1236459540f43d7fb8883cff769a8db0612d/jiter-0.12.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:c46d927acd09c67a9fb1416df45c5a04c27e83aae969267e98fba35b74e99525", size = 312478, upload-time = "2025-11-09T20:48:10.898Z" }, + { url = "https://files.pythonhosted.org/packages/4c/a7/54ae75613ba9e0f55fcb0bc5d1f807823b5167cc944e9333ff322e9f07dd/jiter-0.12.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:774ff60b27a84a85b27b88cd5583899c59940bcc126caca97eb2a9df6aa00c49", size = 318706, upload-time = "2025-11-09T20:48:12.266Z" }, + { url = "https://files.pythonhosted.org/packages/59/31/2aa241ad2c10774baf6c37f8b8e1f39c07db358f1329f4eb40eba179c2a2/jiter-0.12.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c5433fab222fb072237df3f637d01b81f040a07dcac1cb4a5c75c7aa9ed0bef1", size = 351894, upload-time = "2025-11-09T20:48:13.673Z" }, + { url = "https://files.pythonhosted.org/packages/54/4f/0f2759522719133a9042781b18cc94e335b6d290f5e2d3e6899d6af933e3/jiter-0.12.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f8c593c6e71c07866ec6bfb790e202a833eeec885022296aff6b9e0b92d6a70e", size = 365714, upload-time = "2025-11-09T20:48:15.083Z" }, + { url = "https://files.pythonhosted.org/packages/dc/6f/806b895f476582c62a2f52c453151edd8a0fde5411b0497baaa41018e878/jiter-0.12.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:90d32894d4c6877a87ae00c6b915b609406819dce8bc0d4e962e4de2784e567e", size = 478989, upload-time = "2025-11-09T20:48:16.706Z" }, + { url = "https://files.pythonhosted.org/packages/86/6c/012d894dc6e1033acd8db2b8346add33e413ec1c7c002598915278a37f79/jiter-0.12.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:798e46eed9eb10c3adbbacbd3bdb5ecd4cf7064e453d00dbef08802dae6937ff", size = 378615, upload-time = "2025-11-09T20:48:18.614Z" }, + { url = "https://files.pythonhosted.org/packages/87/30/d718d599f6700163e28e2c71c0bbaf6dace692e7df2592fd793ac9276717/jiter-0.12.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b3f1368f0a6719ea80013a4eb90ba72e75d7ea67cfc7846db2ca504f3df0169a", size = 364745, upload-time = "2025-11-09T20:48:20.117Z" }, + { url = "https://files.pythonhosted.org/packages/8f/85/315b45ce4b6ddc7d7fceca24068543b02bdc8782942f4ee49d652e2cc89f/jiter-0.12.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:65f04a9d0b4406f7e51279710b27484af411896246200e461d80d3ba0caa901a", size = 386502, upload-time = "2025-11-09T20:48:21.543Z" }, + { url = "https://files.pythonhosted.org/packages/74/0b/ce0434fb40c5b24b368fe81b17074d2840748b4952256bab451b72290a49/jiter-0.12.0-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:fd990541982a24281d12b67a335e44f117e4c6cbad3c3b75c7dea68bf4ce3a67", size = 519845, upload-time = "2025-11-09T20:48:22.964Z" }, + { url = "https://files.pythonhosted.org/packages/e8/a3/7a7a4488ba052767846b9c916d208b3ed114e3eb670ee984e4c565b9cf0d/jiter-0.12.0-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:b111b0e9152fa7df870ecaebb0bd30240d9f7fff1f2003bcb4ed0f519941820b", size = 510701, upload-time = "2025-11-09T20:48:24.483Z" }, + { url = "https://files.pythonhosted.org/packages/c3/16/052ffbf9d0467b70af24e30f91e0579e13ded0c17bb4a8eb2aed3cb60131/jiter-0.12.0-cp314-cp314-win32.whl", hash = "sha256:a78befb9cc0a45b5a5a0d537b06f8544c2ebb60d19d02c41ff15da28a9e22d42", size = 205029, upload-time = "2025-11-09T20:48:25.749Z" }, + { url = "https://files.pythonhosted.org/packages/e4/18/3cf1f3f0ccc789f76b9a754bdb7a6977e5d1d671ee97a9e14f7eb728d80e/jiter-0.12.0-cp314-cp314-win_amd64.whl", hash = "sha256:e1fe01c082f6aafbe5c8faf0ff074f38dfb911d53f07ec333ca03f8f6226debf", size = 204960, upload-time = "2025-11-09T20:48:27.415Z" }, + { url = "https://files.pythonhosted.org/packages/02/68/736821e52ecfdeeb0f024b8ab01b5a229f6b9293bbdb444c27efade50b0f/jiter-0.12.0-cp314-cp314-win_arm64.whl", hash = "sha256:d72f3b5a432a4c546ea4bedc84cce0c3404874f1d1676260b9c7f048a9855451", size = 185529, upload-time = "2025-11-09T20:48:29.125Z" }, + { url = "https://files.pythonhosted.org/packages/30/61/12ed8ee7a643cce29ac97c2281f9ce3956eb76b037e88d290f4ed0d41480/jiter-0.12.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:e6ded41aeba3603f9728ed2b6196e4df875348ab97b28fc8afff115ed42ba7a7", size = 318974, upload-time = "2025-11-09T20:48:30.87Z" }, + { url = "https://files.pythonhosted.org/packages/2d/c6/f3041ede6d0ed5e0e79ff0de4c8f14f401bbf196f2ef3971cdbe5fd08d1d/jiter-0.12.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a947920902420a6ada6ad51892082521978e9dd44a802663b001436e4b771684", size = 345932, upload-time = "2025-11-09T20:48:32.658Z" }, + { url = "https://files.pythonhosted.org/packages/d5/5d/4d94835889edd01ad0e2dbfc05f7bdfaed46292e7b504a6ac7839aa00edb/jiter-0.12.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:add5e227e0554d3a52cf390a7635edaffdf4f8fce4fdbcef3cc2055bb396a30c", size = 367243, upload-time = "2025-11-09T20:48:34.093Z" }, + { url = "https://files.pythonhosted.org/packages/fd/76/0051b0ac2816253a99d27baf3dda198663aff882fa6ea7deeb94046da24e/jiter-0.12.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3f9b1cda8fcb736250d7e8711d4580ebf004a46771432be0ae4796944b5dfa5d", size = 479315, upload-time = "2025-11-09T20:48:35.507Z" }, + { url = "https://files.pythonhosted.org/packages/70/ae/83f793acd68e5cb24e483f44f482a1a15601848b9b6f199dacb970098f77/jiter-0.12.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:deeb12a2223fe0135c7ff1356a143d57f95bbf1f4a66584f1fc74df21d86b993", size = 380714, upload-time = "2025-11-09T20:48:40.014Z" }, + { url = "https://files.pythonhosted.org/packages/b1/5e/4808a88338ad2c228b1126b93fcd8ba145e919e886fe910d578230dabe3b/jiter-0.12.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c596cc0f4cb574877550ce4ecd51f8037469146addd676d7c1a30ebe6391923f", size = 365168, upload-time = "2025-11-09T20:48:41.462Z" }, + { url = "https://files.pythonhosted.org/packages/0c/d4/04619a9e8095b42aef436b5aeb4c0282b4ff1b27d1db1508df9f5dc82750/jiter-0.12.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5ab4c823b216a4aeab3fdbf579c5843165756bd9ad87cc6b1c65919c4715f783", size = 387893, upload-time = "2025-11-09T20:48:42.921Z" }, + { url = "https://files.pythonhosted.org/packages/17/ea/d3c7e62e4546fdc39197fa4a4315a563a89b95b6d54c0d25373842a59cbe/jiter-0.12.0-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:e427eee51149edf962203ff8db75a7514ab89be5cb623fb9cea1f20b54f1107b", size = 520828, upload-time = "2025-11-09T20:48:44.278Z" }, + { url = "https://files.pythonhosted.org/packages/cc/0b/c6d3562a03fd767e31cb119d9041ea7958c3c80cb3d753eafb19b3b18349/jiter-0.12.0-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:edb868841f84c111255ba5e80339d386d937ec1fdce419518ce1bd9370fac5b6", size = 511009, upload-time = "2025-11-09T20:48:45.726Z" }, + { url = "https://files.pythonhosted.org/packages/aa/51/2cb4468b3448a8385ebcd15059d325c9ce67df4e2758d133ab9442b19834/jiter-0.12.0-cp314-cp314t-win32.whl", hash = "sha256:8bbcfe2791dfdb7c5e48baf646d37a6a3dcb5a97a032017741dea9f817dca183", size = 205110, upload-time = "2025-11-09T20:48:47.033Z" }, + { url = "https://files.pythonhosted.org/packages/b2/c5/ae5ec83dec9c2d1af805fd5fe8f74ebded9c8670c5210ec7820ce0dbeb1e/jiter-0.12.0-cp314-cp314t-win_amd64.whl", hash = "sha256:2fa940963bf02e1d8226027ef461e36af472dea85d36054ff835aeed944dd873", size = 205223, upload-time = "2025-11-09T20:48:49.076Z" }, + { url = "https://files.pythonhosted.org/packages/97/9a/3c5391907277f0e55195550cf3fa8e293ae9ee0c00fb402fec1e38c0c82f/jiter-0.12.0-cp314-cp314t-win_arm64.whl", hash = "sha256:506c9708dd29b27288f9f8f1140c3cb0e3d8ddb045956d7757b1fa0e0f39a473", size = 185564, upload-time = "2025-11-09T20:48:50.376Z" }, ] [[package]] name = "jmespath" version = "1.0.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/00/2a/e867e8531cf3e36b41201936b7fa7ba7b5702dbef42922193f05c8976cd6/jmespath-1.0.1.tar.gz", hash = "sha256:90261b206d6defd58fdd5e85f478bf633a2901798906be2ad389150c5c60edbe", size = 25843 } +sdist = { url = "https://files.pythonhosted.org/packages/00/2a/e867e8531cf3e36b41201936b7fa7ba7b5702dbef42922193f05c8976cd6/jmespath-1.0.1.tar.gz", hash = "sha256:90261b206d6defd58fdd5e85f478bf633a2901798906be2ad389150c5c60edbe", size = 25843, upload-time = "2022-06-17T18:00:12.224Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/31/b4/b9b800c45527aadd64d5b442f9b932b00648617eb5d63d2c7a6587b7cafc/jmespath-1.0.1-py3-none-any.whl", hash = "sha256:02e2e4cc71b5bcab88332eebf907519190dd9e6e82107fa7f83b1003a6252980", size = 20256 }, + { url = "https://files.pythonhosted.org/packages/31/b4/b9b800c45527aadd64d5b442f9b932b00648617eb5d63d2c7a6587b7cafc/jmespath-1.0.1-py3-none-any.whl", hash = "sha256:02e2e4cc71b5bcab88332eebf907519190dd9e6e82107fa7f83b1003a6252980", size = 20256, upload-time = "2022-06-17T18:00:10.251Z" }, ] [[package]] @@ -1218,9 +1199,9 @@ dependencies = [ { name = "referencing" }, { name = "rpds-py" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/74/69/f7185de793a29082a9f3c7728268ffb31cb5095131a9c139a74078e27336/jsonschema-4.25.1.tar.gz", hash = "sha256:e4a9655ce0da0c0b67a085847e00a3a51449e1157f4f75e9fb5aa545e122eb85", size = 357342 } +sdist = { url = "https://files.pythonhosted.org/packages/74/69/f7185de793a29082a9f3c7728268ffb31cb5095131a9c139a74078e27336/jsonschema-4.25.1.tar.gz", hash = "sha256:e4a9655ce0da0c0b67a085847e00a3a51449e1157f4f75e9fb5aa545e122eb85", size = 357342, upload-time = "2025-08-18T17:03:50.038Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/bf/9c/8c95d856233c1f82500c2450b8c68576b4cf1c871db3afac5c34ff84e6fd/jsonschema-4.25.1-py3-none-any.whl", hash = "sha256:3fba0169e345c7175110351d456342c364814cfcf3b964ba4587f22915230a63", size = 90040 }, + { url = "https://files.pythonhosted.org/packages/bf/9c/8c95d856233c1f82500c2450b8c68576b4cf1c871db3afac5c34ff84e6fd/jsonschema-4.25.1-py3-none-any.whl", hash = "sha256:3fba0169e345c7175110351d456342c364814cfcf3b964ba4587f22915230a63", size = 90040, upload-time = "2025-08-18T17:03:48.373Z" }, ] [[package]] @@ -1233,9 +1214,9 @@ dependencies = [ { name = "referencing" }, { name = "requests" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/6e/45/41ebc679c2a4fced6a722f624c18d658dee42612b83ea24c1caf7c0eb3a8/jsonschema_path-0.3.4.tar.gz", hash = "sha256:8365356039f16cc65fddffafda5f58766e34bebab7d6d105616ab52bc4297001", size = 11159 } +sdist = { url = "https://files.pythonhosted.org/packages/6e/45/41ebc679c2a4fced6a722f624c18d658dee42612b83ea24c1caf7c0eb3a8/jsonschema_path-0.3.4.tar.gz", hash = "sha256:8365356039f16cc65fddffafda5f58766e34bebab7d6d105616ab52bc4297001", size = 11159, upload-time = "2025-01-24T14:33:16.547Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/cb/58/3485da8cb93d2f393bce453adeef16896751f14ba3e2024bc21dc9597646/jsonschema_path-0.3.4-py3-none-any.whl", hash = "sha256:f502191fdc2b22050f9a81c9237be9d27145b9001c55842bece5e94e382e52f8", size = 14810 }, + { url = "https://files.pythonhosted.org/packages/cb/58/3485da8cb93d2f393bce453adeef16896751f14ba3e2024bc21dc9597646/jsonschema_path-0.3.4-py3-none-any.whl", hash = "sha256:f502191fdc2b22050f9a81c9237be9d27145b9001c55842bece5e94e382e52f8", size = 14810, upload-time = "2025-01-24T14:33:14.652Z" }, ] [[package]] @@ -1245,9 +1226,9 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "referencing" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/19/74/a633ee74eb36c44aa6d1095e7cc5569bebf04342ee146178e2d36600708b/jsonschema_specifications-2025.9.1.tar.gz", hash = "sha256:b540987f239e745613c7a9176f3edb72b832a4ac465cf02712288397832b5e8d", size = 32855 } +sdist = { url = "https://files.pythonhosted.org/packages/19/74/a633ee74eb36c44aa6d1095e7cc5569bebf04342ee146178e2d36600708b/jsonschema_specifications-2025.9.1.tar.gz", hash = "sha256:b540987f239e745613c7a9176f3edb72b832a4ac465cf02712288397832b5e8d", size = 32855, upload-time = "2025-09-08T01:34:59.186Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl", hash = "sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe", size = 18437 }, + { url = "https://files.pythonhosted.org/packages/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl", hash = "sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe", size = 18437, upload-time = "2025-09-08T01:34:57.871Z" }, ] [[package]] @@ -1262,9 +1243,9 @@ dependencies = [ { name = "pywin32-ctypes", marker = "sys_platform == 'win32'" }, { name = "secretstorage", marker = "sys_platform == 'linux'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/43/4b/674af6ef2f97d56f0ab5153bf0bfa28ccb6c3ed4d1babf4305449668807b/keyring-25.7.0.tar.gz", hash = "sha256:fe01bd85eb3f8fb3dd0405defdeac9a5b4f6f0439edbb3149577f244a2e8245b", size = 63516 } +sdist = { url = "https://files.pythonhosted.org/packages/43/4b/674af6ef2f97d56f0ab5153bf0bfa28ccb6c3ed4d1babf4305449668807b/keyring-25.7.0.tar.gz", hash = "sha256:fe01bd85eb3f8fb3dd0405defdeac9a5b4f6f0439edbb3149577f244a2e8245b", size = 63516, upload-time = "2025-11-16T16:26:09.482Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/81/db/e655086b7f3a705df045bf0933bdd9c2f79bb3c97bfef1384598bb79a217/keyring-25.7.0-py3-none-any.whl", hash = "sha256:be4a0b195f149690c166e850609a477c532ddbfbaed96a404d4e43f8d5e2689f", size = 39160 }, + { url = "https://files.pythonhosted.org/packages/81/db/e655086b7f3a705df045bf0933bdd9c2f79bb3c97bfef1384598bb79a217/keyring-25.7.0-py3-none-any.whl", hash = "sha256:be4a0b195f149690c166e850609a477c532ddbfbaed96a404d4e43f8d5e2689f", size = 39160, upload-time = "2025-11-16T16:26:08.402Z" }, ] [[package]] @@ -1283,9 +1264,9 @@ dependencies = [ { name = "urllib3" }, { name = "websocket-client" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ef/55/3f880ef65f559cbed44a9aa20d3bdbc219a2c3a3bac4a30a513029b03ee9/kubernetes-34.1.0.tar.gz", hash = "sha256:8fe8edb0b5d290a2f3ac06596b23f87c658977d46b5f8df9d0f4ea83d0003912", size = 1083771 } +sdist = { url = "https://files.pythonhosted.org/packages/ef/55/3f880ef65f559cbed44a9aa20d3bdbc219a2c3a3bac4a30a513029b03ee9/kubernetes-34.1.0.tar.gz", hash = "sha256:8fe8edb0b5d290a2f3ac06596b23f87c658977d46b5f8df9d0f4ea83d0003912", size = 1083771, upload-time = "2025-09-29T20:23:49.283Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ca/ec/65f7d563aa4a62dd58777e8f6aa882f15db53b14eb29aba0c28a20f7eb26/kubernetes-34.1.0-py2.py3-none-any.whl", hash = "sha256:bffba2272534e224e6a7a74d582deb0b545b7c9879d2cd9e4aae9481d1f2cc2a", size = 2008380 }, + { url = "https://files.pythonhosted.org/packages/ca/ec/65f7d563aa4a62dd58777e8f6aa882f15db53b14eb29aba0c28a20f7eb26/kubernetes-34.1.0-py2.py3-none-any.whl", hash = "sha256:bffba2272534e224e6a7a74d582deb0b545b7c9879d2cd9e4aae9481d1f2cc2a", size = 2008380, upload-time = "2025-09-29T20:23:47.684Z" }, ] [[package]] @@ -1301,9 +1282,9 @@ dependencies = [ { name = "rich" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/e2/60/b8040db3598a55da64c45e3e689f2baa87389a4648a6f46ba80be3329f23/logfire-4.16.0.tar.gz", hash = "sha256:03a3ab8fdc13399309cb55d69cba7a6fcbad3526cfad85fc4f72e7d75e22b654", size = 550759 } +sdist = { url = "https://files.pythonhosted.org/packages/e2/60/b8040db3598a55da64c45e3e689f2baa87389a4648a6f46ba80be3329f23/logfire-4.16.0.tar.gz", hash = "sha256:03a3ab8fdc13399309cb55d69cba7a6fcbad3526cfad85fc4f72e7d75e22b654", size = 550759, upload-time = "2025-12-04T16:16:39.477Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/53/f7/ffcf81eb4aea75e40c0646b9519947d2070626c5d533922df92975045181/logfire-4.16.0-py3-none-any.whl", hash = "sha256:8f895f6c2efa593ad6d49e1b06d8e6e351d3dd0cad61ce5def0c3d401f8ea707", size = 229122 }, + { url = "https://files.pythonhosted.org/packages/53/f7/ffcf81eb4aea75e40c0646b9519947d2070626c5d533922df92975045181/logfire-4.16.0-py3-none-any.whl", hash = "sha256:8f895f6c2efa593ad6d49e1b06d8e6e351d3dd0cad61ce5def0c3d401f8ea707", size = 229122, upload-time = "2025-12-04T16:16:35.963Z" }, ] [package.optional-dependencies] @@ -1315,50 +1296,50 @@ httpx = [ name = "logfire-api" version = "4.16.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/9a/d9/d5f5e276371d5c8cde559d558de44b8378641231a23f3a632ebfe4b05c9b/logfire_api-4.16.0.tar.gz", hash = "sha256:0efa62f5e73abdea670b5e9384c841b544474207110a089536a0fa8704f9e386", size = 57702 } +sdist = { url = "https://files.pythonhosted.org/packages/9a/d9/d5f5e276371d5c8cde559d558de44b8378641231a23f3a632ebfe4b05c9b/logfire_api-4.16.0.tar.gz", hash = "sha256:0efa62f5e73abdea670b5e9384c841b544474207110a089536a0fa8704f9e386", size = 57702, upload-time = "2025-12-04T16:16:40.725Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/9b/6e/6d500ce6352c54566d03c65d92a8f3fc7045645814de046707b105dda2a6/logfire_api-4.16.0-py3-none-any.whl", hash = "sha256:7351153c35cb61f0f89d2d4123ebf99b5469d70ef34c613a5ce56f85bf1b14fb", size = 95247 }, + { url = "https://files.pythonhosted.org/packages/9b/6e/6d500ce6352c54566d03c65d92a8f3fc7045645814de046707b105dda2a6/logfire_api-4.16.0-py3-none-any.whl", hash = "sha256:7351153c35cb61f0f89d2d4123ebf99b5469d70ef34c613a5ce56f85bf1b14fb", size = 95247, upload-time = "2025-12-04T16:16:38.007Z" }, ] [[package]] name = "lupa" version = "2.6" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b8/1c/191c3e6ec6502e3dbe25a53e27f69a5daeac3e56de1f73c0138224171ead/lupa-2.6.tar.gz", hash = "sha256:9a770a6e89576be3447668d7ced312cd6fd41d3c13c2462c9dc2c2ab570e45d9", size = 7240282 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/28/1d/21176b682ca5469001199d8b95fa1737e29957a3d185186e7a8b55345f2e/lupa-2.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:663a6e58a0f60e7d212017d6678639ac8df0119bc13c2145029dcba084391310", size = 947232 }, - { url = "https://files.pythonhosted.org/packages/ce/4c/d327befb684660ca13cf79cd1f1d604331808f9f1b6fb6bf57832f8edf80/lupa-2.6-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:d1f5afda5c20b1f3217a80e9bc1b77037f8a6eb11612fd3ada19065303c8f380", size = 1908625 }, - { url = "https://files.pythonhosted.org/packages/66/8e/ad22b0a19454dfd08662237a84c792d6d420d36b061f239e084f29d1a4f3/lupa-2.6-cp313-cp313-macosx_11_0_x86_64.whl", hash = "sha256:26f2b3c085fe76e9119e48c1013c1cccdc1f51585d456858290475aa38e7089e", size = 981057 }, - { url = "https://files.pythonhosted.org/packages/5c/48/74859073ab276bd0566c719f9ca0108b0cfc1956ca0d68678d117d47d155/lupa-2.6-cp313-cp313-manylinux2010_i686.manylinux_2_12_i686.manylinux_2_28_i686.whl", hash = "sha256:60d2f902c7b96fb8ab98493dcff315e7bb4d0b44dc9dd76eb37de575025d5685", size = 1156227 }, - { url = "https://files.pythonhosted.org/packages/09/6c/0e9ded061916877253c2266074060eb71ed99fb21d73c8c114a76725bce2/lupa-2.6-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a02d25dee3a3250967c36590128d9220ae02f2eda166a24279da0b481519cbff", size = 1035752 }, - { url = "https://files.pythonhosted.org/packages/dd/ef/f8c32e454ef9f3fe909f6c7d57a39f950996c37a3deb7b391fec7903dab7/lupa-2.6-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6eae1ee16b886b8914ff292dbefbf2f48abfbdee94b33a88d1d5475e02423203", size = 2069009 }, - { url = "https://files.pythonhosted.org/packages/53/dc/15b80c226a5225815a890ee1c11f07968e0aba7a852df41e8ae6fe285063/lupa-2.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b0edd5073a4ee74ab36f74fe61450148e6044f3952b8d21248581f3c5d1a58be", size = 1056301 }, - { url = "https://files.pythonhosted.org/packages/31/14/2086c1425c985acfb30997a67e90c39457122df41324d3c179d6ee2292c6/lupa-2.6-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:0c53ee9f22a8a17e7d4266ad48e86f43771951797042dd51d1494aaa4f5f3f0a", size = 1170673 }, - { url = "https://files.pythonhosted.org/packages/10/e5/b216c054cf86576c0191bf9a9f05de6f7e8e07164897d95eea0078dca9b2/lupa-2.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:de7c0f157a9064a400d828789191a96da7f4ce889969a588b87ec80de9b14772", size = 2162227 }, - { url = "https://files.pythonhosted.org/packages/59/2f/33ecb5bedf4f3bc297ceacb7f016ff951331d352f58e7e791589609ea306/lupa-2.6-cp313-cp313-win32.whl", hash = "sha256:ee9523941ae0a87b5b703417720c5d78f72d2f5bc23883a2ea80a949a3ed9e75", size = 1419558 }, - { url = "https://files.pythonhosted.org/packages/f9/b4/55e885834c847ea610e111d87b9ed4768f0afdaeebc00cd46810f25029f6/lupa-2.6-cp313-cp313-win_amd64.whl", hash = "sha256:b1335a5835b0a25ebdbc75cf0bda195e54d133e4d994877ef025e218c2e59db9", size = 1683424 }, - { url = "https://files.pythonhosted.org/packages/66/9d/d9427394e54d22a35d1139ef12e845fd700d4872a67a34db32516170b746/lupa-2.6-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:dcb6d0a3264873e1653bc188499f48c1fb4b41a779e315eba45256cfe7bc33c1", size = 953818 }, - { url = "https://files.pythonhosted.org/packages/10/41/27bbe81953fb2f9ecfced5d9c99f85b37964cfaf6aa8453bb11283983721/lupa-2.6-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:a37e01f2128f8c36106726cb9d360bac087d58c54b4522b033cc5691c584db18", size = 1915850 }, - { url = "https://files.pythonhosted.org/packages/a3/98/f9ff60db84a75ba8725506bbf448fb085bc77868a021998ed2a66d920568/lupa-2.6-cp314-cp314-macosx_11_0_x86_64.whl", hash = "sha256:458bd7e9ff3c150b245b0fcfbb9bd2593d1152ea7f0a7b91c1d185846da033fe", size = 982344 }, - { url = "https://files.pythonhosted.org/packages/41/f7/f39e0f1c055c3b887d86b404aaf0ca197b5edfd235a8b81b45b25bac7fc3/lupa-2.6-cp314-cp314-manylinux2010_i686.manylinux_2_12_i686.manylinux_2_28_i686.whl", hash = "sha256:052ee82cac5206a02df77119c325339acbc09f5ce66967f66a2e12a0f3211cad", size = 1156543 }, - { url = "https://files.pythonhosted.org/packages/9e/9c/59e6cffa0d672d662ae17bd7ac8ecd2c89c9449dee499e3eb13ca9cd10d9/lupa-2.6-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96594eca3c87dd07938009e95e591e43d554c1dbd0385be03c100367141db5a8", size = 1047974 }, - { url = "https://files.pythonhosted.org/packages/23/c6/a04e9cef7c052717fcb28fb63b3824802488f688391895b618e39be0f684/lupa-2.6-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e8faddd9d198688c8884091173a088a8e920ecc96cda2ffed576a23574c4b3f6", size = 2073458 }, - { url = "https://files.pythonhosted.org/packages/e6/10/824173d10f38b51fc77785228f01411b6ca28826ce27404c7c912e0e442c/lupa-2.6-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:daebb3a6b58095c917e76ba727ab37b27477fb926957c825205fbda431552134", size = 1067683 }, - { url = "https://files.pythonhosted.org/packages/b6/dc/9692fbcf3c924d9c4ece2d8d2f724451ac2e09af0bd2a782db1cef34e799/lupa-2.6-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:f3154e68972befe0f81564e37d8142b5d5d79931a18309226a04ec92487d4ea3", size = 1171892 }, - { url = "https://files.pythonhosted.org/packages/84/ff/e318b628d4643c278c96ab3ddea07fc36b075a57383c837f5b11e537ba9d/lupa-2.6-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:e4dadf77b9fedc0bfa53417cc28dc2278a26d4cbd95c29f8927ad4d8fe0a7ef9", size = 2166641 }, - { url = "https://files.pythonhosted.org/packages/12/f7/a6f9ec2806cf2d50826980cdb4b3cffc7691dc6f95e13cc728846d5cb793/lupa-2.6-cp314-cp314-win32.whl", hash = "sha256:cb34169c6fa3bab3e8ac58ca21b8a7102f6a94b6a5d08d3636312f3f02fafd8f", size = 1456857 }, - { url = "https://files.pythonhosted.org/packages/c5/de/df71896f25bdc18360fdfa3b802cd7d57d7fede41a0e9724a4625b412c85/lupa-2.6-cp314-cp314-win_amd64.whl", hash = "sha256:b74f944fe46c421e25d0f8692aef1e842192f6f7f68034201382ac440ef9ea67", size = 1731191 }, - { url = "https://files.pythonhosted.org/packages/47/3c/a1f23b01c54669465f5f4c4083107d496fbe6fb45998771420e9aadcf145/lupa-2.6-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0e21b716408a21ab65723f8841cf7f2f37a844b7a965eeabb785e27fca4099cf", size = 999343 }, - { url = "https://files.pythonhosted.org/packages/c5/6d/501994291cb640bfa2ccf7f554be4e6914afa21c4026bd01bff9ca8aac57/lupa-2.6-cp314-cp314t-macosx_11_0_universal2.whl", hash = "sha256:589db872a141bfff828340079bbdf3e9a31f2689f4ca0d88f97d9e8c2eae6142", size = 2000730 }, - { url = "https://files.pythonhosted.org/packages/53/a5/457ffb4f3f20469956c2d4c4842a7675e884efc895b2f23d126d23e126cc/lupa-2.6-cp314-cp314t-macosx_11_0_x86_64.whl", hash = "sha256:cd852a91a4a9d4dcbb9a58100f820a75a425703ec3e3f049055f60b8533b7953", size = 1021553 }, - { url = "https://files.pythonhosted.org/packages/51/6b/36bb5a5d0960f2a5c7c700e0819abb76fd9bf9c1d8a66e5106416d6e9b14/lupa-2.6-cp314-cp314t-manylinux2010_i686.manylinux_2_12_i686.manylinux_2_28_i686.whl", hash = "sha256:0334753be028358922415ca97a64a3048e4ed155413fc4eaf87dd0a7e2752983", size = 1133275 }, - { url = "https://files.pythonhosted.org/packages/19/86/202ff4429f663013f37d2229f6176ca9f83678a50257d70f61a0a97281bf/lupa-2.6-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:661d895cd38c87658a34780fac54a690ec036ead743e41b74c3fb81a9e65a6aa", size = 1038441 }, - { url = "https://files.pythonhosted.org/packages/a7/42/d8125f8e420714e5b52e9c08d88b5329dfb02dcca731b4f21faaee6cc5b5/lupa-2.6-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6aa58454ccc13878cc177c62529a2056be734da16369e451987ff92784994ca7", size = 2058324 }, - { url = "https://files.pythonhosted.org/packages/2b/2c/47bf8b84059876e877a339717ddb595a4a7b0e8740bacae78ba527562e1c/lupa-2.6-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:1425017264e470c98022bba8cff5bd46d054a827f5df6b80274f9cc71dafd24f", size = 1060250 }, - { url = "https://files.pythonhosted.org/packages/c2/06/d88add2b6406ca1bdec99d11a429222837ca6d03bea42ca75afa169a78cb/lupa-2.6-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:224af0532d216e3105f0a127410f12320f7c5f1aa0300bdf9646b8d9afb0048c", size = 1151126 }, - { url = "https://files.pythonhosted.org/packages/b4/a0/89e6a024c3b4485b89ef86881c9d55e097e7cb0bdb74efb746f2fa6a9a76/lupa-2.6-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:9abb98d5a8fd27c8285302e82199f0e56e463066f88f619d6594a450bf269d80", size = 2153693 }, - { url = "https://files.pythonhosted.org/packages/b6/36/a0f007dc58fc1bbf51fb85dcc82fcb1f21b8c4261361de7dab0e3d8521ef/lupa-2.6-cp314-cp314t-win32.whl", hash = "sha256:1849efeba7a8f6fb8aa2c13790bee988fd242ae404bd459509640eeea3d1e291", size = 1590104 }, - { url = "https://files.pythonhosted.org/packages/7d/5e/db903ce9cf82c48d6b91bf6d63ae4c8d0d17958939a4e04ba6b9f38b8643/lupa-2.6-cp314-cp314t-win_amd64.whl", hash = "sha256:fc1498d1a4fc028bc521c26d0fad4ca00ed63b952e32fb95949bda76a04bad52", size = 1913818 }, +sdist = { url = "https://files.pythonhosted.org/packages/b8/1c/191c3e6ec6502e3dbe25a53e27f69a5daeac3e56de1f73c0138224171ead/lupa-2.6.tar.gz", hash = "sha256:9a770a6e89576be3447668d7ced312cd6fd41d3c13c2462c9dc2c2ab570e45d9", size = 7240282, upload-time = "2025-10-24T07:20:29.738Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/28/1d/21176b682ca5469001199d8b95fa1737e29957a3d185186e7a8b55345f2e/lupa-2.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:663a6e58a0f60e7d212017d6678639ac8df0119bc13c2145029dcba084391310", size = 947232, upload-time = "2025-10-24T07:18:27.878Z" }, + { url = "https://files.pythonhosted.org/packages/ce/4c/d327befb684660ca13cf79cd1f1d604331808f9f1b6fb6bf57832f8edf80/lupa-2.6-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:d1f5afda5c20b1f3217a80e9bc1b77037f8a6eb11612fd3ada19065303c8f380", size = 1908625, upload-time = "2025-10-24T07:18:29.944Z" }, + { url = "https://files.pythonhosted.org/packages/66/8e/ad22b0a19454dfd08662237a84c792d6d420d36b061f239e084f29d1a4f3/lupa-2.6-cp313-cp313-macosx_11_0_x86_64.whl", hash = "sha256:26f2b3c085fe76e9119e48c1013c1cccdc1f51585d456858290475aa38e7089e", size = 981057, upload-time = "2025-10-24T07:18:31.553Z" }, + { url = "https://files.pythonhosted.org/packages/5c/48/74859073ab276bd0566c719f9ca0108b0cfc1956ca0d68678d117d47d155/lupa-2.6-cp313-cp313-manylinux2010_i686.manylinux_2_12_i686.manylinux_2_28_i686.whl", hash = "sha256:60d2f902c7b96fb8ab98493dcff315e7bb4d0b44dc9dd76eb37de575025d5685", size = 1156227, upload-time = "2025-10-24T07:18:33.981Z" }, + { url = "https://files.pythonhosted.org/packages/09/6c/0e9ded061916877253c2266074060eb71ed99fb21d73c8c114a76725bce2/lupa-2.6-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a02d25dee3a3250967c36590128d9220ae02f2eda166a24279da0b481519cbff", size = 1035752, upload-time = "2025-10-24T07:18:36.32Z" }, + { url = "https://files.pythonhosted.org/packages/dd/ef/f8c32e454ef9f3fe909f6c7d57a39f950996c37a3deb7b391fec7903dab7/lupa-2.6-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6eae1ee16b886b8914ff292dbefbf2f48abfbdee94b33a88d1d5475e02423203", size = 2069009, upload-time = "2025-10-24T07:18:38.072Z" }, + { url = "https://files.pythonhosted.org/packages/53/dc/15b80c226a5225815a890ee1c11f07968e0aba7a852df41e8ae6fe285063/lupa-2.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b0edd5073a4ee74ab36f74fe61450148e6044f3952b8d21248581f3c5d1a58be", size = 1056301, upload-time = "2025-10-24T07:18:40.165Z" }, + { url = "https://files.pythonhosted.org/packages/31/14/2086c1425c985acfb30997a67e90c39457122df41324d3c179d6ee2292c6/lupa-2.6-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:0c53ee9f22a8a17e7d4266ad48e86f43771951797042dd51d1494aaa4f5f3f0a", size = 1170673, upload-time = "2025-10-24T07:18:42.426Z" }, + { url = "https://files.pythonhosted.org/packages/10/e5/b216c054cf86576c0191bf9a9f05de6f7e8e07164897d95eea0078dca9b2/lupa-2.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:de7c0f157a9064a400d828789191a96da7f4ce889969a588b87ec80de9b14772", size = 2162227, upload-time = "2025-10-24T07:18:46.112Z" }, + { url = "https://files.pythonhosted.org/packages/59/2f/33ecb5bedf4f3bc297ceacb7f016ff951331d352f58e7e791589609ea306/lupa-2.6-cp313-cp313-win32.whl", hash = "sha256:ee9523941ae0a87b5b703417720c5d78f72d2f5bc23883a2ea80a949a3ed9e75", size = 1419558, upload-time = "2025-10-24T07:18:48.371Z" }, + { url = "https://files.pythonhosted.org/packages/f9/b4/55e885834c847ea610e111d87b9ed4768f0afdaeebc00cd46810f25029f6/lupa-2.6-cp313-cp313-win_amd64.whl", hash = "sha256:b1335a5835b0a25ebdbc75cf0bda195e54d133e4d994877ef025e218c2e59db9", size = 1683424, upload-time = "2025-10-24T07:18:50.976Z" }, + { url = "https://files.pythonhosted.org/packages/66/9d/d9427394e54d22a35d1139ef12e845fd700d4872a67a34db32516170b746/lupa-2.6-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:dcb6d0a3264873e1653bc188499f48c1fb4b41a779e315eba45256cfe7bc33c1", size = 953818, upload-time = "2025-10-24T07:18:53.378Z" }, + { url = "https://files.pythonhosted.org/packages/10/41/27bbe81953fb2f9ecfced5d9c99f85b37964cfaf6aa8453bb11283983721/lupa-2.6-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:a37e01f2128f8c36106726cb9d360bac087d58c54b4522b033cc5691c584db18", size = 1915850, upload-time = "2025-10-24T07:18:55.259Z" }, + { url = "https://files.pythonhosted.org/packages/a3/98/f9ff60db84a75ba8725506bbf448fb085bc77868a021998ed2a66d920568/lupa-2.6-cp314-cp314-macosx_11_0_x86_64.whl", hash = "sha256:458bd7e9ff3c150b245b0fcfbb9bd2593d1152ea7f0a7b91c1d185846da033fe", size = 982344, upload-time = "2025-10-24T07:18:57.05Z" }, + { url = "https://files.pythonhosted.org/packages/41/f7/f39e0f1c055c3b887d86b404aaf0ca197b5edfd235a8b81b45b25bac7fc3/lupa-2.6-cp314-cp314-manylinux2010_i686.manylinux_2_12_i686.manylinux_2_28_i686.whl", hash = "sha256:052ee82cac5206a02df77119c325339acbc09f5ce66967f66a2e12a0f3211cad", size = 1156543, upload-time = "2025-10-24T07:18:59.251Z" }, + { url = "https://files.pythonhosted.org/packages/9e/9c/59e6cffa0d672d662ae17bd7ac8ecd2c89c9449dee499e3eb13ca9cd10d9/lupa-2.6-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96594eca3c87dd07938009e95e591e43d554c1dbd0385be03c100367141db5a8", size = 1047974, upload-time = "2025-10-24T07:19:01.449Z" }, + { url = "https://files.pythonhosted.org/packages/23/c6/a04e9cef7c052717fcb28fb63b3824802488f688391895b618e39be0f684/lupa-2.6-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e8faddd9d198688c8884091173a088a8e920ecc96cda2ffed576a23574c4b3f6", size = 2073458, upload-time = "2025-10-24T07:19:03.369Z" }, + { url = "https://files.pythonhosted.org/packages/e6/10/824173d10f38b51fc77785228f01411b6ca28826ce27404c7c912e0e442c/lupa-2.6-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:daebb3a6b58095c917e76ba727ab37b27477fb926957c825205fbda431552134", size = 1067683, upload-time = "2025-10-24T07:19:06.2Z" }, + { url = "https://files.pythonhosted.org/packages/b6/dc/9692fbcf3c924d9c4ece2d8d2f724451ac2e09af0bd2a782db1cef34e799/lupa-2.6-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:f3154e68972befe0f81564e37d8142b5d5d79931a18309226a04ec92487d4ea3", size = 1171892, upload-time = "2025-10-24T07:19:08.544Z" }, + { url = "https://files.pythonhosted.org/packages/84/ff/e318b628d4643c278c96ab3ddea07fc36b075a57383c837f5b11e537ba9d/lupa-2.6-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:e4dadf77b9fedc0bfa53417cc28dc2278a26d4cbd95c29f8927ad4d8fe0a7ef9", size = 2166641, upload-time = "2025-10-24T07:19:10.485Z" }, + { url = "https://files.pythonhosted.org/packages/12/f7/a6f9ec2806cf2d50826980cdb4b3cffc7691dc6f95e13cc728846d5cb793/lupa-2.6-cp314-cp314-win32.whl", hash = "sha256:cb34169c6fa3bab3e8ac58ca21b8a7102f6a94b6a5d08d3636312f3f02fafd8f", size = 1456857, upload-time = "2025-10-24T07:19:37.989Z" }, + { url = "https://files.pythonhosted.org/packages/c5/de/df71896f25bdc18360fdfa3b802cd7d57d7fede41a0e9724a4625b412c85/lupa-2.6-cp314-cp314-win_amd64.whl", hash = "sha256:b74f944fe46c421e25d0f8692aef1e842192f6f7f68034201382ac440ef9ea67", size = 1731191, upload-time = "2025-10-24T07:19:40.281Z" }, + { url = "https://files.pythonhosted.org/packages/47/3c/a1f23b01c54669465f5f4c4083107d496fbe6fb45998771420e9aadcf145/lupa-2.6-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0e21b716408a21ab65723f8841cf7f2f37a844b7a965eeabb785e27fca4099cf", size = 999343, upload-time = "2025-10-24T07:19:12.519Z" }, + { url = "https://files.pythonhosted.org/packages/c5/6d/501994291cb640bfa2ccf7f554be4e6914afa21c4026bd01bff9ca8aac57/lupa-2.6-cp314-cp314t-macosx_11_0_universal2.whl", hash = "sha256:589db872a141bfff828340079bbdf3e9a31f2689f4ca0d88f97d9e8c2eae6142", size = 2000730, upload-time = "2025-10-24T07:19:14.869Z" }, + { url = "https://files.pythonhosted.org/packages/53/a5/457ffb4f3f20469956c2d4c4842a7675e884efc895b2f23d126d23e126cc/lupa-2.6-cp314-cp314t-macosx_11_0_x86_64.whl", hash = "sha256:cd852a91a4a9d4dcbb9a58100f820a75a425703ec3e3f049055f60b8533b7953", size = 1021553, upload-time = "2025-10-24T07:19:17.123Z" }, + { url = "https://files.pythonhosted.org/packages/51/6b/36bb5a5d0960f2a5c7c700e0819abb76fd9bf9c1d8a66e5106416d6e9b14/lupa-2.6-cp314-cp314t-manylinux2010_i686.manylinux_2_12_i686.manylinux_2_28_i686.whl", hash = "sha256:0334753be028358922415ca97a64a3048e4ed155413fc4eaf87dd0a7e2752983", size = 1133275, upload-time = "2025-10-24T07:19:20.51Z" }, + { url = "https://files.pythonhosted.org/packages/19/86/202ff4429f663013f37d2229f6176ca9f83678a50257d70f61a0a97281bf/lupa-2.6-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:661d895cd38c87658a34780fac54a690ec036ead743e41b74c3fb81a9e65a6aa", size = 1038441, upload-time = "2025-10-24T07:19:22.509Z" }, + { url = "https://files.pythonhosted.org/packages/a7/42/d8125f8e420714e5b52e9c08d88b5329dfb02dcca731b4f21faaee6cc5b5/lupa-2.6-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6aa58454ccc13878cc177c62529a2056be734da16369e451987ff92784994ca7", size = 2058324, upload-time = "2025-10-24T07:19:24.979Z" }, + { url = "https://files.pythonhosted.org/packages/2b/2c/47bf8b84059876e877a339717ddb595a4a7b0e8740bacae78ba527562e1c/lupa-2.6-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:1425017264e470c98022bba8cff5bd46d054a827f5df6b80274f9cc71dafd24f", size = 1060250, upload-time = "2025-10-24T07:19:27.262Z" }, + { url = "https://files.pythonhosted.org/packages/c2/06/d88add2b6406ca1bdec99d11a429222837ca6d03bea42ca75afa169a78cb/lupa-2.6-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:224af0532d216e3105f0a127410f12320f7c5f1aa0300bdf9646b8d9afb0048c", size = 1151126, upload-time = "2025-10-24T07:19:29.522Z" }, + { url = "https://files.pythonhosted.org/packages/b4/a0/89e6a024c3b4485b89ef86881c9d55e097e7cb0bdb74efb746f2fa6a9a76/lupa-2.6-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:9abb98d5a8fd27c8285302e82199f0e56e463066f88f619d6594a450bf269d80", size = 2153693, upload-time = "2025-10-24T07:19:31.379Z" }, + { url = "https://files.pythonhosted.org/packages/b6/36/a0f007dc58fc1bbf51fb85dcc82fcb1f21b8c4261361de7dab0e3d8521ef/lupa-2.6-cp314-cp314t-win32.whl", hash = "sha256:1849efeba7a8f6fb8aa2c13790bee988fd242ae404bd459509640eeea3d1e291", size = 1590104, upload-time = "2025-10-24T07:19:33.514Z" }, + { url = "https://files.pythonhosted.org/packages/7d/5e/db903ce9cf82c48d6b91bf6d63ae4c8d0d17958939a4e04ba6b9f38b8643/lupa-2.6-cp314-cp314t-win_amd64.whl", hash = "sha256:fc1498d1a4fc028bc521c26d0fad4ca00ed63b952e32fb95949bda76a04bad52", size = 1913818, upload-time = "2025-10-24T07:19:36.039Z" }, ] [[package]] @@ -1366,9 +1347,7 @@ name = "mainloop-backend" version = "0.1.0" source = { editable = "." } dependencies = [ - { name = "anthropic" }, { name = "asyncpg" }, - { name = "claude-agent-sdk" }, { name = "dbos" }, { name = "fastapi" }, { name = "githubkit" }, @@ -1384,9 +1363,7 @@ dependencies = [ [package.metadata] requires-dist = [ - { name = "anthropic", specifier = ">=0.75.0" }, { name = "asyncpg", specifier = ">=0.30.0" }, - { name = "claude-agent-sdk", specifier = ">=0.1.18" }, { name = "dbos", specifier = ">=2.7.0" }, { name = "fastapi", specifier = ">=0.104.0" }, { name = "githubkit", specifier = ">=0.11.0" }, @@ -1407,9 +1384,9 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "mdurl" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/5b/f5/4ec618ed16cc4f8fb3b701563655a69816155e79e24a17b651541804721d/markdown_it_py-4.0.0.tar.gz", hash = "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3", size = 73070 } +sdist = { url = "https://files.pythonhosted.org/packages/5b/f5/4ec618ed16cc4f8fb3b701563655a69816155e79e24a17b651541804721d/markdown_it_py-4.0.0.tar.gz", hash = "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3", size = 73070, upload-time = "2025-08-11T12:57:52.854Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147", size = 87321 }, + { url = "https://files.pythonhosted.org/packages/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147", size = 87321, upload-time = "2025-08-11T12:57:51.923Z" }, ] [[package]] @@ -1432,18 +1409,18 @@ dependencies = [ { name = "typing-inspection" }, { name = "uvicorn", marker = "sys_platform != 'emscripten'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/d5/2d/649d80a0ecf6a1f82632ca44bec21c0461a9d9fc8934d38cb5b319f2db5e/mcp-1.25.0.tar.gz", hash = "sha256:56310361ebf0364e2d438e5b45f7668cbb124e158bb358333cd06e49e83a6802", size = 605387 } +sdist = { url = "https://files.pythonhosted.org/packages/d5/2d/649d80a0ecf6a1f82632ca44bec21c0461a9d9fc8934d38cb5b319f2db5e/mcp-1.25.0.tar.gz", hash = "sha256:56310361ebf0364e2d438e5b45f7668cbb124e158bb358333cd06e49e83a6802", size = 605387, upload-time = "2025-12-19T10:19:56.985Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e2/fc/6dc7659c2ae5ddf280477011f4213a74f806862856b796ef08f028e664bf/mcp-1.25.0-py3-none-any.whl", hash = "sha256:b37c38144a666add0862614cc79ec276e97d72aa8ca26d622818d4e278b9721a", size = 233076 }, + { url = "https://files.pythonhosted.org/packages/e2/fc/6dc7659c2ae5ddf280477011f4213a74f806862856b796ef08f028e664bf/mcp-1.25.0-py3-none-any.whl", hash = "sha256:b37c38144a666add0862614cc79ec276e97d72aa8ca26d622818d4e278b9721a", size = 233076, upload-time = "2025-12-19T10:19:55.416Z" }, ] [[package]] name = "mdurl" version = "0.1.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729 } +sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979 }, + { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, ] [[package]] @@ -1459,9 +1436,9 @@ dependencies = [ { name = "pyyaml" }, { name = "typing-inspection" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/5a/8d/d8b7af67a966b6f227024e1cb7287fc19901a434f87a5a391dcfe635d338/mistralai-1.9.11.tar.gz", hash = "sha256:3df9e403c31a756ec79e78df25ee73cea3eb15f86693773e16b16adaf59c9b8a", size = 208051 } +sdist = { url = "https://files.pythonhosted.org/packages/5a/8d/d8b7af67a966b6f227024e1cb7287fc19901a434f87a5a391dcfe635d338/mistralai-1.9.11.tar.gz", hash = "sha256:3df9e403c31a756ec79e78df25ee73cea3eb15f86693773e16b16adaf59c9b8a", size = 208051, upload-time = "2025-10-02T15:53:40.473Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/fe/76/4ce12563aea5a76016f8643eff30ab731e6656c845e9e4d090ef10c7b925/mistralai-1.9.11-py3-none-any.whl", hash = "sha256:7a3dc2b8ef3fceaa3582220234261b5c4e3e03a972563b07afa150e44a25a6d3", size = 442796 }, + { url = "https://files.pythonhosted.org/packages/fe/76/4ce12563aea5a76016f8643eff30ab731e6656c845e9e4d090ef10c7b925/mistralai-1.9.11-py3-none-any.whl", hash = "sha256:7a3dc2b8ef3fceaa3582220234261b5c4e3e03a972563b07afa150e44a25a6d3", size = 442796, upload-time = "2025-10-02T15:53:39.134Z" }, ] [[package]] @@ -1479,125 +1456,125 @@ requires-dist = [{ name = "pydantic", specifier = ">=2.12.5" }] name = "more-itertools" version = "10.8.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ea/5d/38b681d3fce7a266dd9ab73c66959406d565b3e85f21d5e66e1181d93721/more_itertools-10.8.0.tar.gz", hash = "sha256:f638ddf8a1a0d134181275fb5d58b086ead7c6a72429ad725c67503f13ba30bd", size = 137431 } +sdist = { url = "https://files.pythonhosted.org/packages/ea/5d/38b681d3fce7a266dd9ab73c66959406d565b3e85f21d5e66e1181d93721/more_itertools-10.8.0.tar.gz", hash = "sha256:f638ddf8a1a0d134181275fb5d58b086ead7c6a72429ad725c67503f13ba30bd", size = 137431, upload-time = "2025-09-02T15:23:11.018Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a4/8e/469e5a4a2f5855992e425f3cb33804cc07bf18d48f2db061aec61ce50270/more_itertools-10.8.0-py3-none-any.whl", hash = "sha256:52d4362373dcf7c52546bc4af9a86ee7c4579df9a8dc268be0a2f949d376cc9b", size = 69667 }, + { url = "https://files.pythonhosted.org/packages/a4/8e/469e5a4a2f5855992e425f3cb33804cc07bf18d48f2db061aec61ce50270/more_itertools-10.8.0-py3-none-any.whl", hash = "sha256:52d4362373dcf7c52546bc4af9a86ee7c4579df9a8dc268be0a2f949d376cc9b", size = 69667, upload-time = "2025-09-02T15:23:09.635Z" }, ] [[package]] name = "msgpack" version = "1.1.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/4d/f2/bfb55a6236ed8725a96b0aa3acbd0ec17588e6a2c3b62a93eb513ed8783f/msgpack-1.1.2.tar.gz", hash = "sha256:3b60763c1373dd60f398488069bcdc703cd08a711477b5d480eecc9f9626f47e", size = 173581 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/6b/31/b46518ecc604d7edf3a4f94cb3bf021fc62aa301f0cb849936968164ef23/msgpack-1.1.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4efd7b5979ccb539c221a4c4e16aac1a533efc97f3b759bb5a5ac9f6d10383bf", size = 81212 }, - { url = "https://files.pythonhosted.org/packages/92/dc/c385f38f2c2433333345a82926c6bfa5ecfff3ef787201614317b58dd8be/msgpack-1.1.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:42eefe2c3e2af97ed470eec850facbe1b5ad1d6eacdbadc42ec98e7dcf68b4b7", size = 84315 }, - { url = "https://files.pythonhosted.org/packages/d3/68/93180dce57f684a61a88a45ed13047558ded2be46f03acb8dec6d7c513af/msgpack-1.1.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1fdf7d83102bf09e7ce3357de96c59b627395352a4024f6e2458501f158bf999", size = 412721 }, - { url = "https://files.pythonhosted.org/packages/5d/ba/459f18c16f2b3fc1a1ca871f72f07d70c07bf768ad0a507a698b8052ac58/msgpack-1.1.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fac4be746328f90caa3cd4bc67e6fe36ca2bf61d5c6eb6d895b6527e3f05071e", size = 424657 }, - { url = "https://files.pythonhosted.org/packages/38/f8/4398c46863b093252fe67368b44edc6c13b17f4e6b0e4929dbf0bdb13f23/msgpack-1.1.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:fffee09044073e69f2bad787071aeec727183e7580443dfeb8556cbf1978d162", size = 402668 }, - { url = "https://files.pythonhosted.org/packages/28/ce/698c1eff75626e4124b4d78e21cca0b4cc90043afb80a507626ea354ab52/msgpack-1.1.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5928604de9b032bc17f5099496417f113c45bc6bc21b5c6920caf34b3c428794", size = 419040 }, - { url = "https://files.pythonhosted.org/packages/67/32/f3cd1667028424fa7001d82e10ee35386eea1408b93d399b09fb0aa7875f/msgpack-1.1.2-cp313-cp313-win32.whl", hash = "sha256:a7787d353595c7c7e145e2331abf8b7ff1e6673a6b974ded96e6d4ec09f00c8c", size = 65037 }, - { url = "https://files.pythonhosted.org/packages/74/07/1ed8277f8653c40ebc65985180b007879f6a836c525b3885dcc6448ae6cb/msgpack-1.1.2-cp313-cp313-win_amd64.whl", hash = "sha256:a465f0dceb8e13a487e54c07d04ae3ba131c7c5b95e2612596eafde1dccf64a9", size = 72631 }, - { url = "https://files.pythonhosted.org/packages/e5/db/0314e4e2db56ebcf450f277904ffd84a7988b9e5da8d0d61ab2d057df2b6/msgpack-1.1.2-cp313-cp313-win_arm64.whl", hash = "sha256:e69b39f8c0aa5ec24b57737ebee40be647035158f14ed4b40e6f150077e21a84", size = 64118 }, - { url = "https://files.pythonhosted.org/packages/22/71/201105712d0a2ff07b7873ed3c220292fb2ea5120603c00c4b634bcdafb3/msgpack-1.1.2-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:e23ce8d5f7aa6ea6d2a2b326b4ba46c985dbb204523759984430db7114f8aa00", size = 81127 }, - { url = "https://files.pythonhosted.org/packages/1b/9f/38ff9e57a2eade7bf9dfee5eae17f39fc0e998658050279cbb14d97d36d9/msgpack-1.1.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:6c15b7d74c939ebe620dd8e559384be806204d73b4f9356320632d783d1f7939", size = 84981 }, - { url = "https://files.pythonhosted.org/packages/8e/a9/3536e385167b88c2cc8f4424c49e28d49a6fc35206d4a8060f136e71f94c/msgpack-1.1.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:99e2cb7b9031568a2a5c73aa077180f93dd2e95b4f8d3b8e14a73ae94a9e667e", size = 411885 }, - { url = "https://files.pythonhosted.org/packages/2f/40/dc34d1a8d5f1e51fc64640b62b191684da52ca469da9cd74e84936ffa4a6/msgpack-1.1.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:180759d89a057eab503cf62eeec0aa61c4ea1200dee709f3a8e9397dbb3b6931", size = 419658 }, - { url = "https://files.pythonhosted.org/packages/3b/ef/2b92e286366500a09a67e03496ee8b8ba00562797a52f3c117aa2b29514b/msgpack-1.1.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:04fb995247a6e83830b62f0b07bf36540c213f6eac8e851166d8d86d83cbd014", size = 403290 }, - { url = "https://files.pythonhosted.org/packages/78/90/e0ea7990abea5764e4655b8177aa7c63cdfa89945b6e7641055800f6c16b/msgpack-1.1.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:8e22ab046fa7ede9e36eeb4cfad44d46450f37bb05d5ec482b02868f451c95e2", size = 415234 }, - { url = "https://files.pythonhosted.org/packages/72/4e/9390aed5db983a2310818cd7d3ec0aecad45e1f7007e0cda79c79507bb0d/msgpack-1.1.2-cp314-cp314-win32.whl", hash = "sha256:80a0ff7d4abf5fecb995fcf235d4064b9a9a8a40a3ab80999e6ac1e30b702717", size = 66391 }, - { url = "https://files.pythonhosted.org/packages/6e/f1/abd09c2ae91228c5f3998dbd7f41353def9eac64253de3c8105efa2082f7/msgpack-1.1.2-cp314-cp314-win_amd64.whl", hash = "sha256:9ade919fac6a3e7260b7f64cea89df6bec59104987cbea34d34a2fa15d74310b", size = 73787 }, - { url = "https://files.pythonhosted.org/packages/6a/b0/9d9f667ab48b16ad4115c1935d94023b82b3198064cb84a123e97f7466c1/msgpack-1.1.2-cp314-cp314-win_arm64.whl", hash = "sha256:59415c6076b1e30e563eb732e23b994a61c159cec44deaf584e5cc1dd662f2af", size = 66453 }, - { url = "https://files.pythonhosted.org/packages/16/67/93f80545eb1792b61a217fa7f06d5e5cb9e0055bed867f43e2b8e012e137/msgpack-1.1.2-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:897c478140877e5307760b0ea66e0932738879e7aa68144d9b78ea4c8302a84a", size = 85264 }, - { url = "https://files.pythonhosted.org/packages/87/1c/33c8a24959cf193966ef11a6f6a2995a65eb066bd681fd085afd519a57ce/msgpack-1.1.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a668204fa43e6d02f89dbe79a30b0d67238d9ec4c5bd8a940fc3a004a47b721b", size = 89076 }, - { url = "https://files.pythonhosted.org/packages/fc/6b/62e85ff7193663fbea5c0254ef32f0c77134b4059f8da89b958beb7696f3/msgpack-1.1.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5559d03930d3aa0f3aacb4c42c776af1a2ace2611871c84a75afe436695e6245", size = 435242 }, - { url = "https://files.pythonhosted.org/packages/c1/47/5c74ecb4cc277cf09f64e913947871682ffa82b3b93c8dad68083112f412/msgpack-1.1.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:70c5a7a9fea7f036b716191c29047374c10721c389c21e9ffafad04df8c52c90", size = 432509 }, - { url = "https://files.pythonhosted.org/packages/24/a4/e98ccdb56dc4e98c929a3f150de1799831c0a800583cde9fa022fa90602d/msgpack-1.1.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:f2cb069d8b981abc72b41aea1c580ce92d57c673ec61af4c500153a626cb9e20", size = 415957 }, - { url = "https://files.pythonhosted.org/packages/da/28/6951f7fb67bc0a4e184a6b38ab71a92d9ba58080b27a77d3e2fb0be5998f/msgpack-1.1.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:d62ce1f483f355f61adb5433ebfd8868c5f078d1a52d042b0a998682b4fa8c27", size = 422910 }, - { url = "https://files.pythonhosted.org/packages/f0/03/42106dcded51f0a0b5284d3ce30a671e7bd3f7318d122b2ead66ad289fed/msgpack-1.1.2-cp314-cp314t-win32.whl", hash = "sha256:1d1418482b1ee984625d88aa9585db570180c286d942da463533b238b98b812b", size = 75197 }, - { url = "https://files.pythonhosted.org/packages/15/86/d0071e94987f8db59d4eeb386ddc64d0bb9b10820a8d82bcd3e53eeb2da6/msgpack-1.1.2-cp314-cp314t-win_amd64.whl", hash = "sha256:5a46bf7e831d09470ad92dff02b8b1ac92175ca36b087f904a0519857c6be3ff", size = 85772 }, - { url = "https://files.pythonhosted.org/packages/81/f2/08ace4142eb281c12701fc3b93a10795e4d4dc7f753911d836675050f886/msgpack-1.1.2-cp314-cp314t-win_arm64.whl", hash = "sha256:d99ef64f349d5ec3293688e91486c5fdb925ed03807f64d98d205d2713c60b46", size = 70868 }, +sdist = { url = "https://files.pythonhosted.org/packages/4d/f2/bfb55a6236ed8725a96b0aa3acbd0ec17588e6a2c3b62a93eb513ed8783f/msgpack-1.1.2.tar.gz", hash = "sha256:3b60763c1373dd60f398488069bcdc703cd08a711477b5d480eecc9f9626f47e", size = 173581, upload-time = "2025-10-08T09:15:56.596Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6b/31/b46518ecc604d7edf3a4f94cb3bf021fc62aa301f0cb849936968164ef23/msgpack-1.1.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4efd7b5979ccb539c221a4c4e16aac1a533efc97f3b759bb5a5ac9f6d10383bf", size = 81212, upload-time = "2025-10-08T09:15:14.552Z" }, + { url = "https://files.pythonhosted.org/packages/92/dc/c385f38f2c2433333345a82926c6bfa5ecfff3ef787201614317b58dd8be/msgpack-1.1.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:42eefe2c3e2af97ed470eec850facbe1b5ad1d6eacdbadc42ec98e7dcf68b4b7", size = 84315, upload-time = "2025-10-08T09:15:15.543Z" }, + { url = "https://files.pythonhosted.org/packages/d3/68/93180dce57f684a61a88a45ed13047558ded2be46f03acb8dec6d7c513af/msgpack-1.1.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1fdf7d83102bf09e7ce3357de96c59b627395352a4024f6e2458501f158bf999", size = 412721, upload-time = "2025-10-08T09:15:16.567Z" }, + { url = "https://files.pythonhosted.org/packages/5d/ba/459f18c16f2b3fc1a1ca871f72f07d70c07bf768ad0a507a698b8052ac58/msgpack-1.1.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fac4be746328f90caa3cd4bc67e6fe36ca2bf61d5c6eb6d895b6527e3f05071e", size = 424657, upload-time = "2025-10-08T09:15:17.825Z" }, + { url = "https://files.pythonhosted.org/packages/38/f8/4398c46863b093252fe67368b44edc6c13b17f4e6b0e4929dbf0bdb13f23/msgpack-1.1.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:fffee09044073e69f2bad787071aeec727183e7580443dfeb8556cbf1978d162", size = 402668, upload-time = "2025-10-08T09:15:19.003Z" }, + { url = "https://files.pythonhosted.org/packages/28/ce/698c1eff75626e4124b4d78e21cca0b4cc90043afb80a507626ea354ab52/msgpack-1.1.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5928604de9b032bc17f5099496417f113c45bc6bc21b5c6920caf34b3c428794", size = 419040, upload-time = "2025-10-08T09:15:20.183Z" }, + { url = "https://files.pythonhosted.org/packages/67/32/f3cd1667028424fa7001d82e10ee35386eea1408b93d399b09fb0aa7875f/msgpack-1.1.2-cp313-cp313-win32.whl", hash = "sha256:a7787d353595c7c7e145e2331abf8b7ff1e6673a6b974ded96e6d4ec09f00c8c", size = 65037, upload-time = "2025-10-08T09:15:21.416Z" }, + { url = "https://files.pythonhosted.org/packages/74/07/1ed8277f8653c40ebc65985180b007879f6a836c525b3885dcc6448ae6cb/msgpack-1.1.2-cp313-cp313-win_amd64.whl", hash = "sha256:a465f0dceb8e13a487e54c07d04ae3ba131c7c5b95e2612596eafde1dccf64a9", size = 72631, upload-time = "2025-10-08T09:15:22.431Z" }, + { url = "https://files.pythonhosted.org/packages/e5/db/0314e4e2db56ebcf450f277904ffd84a7988b9e5da8d0d61ab2d057df2b6/msgpack-1.1.2-cp313-cp313-win_arm64.whl", hash = "sha256:e69b39f8c0aa5ec24b57737ebee40be647035158f14ed4b40e6f150077e21a84", size = 64118, upload-time = "2025-10-08T09:15:23.402Z" }, + { url = "https://files.pythonhosted.org/packages/22/71/201105712d0a2ff07b7873ed3c220292fb2ea5120603c00c4b634bcdafb3/msgpack-1.1.2-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:e23ce8d5f7aa6ea6d2a2b326b4ba46c985dbb204523759984430db7114f8aa00", size = 81127, upload-time = "2025-10-08T09:15:24.408Z" }, + { url = "https://files.pythonhosted.org/packages/1b/9f/38ff9e57a2eade7bf9dfee5eae17f39fc0e998658050279cbb14d97d36d9/msgpack-1.1.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:6c15b7d74c939ebe620dd8e559384be806204d73b4f9356320632d783d1f7939", size = 84981, upload-time = "2025-10-08T09:15:25.812Z" }, + { url = "https://files.pythonhosted.org/packages/8e/a9/3536e385167b88c2cc8f4424c49e28d49a6fc35206d4a8060f136e71f94c/msgpack-1.1.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:99e2cb7b9031568a2a5c73aa077180f93dd2e95b4f8d3b8e14a73ae94a9e667e", size = 411885, upload-time = "2025-10-08T09:15:27.22Z" }, + { url = "https://files.pythonhosted.org/packages/2f/40/dc34d1a8d5f1e51fc64640b62b191684da52ca469da9cd74e84936ffa4a6/msgpack-1.1.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:180759d89a057eab503cf62eeec0aa61c4ea1200dee709f3a8e9397dbb3b6931", size = 419658, upload-time = "2025-10-08T09:15:28.4Z" }, + { url = "https://files.pythonhosted.org/packages/3b/ef/2b92e286366500a09a67e03496ee8b8ba00562797a52f3c117aa2b29514b/msgpack-1.1.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:04fb995247a6e83830b62f0b07bf36540c213f6eac8e851166d8d86d83cbd014", size = 403290, upload-time = "2025-10-08T09:15:29.764Z" }, + { url = "https://files.pythonhosted.org/packages/78/90/e0ea7990abea5764e4655b8177aa7c63cdfa89945b6e7641055800f6c16b/msgpack-1.1.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:8e22ab046fa7ede9e36eeb4cfad44d46450f37bb05d5ec482b02868f451c95e2", size = 415234, upload-time = "2025-10-08T09:15:31.022Z" }, + { url = "https://files.pythonhosted.org/packages/72/4e/9390aed5db983a2310818cd7d3ec0aecad45e1f7007e0cda79c79507bb0d/msgpack-1.1.2-cp314-cp314-win32.whl", hash = "sha256:80a0ff7d4abf5fecb995fcf235d4064b9a9a8a40a3ab80999e6ac1e30b702717", size = 66391, upload-time = "2025-10-08T09:15:32.265Z" }, + { url = "https://files.pythonhosted.org/packages/6e/f1/abd09c2ae91228c5f3998dbd7f41353def9eac64253de3c8105efa2082f7/msgpack-1.1.2-cp314-cp314-win_amd64.whl", hash = "sha256:9ade919fac6a3e7260b7f64cea89df6bec59104987cbea34d34a2fa15d74310b", size = 73787, upload-time = "2025-10-08T09:15:33.219Z" }, + { url = "https://files.pythonhosted.org/packages/6a/b0/9d9f667ab48b16ad4115c1935d94023b82b3198064cb84a123e97f7466c1/msgpack-1.1.2-cp314-cp314-win_arm64.whl", hash = "sha256:59415c6076b1e30e563eb732e23b994a61c159cec44deaf584e5cc1dd662f2af", size = 66453, upload-time = "2025-10-08T09:15:34.225Z" }, + { url = "https://files.pythonhosted.org/packages/16/67/93f80545eb1792b61a217fa7f06d5e5cb9e0055bed867f43e2b8e012e137/msgpack-1.1.2-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:897c478140877e5307760b0ea66e0932738879e7aa68144d9b78ea4c8302a84a", size = 85264, upload-time = "2025-10-08T09:15:35.61Z" }, + { url = "https://files.pythonhosted.org/packages/87/1c/33c8a24959cf193966ef11a6f6a2995a65eb066bd681fd085afd519a57ce/msgpack-1.1.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a668204fa43e6d02f89dbe79a30b0d67238d9ec4c5bd8a940fc3a004a47b721b", size = 89076, upload-time = "2025-10-08T09:15:36.619Z" }, + { url = "https://files.pythonhosted.org/packages/fc/6b/62e85ff7193663fbea5c0254ef32f0c77134b4059f8da89b958beb7696f3/msgpack-1.1.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5559d03930d3aa0f3aacb4c42c776af1a2ace2611871c84a75afe436695e6245", size = 435242, upload-time = "2025-10-08T09:15:37.647Z" }, + { url = "https://files.pythonhosted.org/packages/c1/47/5c74ecb4cc277cf09f64e913947871682ffa82b3b93c8dad68083112f412/msgpack-1.1.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:70c5a7a9fea7f036b716191c29047374c10721c389c21e9ffafad04df8c52c90", size = 432509, upload-time = "2025-10-08T09:15:38.794Z" }, + { url = "https://files.pythonhosted.org/packages/24/a4/e98ccdb56dc4e98c929a3f150de1799831c0a800583cde9fa022fa90602d/msgpack-1.1.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:f2cb069d8b981abc72b41aea1c580ce92d57c673ec61af4c500153a626cb9e20", size = 415957, upload-time = "2025-10-08T09:15:40.238Z" }, + { url = "https://files.pythonhosted.org/packages/da/28/6951f7fb67bc0a4e184a6b38ab71a92d9ba58080b27a77d3e2fb0be5998f/msgpack-1.1.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:d62ce1f483f355f61adb5433ebfd8868c5f078d1a52d042b0a998682b4fa8c27", size = 422910, upload-time = "2025-10-08T09:15:41.505Z" }, + { url = "https://files.pythonhosted.org/packages/f0/03/42106dcded51f0a0b5284d3ce30a671e7bd3f7318d122b2ead66ad289fed/msgpack-1.1.2-cp314-cp314t-win32.whl", hash = "sha256:1d1418482b1ee984625d88aa9585db570180c286d942da463533b238b98b812b", size = 75197, upload-time = "2025-10-08T09:15:42.954Z" }, + { url = "https://files.pythonhosted.org/packages/15/86/d0071e94987f8db59d4eeb386ddc64d0bb9b10820a8d82bcd3e53eeb2da6/msgpack-1.1.2-cp314-cp314t-win_amd64.whl", hash = "sha256:5a46bf7e831d09470ad92dff02b8b1ac92175ca36b087f904a0519857c6be3ff", size = 85772, upload-time = "2025-10-08T09:15:43.954Z" }, + { url = "https://files.pythonhosted.org/packages/81/f2/08ace4142eb281c12701fc3b93a10795e4d4dc7f753911d836675050f886/msgpack-1.1.2-cp314-cp314t-win_arm64.whl", hash = "sha256:d99ef64f349d5ec3293688e91486c5fdb925ed03807f64d98d205d2713c60b46", size = 70868, upload-time = "2025-10-08T09:15:44.959Z" }, ] [[package]] name = "multidict" version = "6.7.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/80/1e/5492c365f222f907de1039b91f922b93fa4f764c713ee858d235495d8f50/multidict-6.7.0.tar.gz", hash = "sha256:c6e99d9a65ca282e578dfea819cfa9c0a62b2499d8677392e09feaf305e9e6f5", size = 101834 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d2/86/33272a544eeb36d66e4d9a920602d1a2f57d4ebea4ef3cdfe5a912574c95/multidict-6.7.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:bee7c0588aa0076ce77c0ea5d19a68d76ad81fcd9fe8501003b9a24f9d4000f6", size = 76135 }, - { url = "https://files.pythonhosted.org/packages/91/1c/eb97db117a1ebe46d457a3d235a7b9d2e6dcab174f42d1b67663dd9e5371/multidict-6.7.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7ef6b61cad77091056ce0e7ce69814ef72afacb150b7ac6a3e9470def2198159", size = 45117 }, - { url = "https://files.pythonhosted.org/packages/f1/d8/6c3442322e41fb1dd4de8bd67bfd11cd72352ac131f6368315617de752f1/multidict-6.7.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9c0359b1ec12b1d6849c59f9d319610b7f20ef990a6d454ab151aa0e3b9f78ca", size = 43472 }, - { url = "https://files.pythonhosted.org/packages/75/3f/e2639e80325af0b6c6febdf8e57cc07043ff15f57fa1ef808f4ccb5ac4cd/multidict-6.7.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:cd240939f71c64bd658f186330603aac1a9a81bf6273f523fca63673cb7378a8", size = 249342 }, - { url = "https://files.pythonhosted.org/packages/5d/cc/84e0585f805cbeaa9cbdaa95f9a3d6aed745b9d25700623ac89a6ecff400/multidict-6.7.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a60a4d75718a5efa473ebd5ab685786ba0c67b8381f781d1be14da49f1a2dc60", size = 257082 }, - { url = "https://files.pythonhosted.org/packages/b0/9c/ac851c107c92289acbbf5cfb485694084690c1b17e555f44952c26ddc5bd/multidict-6.7.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:53a42d364f323275126aff81fb67c5ca1b7a04fda0546245730a55c8c5f24bc4", size = 240704 }, - { url = "https://files.pythonhosted.org/packages/50/cc/5f93e99427248c09da95b62d64b25748a5f5c98c7c2ab09825a1d6af0e15/multidict-6.7.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3b29b980d0ddbecb736735ee5bef69bb2ddca56eff603c86f3f29a1128299b4f", size = 266355 }, - { url = "https://files.pythonhosted.org/packages/ec/0c/2ec1d883ceb79c6f7f6d7ad90c919c898f5d1c6ea96d322751420211e072/multidict-6.7.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f8a93b1c0ed2d04b97a5e9336fd2d33371b9a6e29ab7dd6503d63407c20ffbaf", size = 267259 }, - { url = "https://files.pythonhosted.org/packages/c6/2d/f0b184fa88d6630aa267680bdb8623fb69cb0d024b8c6f0d23f9a0f406d3/multidict-6.7.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9ff96e8815eecacc6645da76c413eb3b3d34cfca256c70b16b286a687d013c32", size = 254903 }, - { url = "https://files.pythonhosted.org/packages/06/c9/11ea263ad0df7dfabcad404feb3c0dd40b131bc7f232d5537f2fb1356951/multidict-6.7.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7516c579652f6a6be0e266aec0acd0db80829ca305c3d771ed898538804c2036", size = 252365 }, - { url = "https://files.pythonhosted.org/packages/41/88/d714b86ee2c17d6e09850c70c9d310abac3d808ab49dfa16b43aba9d53fd/multidict-6.7.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:040f393368e63fb0f3330e70c26bfd336656bed925e5cbe17c9da839a6ab13ec", size = 250062 }, - { url = "https://files.pythonhosted.org/packages/15/fe/ad407bb9e818c2b31383f6131ca19ea7e35ce93cf1310fce69f12e89de75/multidict-6.7.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b3bc26a951007b1057a1c543af845f1c7e3e71cc240ed1ace7bf4484aa99196e", size = 249683 }, - { url = "https://files.pythonhosted.org/packages/8c/a4/a89abdb0229e533fb925e7c6e5c40201c2873efebc9abaf14046a4536ee6/multidict-6.7.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:7b022717c748dd1992a83e219587aabe45980d88969f01b316e78683e6285f64", size = 261254 }, - { url = "https://files.pythonhosted.org/packages/8d/aa/0e2b27bd88b40a4fb8dc53dd74eecac70edaa4c1dd0707eb2164da3675b3/multidict-6.7.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:9600082733859f00d79dee64effc7aef1beb26adb297416a4ad2116fd61374bd", size = 257967 }, - { url = "https://files.pythonhosted.org/packages/d0/8e/0c67b7120d5d5f6d874ed85a085f9dc770a7f9d8813e80f44a9fec820bb7/multidict-6.7.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:94218fcec4d72bc61df51c198d098ce2b378e0ccbac41ddbed5ef44092913288", size = 250085 }, - { url = "https://files.pythonhosted.org/packages/ba/55/b73e1d624ea4b8fd4dd07a3bb70f6e4c7c6c5d9d640a41c6ffe5cdbd2a55/multidict-6.7.0-cp313-cp313-win32.whl", hash = "sha256:a37bd74c3fa9d00be2d7b8eca074dc56bd8077ddd2917a839bd989612671ed17", size = 41713 }, - { url = "https://files.pythonhosted.org/packages/32/31/75c59e7d3b4205075b4c183fa4ca398a2daf2303ddf616b04ae6ef55cffe/multidict-6.7.0-cp313-cp313-win_amd64.whl", hash = "sha256:30d193c6cc6d559db42b6bcec8a5d395d34d60c9877a0b71ecd7c204fcf15390", size = 45915 }, - { url = "https://files.pythonhosted.org/packages/31/2a/8987831e811f1184c22bc2e45844934385363ee61c0a2dcfa8f71b87e608/multidict-6.7.0-cp313-cp313-win_arm64.whl", hash = "sha256:ea3334cabe4d41b7ccd01e4d349828678794edbc2d3ae97fc162a3312095092e", size = 43077 }, - { url = "https://files.pythonhosted.org/packages/e8/68/7b3a5170a382a340147337b300b9eb25a9ddb573bcdfff19c0fa3f31ffba/multidict-6.7.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:ad9ce259f50abd98a1ca0aa6e490b58c316a0fce0617f609723e40804add2c00", size = 83114 }, - { url = "https://files.pythonhosted.org/packages/55/5c/3fa2d07c84df4e302060f555bbf539310980362236ad49f50eeb0a1c1eb9/multidict-6.7.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:07f5594ac6d084cbb5de2df218d78baf55ef150b91f0ff8a21cc7a2e3a5a58eb", size = 48442 }, - { url = "https://files.pythonhosted.org/packages/fc/56/67212d33239797f9bd91962bb899d72bb0f4c35a8652dcdb8ed049bef878/multidict-6.7.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:0591b48acf279821a579282444814a2d8d0af624ae0bc600aa4d1b920b6e924b", size = 46885 }, - { url = "https://files.pythonhosted.org/packages/46/d1/908f896224290350721597a61a69cd19b89ad8ee0ae1f38b3f5cd12ea2ac/multidict-6.7.0-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:749a72584761531d2b9467cfbdfd29487ee21124c304c4b6cb760d8777b27f9c", size = 242588 }, - { url = "https://files.pythonhosted.org/packages/ab/67/8604288bbd68680eee0ab568fdcb56171d8b23a01bcd5cb0c8fedf6e5d99/multidict-6.7.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b4c3d199f953acd5b446bf7c0de1fe25d94e09e79086f8dc2f48a11a129cdf1", size = 249966 }, - { url = "https://files.pythonhosted.org/packages/20/33/9228d76339f1ba51e3efef7da3ebd91964d3006217aae13211653193c3ff/multidict-6.7.0-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:9fb0211dfc3b51efea2f349ec92c114d7754dd62c01f81c3e32b765b70c45c9b", size = 228618 }, - { url = "https://files.pythonhosted.org/packages/f8/2d/25d9b566d10cab1c42b3b9e5b11ef79c9111eaf4463b8c257a3bd89e0ead/multidict-6.7.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a027ec240fe73a8d6281872690b988eed307cd7d91b23998ff35ff577ca688b5", size = 257539 }, - { url = "https://files.pythonhosted.org/packages/b6/b1/8d1a965e6637fc33de3c0d8f414485c2b7e4af00f42cab3d84e7b955c222/multidict-6.7.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d1d964afecdf3a8288789df2f5751dc0a8261138c3768d9af117ed384e538fad", size = 256345 }, - { url = "https://files.pythonhosted.org/packages/ba/0c/06b5a8adbdeedada6f4fb8d8f193d44a347223b11939b42953eeb6530b6b/multidict-6.7.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:caf53b15b1b7df9fbd0709aa01409000a2b4dd03a5f6f5cc548183c7c8f8b63c", size = 247934 }, - { url = "https://files.pythonhosted.org/packages/8f/31/b2491b5fe167ca044c6eb4b8f2c9f3b8a00b24c432c365358eadac5d7625/multidict-6.7.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:654030da3197d927f05a536a66186070e98765aa5142794c9904555d3a9d8fb5", size = 245243 }, - { url = "https://files.pythonhosted.org/packages/61/1a/982913957cb90406c8c94f53001abd9eafc271cb3e70ff6371590bec478e/multidict-6.7.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:2090d3718829d1e484706a2f525e50c892237b2bf9b17a79b059cb98cddc2f10", size = 235878 }, - { url = "https://files.pythonhosted.org/packages/be/c0/21435d804c1a1cf7a2608593f4d19bca5bcbd7a81a70b253fdd1c12af9c0/multidict-6.7.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:2d2cfeec3f6f45651b3d408c4acec0ebf3daa9bc8a112a084206f5db5d05b754", size = 243452 }, - { url = "https://files.pythonhosted.org/packages/54/0a/4349d540d4a883863191be6eb9a928846d4ec0ea007d3dcd36323bb058ac/multidict-6.7.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:4ef089f985b8c194d341eb2c24ae6e7408c9a0e2e5658699c92f497437d88c3c", size = 252312 }, - { url = "https://files.pythonhosted.org/packages/26/64/d5416038dbda1488daf16b676e4dbfd9674dde10a0cc8f4fc2b502d8125d/multidict-6.7.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:e93a0617cd16998784bf4414c7e40f17a35d2350e5c6f0bd900d3a8e02bd3762", size = 246935 }, - { url = "https://files.pythonhosted.org/packages/9f/8c/8290c50d14e49f35e0bd4abc25e1bc7711149ca9588ab7d04f886cdf03d9/multidict-6.7.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f0feece2ef8ebc42ed9e2e8c78fc4aa3cf455733b507c09ef7406364c94376c6", size = 243385 }, - { url = "https://files.pythonhosted.org/packages/ef/a0/f83ae75e42d694b3fbad3e047670e511c138be747bc713cf1b10d5096416/multidict-6.7.0-cp313-cp313t-win32.whl", hash = "sha256:19a1d55338ec1be74ef62440ca9e04a2f001a04d0cc49a4983dc320ff0f3212d", size = 47777 }, - { url = "https://files.pythonhosted.org/packages/dc/80/9b174a92814a3830b7357307a792300f42c9e94664b01dee8e457551fa66/multidict-6.7.0-cp313-cp313t-win_amd64.whl", hash = "sha256:3da4fb467498df97e986af166b12d01f05d2e04f978a9c1c680ea1988e0bc4b6", size = 53104 }, - { url = "https://files.pythonhosted.org/packages/cc/28/04baeaf0428d95bb7a7bea0e691ba2f31394338ba424fb0679a9ed0f4c09/multidict-6.7.0-cp313-cp313t-win_arm64.whl", hash = "sha256:b4121773c49a0776461f4a904cdf6264c88e42218aaa8407e803ca8025872792", size = 45503 }, - { url = "https://files.pythonhosted.org/packages/e2/b1/3da6934455dd4b261d4c72f897e3a5728eba81db59959f3a639245891baa/multidict-6.7.0-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3bab1e4aff7adaa34410f93b1f8e57c4b36b9af0426a76003f441ee1d3c7e842", size = 75128 }, - { url = "https://files.pythonhosted.org/packages/14/2c/f069cab5b51d175a1a2cb4ccdf7a2c2dabd58aa5bd933fa036a8d15e2404/multidict-6.7.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:b8512bac933afc3e45fb2b18da8e59b78d4f408399a960339598374d4ae3b56b", size = 44410 }, - { url = "https://files.pythonhosted.org/packages/42/e2/64bb41266427af6642b6b128e8774ed84c11b80a90702c13ac0a86bb10cc/multidict-6.7.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:79dcf9e477bc65414ebfea98ffd013cb39552b5ecd62908752e0e413d6d06e38", size = 43205 }, - { url = "https://files.pythonhosted.org/packages/02/68/6b086fef8a3f1a8541b9236c594f0c9245617c29841f2e0395d979485cde/multidict-6.7.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:31bae522710064b5cbeddaf2e9f32b1abab70ac6ac91d42572502299e9953128", size = 245084 }, - { url = "https://files.pythonhosted.org/packages/15/ee/f524093232007cd7a75c1d132df70f235cfd590a7c9eaccd7ff422ef4ae8/multidict-6.7.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4a0df7ff02397bb63e2fd22af2c87dfa39e8c7f12947bc524dbdc528282c7e34", size = 252667 }, - { url = "https://files.pythonhosted.org/packages/02/a5/eeb3f43ab45878f1895118c3ef157a480db58ede3f248e29b5354139c2c9/multidict-6.7.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:7a0222514e8e4c514660e182d5156a415c13ef0aabbd71682fc714e327b95e99", size = 233590 }, - { url = "https://files.pythonhosted.org/packages/6a/1e/76d02f8270b97269d7e3dbd45644b1785bda457b474315f8cf999525a193/multidict-6.7.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2397ab4daaf2698eb51a76721e98db21ce4f52339e535725de03ea962b5a3202", size = 264112 }, - { url = "https://files.pythonhosted.org/packages/76/0b/c28a70ecb58963847c2a8efe334904cd254812b10e535aefb3bcce513918/multidict-6.7.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8891681594162635948a636c9fe0ff21746aeb3dd5463f6e25d9bea3a8a39ca1", size = 261194 }, - { url = "https://files.pythonhosted.org/packages/b4/63/2ab26e4209773223159b83aa32721b4021ffb08102f8ac7d689c943fded1/multidict-6.7.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:18706cc31dbf402a7945916dd5cddf160251b6dab8a2c5f3d6d5a55949f676b3", size = 248510 }, - { url = "https://files.pythonhosted.org/packages/93/cd/06c1fa8282af1d1c46fd55c10a7930af652afdce43999501d4d68664170c/multidict-6.7.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f844a1bbf1d207dd311a56f383f7eda2d0e134921d45751842d8235e7778965d", size = 248395 }, - { url = "https://files.pythonhosted.org/packages/99/ac/82cb419dd6b04ccf9e7e61befc00c77614fc8134362488b553402ecd55ce/multidict-6.7.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:d4393e3581e84e5645506923816b9cc81f5609a778c7e7534054091acc64d1c6", size = 239520 }, - { url = "https://files.pythonhosted.org/packages/fa/f3/a0f9bf09493421bd8716a362e0cd1d244f5a6550f5beffdd6b47e885b331/multidict-6.7.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:fbd18dc82d7bf274b37aa48d664534330af744e03bccf696d6f4c6042e7d19e7", size = 245479 }, - { url = "https://files.pythonhosted.org/packages/8d/01/476d38fc73a212843f43c852b0eee266b6971f0e28329c2184a8df90c376/multidict-6.7.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:b6234e14f9314731ec45c42fc4554b88133ad53a09092cc48a88e771c125dadb", size = 258903 }, - { url = "https://files.pythonhosted.org/packages/49/6d/23faeb0868adba613b817d0e69c5f15531b24d462af8012c4f6de4fa8dc3/multidict-6.7.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:08d4379f9744d8f78d98c8673c06e202ffa88296f009c71bbafe8a6bf847d01f", size = 252333 }, - { url = "https://files.pythonhosted.org/packages/1e/cc/48d02ac22b30fa247f7dad82866e4b1015431092f4ba6ebc7e77596e0b18/multidict-6.7.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:9fe04da3f79387f450fd0061d4dd2e45a72749d31bf634aecc9e27f24fdc4b3f", size = 243411 }, - { url = "https://files.pythonhosted.org/packages/4a/03/29a8bf5a18abf1fe34535c88adbdfa88c9fb869b5a3b120692c64abe8284/multidict-6.7.0-cp314-cp314-win32.whl", hash = "sha256:fbafe31d191dfa7c4c51f7a6149c9fb7e914dcf9ffead27dcfd9f1ae382b3885", size = 40940 }, - { url = "https://files.pythonhosted.org/packages/82/16/7ed27b680791b939de138f906d5cf2b4657b0d45ca6f5dd6236fdddafb1a/multidict-6.7.0-cp314-cp314-win_amd64.whl", hash = "sha256:2f67396ec0310764b9222a1728ced1ab638f61aadc6226f17a71dd9324f9a99c", size = 45087 }, - { url = "https://files.pythonhosted.org/packages/cd/3c/e3e62eb35a1950292fe39315d3c89941e30a9d07d5d2df42965ab041da43/multidict-6.7.0-cp314-cp314-win_arm64.whl", hash = "sha256:ba672b26069957ee369cfa7fc180dde1fc6f176eaf1e6beaf61fbebbd3d9c000", size = 42368 }, - { url = "https://files.pythonhosted.org/packages/8b/40/cd499bd0dbc5f1136726db3153042a735fffd0d77268e2ee20d5f33c010f/multidict-6.7.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:c1dcc7524066fa918c6a27d61444d4ee7900ec635779058571f70d042d86ed63", size = 82326 }, - { url = "https://files.pythonhosted.org/packages/13/8a/18e031eca251c8df76daf0288e6790561806e439f5ce99a170b4af30676b/multidict-6.7.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:27e0b36c2d388dc7b6ced3406671b401e84ad7eb0656b8f3a2f46ed0ce483718", size = 48065 }, - { url = "https://files.pythonhosted.org/packages/40/71/5e6701277470a87d234e433fb0a3a7deaf3bcd92566e421e7ae9776319de/multidict-6.7.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2a7baa46a22e77f0988e3b23d4ede5513ebec1929e34ee9495be535662c0dfe2", size = 46475 }, - { url = "https://files.pythonhosted.org/packages/fe/6a/bab00cbab6d9cfb57afe1663318f72ec28289ea03fd4e8236bb78429893a/multidict-6.7.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:7bf77f54997a9166a2f5675d1201520586439424c2511723a7312bdb4bcc034e", size = 239324 }, - { url = "https://files.pythonhosted.org/packages/2a/5f/8de95f629fc22a7769ade8b41028e3e5a822c1f8904f618d175945a81ad3/multidict-6.7.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e011555abada53f1578d63389610ac8a5400fc70ce71156b0aa30d326f1a5064", size = 246877 }, - { url = "https://files.pythonhosted.org/packages/23/b4/38881a960458f25b89e9f4a4fdcb02ac101cfa710190db6e5528841e67de/multidict-6.7.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:28b37063541b897fd6a318007373930a75ca6d6ac7c940dbe14731ffdd8d498e", size = 225824 }, - { url = "https://files.pythonhosted.org/packages/1e/39/6566210c83f8a261575f18e7144736059f0c460b362e96e9cf797a24b8e7/multidict-6.7.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:05047ada7a2fde2631a0ed706f1fd68b169a681dfe5e4cf0f8e4cb6618bbc2cd", size = 253558 }, - { url = "https://files.pythonhosted.org/packages/00/a3/67f18315100f64c269f46e6c0319fa87ba68f0f64f2b8e7fd7c72b913a0b/multidict-6.7.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:716133f7d1d946a4e1b91b1756b23c088881e70ff180c24e864c26192ad7534a", size = 252339 }, - { url = "https://files.pythonhosted.org/packages/c8/2a/1cb77266afee2458d82f50da41beba02159b1d6b1f7973afc9a1cad1499b/multidict-6.7.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d1bed1b467ef657f2a0ae62844a607909ef1c6889562de5e1d505f74457d0b96", size = 244895 }, - { url = "https://files.pythonhosted.org/packages/dd/72/09fa7dd487f119b2eb9524946ddd36e2067c08510576d43ff68469563b3b/multidict-6.7.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:ca43bdfa5d37bd6aee89d85e1d0831fb86e25541be7e9d376ead1b28974f8e5e", size = 241862 }, - { url = "https://files.pythonhosted.org/packages/65/92/bc1f8bd0853d8669300f732c801974dfc3702c3eeadae2f60cef54dc69d7/multidict-6.7.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:44b546bd3eb645fd26fb949e43c02a25a2e632e2ca21a35e2e132c8105dc8599", size = 232376 }, - { url = "https://files.pythonhosted.org/packages/09/86/ac39399e5cb9d0c2ac8ef6e10a768e4d3bc933ac808d49c41f9dc23337eb/multidict-6.7.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:a6ef16328011d3f468e7ebc326f24c1445f001ca1dec335b2f8e66bed3006394", size = 240272 }, - { url = "https://files.pythonhosted.org/packages/3d/b6/fed5ac6b8563ec72df6cb1ea8dac6d17f0a4a1f65045f66b6d3bf1497c02/multidict-6.7.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:5aa873cbc8e593d361ae65c68f85faadd755c3295ea2c12040ee146802f23b38", size = 248774 }, - { url = "https://files.pythonhosted.org/packages/6b/8d/b954d8c0dc132b68f760aefd45870978deec6818897389dace00fcde32ff/multidict-6.7.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:3d7b6ccce016e29df4b7ca819659f516f0bc7a4b3efa3bb2012ba06431b044f9", size = 242731 }, - { url = "https://files.pythonhosted.org/packages/16/9d/a2dac7009125d3540c2f54e194829ea18ac53716c61b655d8ed300120b0f/multidict-6.7.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:171b73bd4ee683d307599b66793ac80981b06f069b62eea1c9e29c9241aa66b0", size = 240193 }, - { url = "https://files.pythonhosted.org/packages/39/ca/c05f144128ea232ae2178b008d5011d4e2cea86e4ee8c85c2631b1b94802/multidict-6.7.0-cp314-cp314t-win32.whl", hash = "sha256:b2d7f80c4e1fd010b07cb26820aae86b7e73b681ee4889684fb8d2d4537aab13", size = 48023 }, - { url = "https://files.pythonhosted.org/packages/ba/8f/0a60e501584145588be1af5cc829265701ba3c35a64aec8e07cbb71d39bb/multidict-6.7.0-cp314-cp314t-win_amd64.whl", hash = "sha256:09929cab6fcb68122776d575e03c6cc64ee0b8fca48d17e135474b042ce515cd", size = 53507 }, - { url = "https://files.pythonhosted.org/packages/7f/ae/3148b988a9c6239903e786eac19c889fab607c31d6efa7fb2147e5680f23/multidict-6.7.0-cp314-cp314t-win_arm64.whl", hash = "sha256:cc41db090ed742f32bd2d2c721861725e6109681eddf835d0a82bd3a5c382827", size = 44804 }, - { url = "https://files.pythonhosted.org/packages/b7/da/7d22601b625e241d4f23ef1ebff8acfc60da633c9e7e7922e24d10f592b3/multidict-6.7.0-py3-none-any.whl", hash = "sha256:394fc5c42a333c9ffc3e421a4c85e08580d990e08b99f6bf35b4132114c5dcb3", size = 12317 }, +sdist = { url = "https://files.pythonhosted.org/packages/80/1e/5492c365f222f907de1039b91f922b93fa4f764c713ee858d235495d8f50/multidict-6.7.0.tar.gz", hash = "sha256:c6e99d9a65ca282e578dfea819cfa9c0a62b2499d8677392e09feaf305e9e6f5", size = 101834, upload-time = "2025-10-06T14:52:30.657Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d2/86/33272a544eeb36d66e4d9a920602d1a2f57d4ebea4ef3cdfe5a912574c95/multidict-6.7.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:bee7c0588aa0076ce77c0ea5d19a68d76ad81fcd9fe8501003b9a24f9d4000f6", size = 76135, upload-time = "2025-10-06T14:49:54.26Z" }, + { url = "https://files.pythonhosted.org/packages/91/1c/eb97db117a1ebe46d457a3d235a7b9d2e6dcab174f42d1b67663dd9e5371/multidict-6.7.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7ef6b61cad77091056ce0e7ce69814ef72afacb150b7ac6a3e9470def2198159", size = 45117, upload-time = "2025-10-06T14:49:55.82Z" }, + { url = "https://files.pythonhosted.org/packages/f1/d8/6c3442322e41fb1dd4de8bd67bfd11cd72352ac131f6368315617de752f1/multidict-6.7.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9c0359b1ec12b1d6849c59f9d319610b7f20ef990a6d454ab151aa0e3b9f78ca", size = 43472, upload-time = "2025-10-06T14:49:57.048Z" }, + { url = "https://files.pythonhosted.org/packages/75/3f/e2639e80325af0b6c6febdf8e57cc07043ff15f57fa1ef808f4ccb5ac4cd/multidict-6.7.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:cd240939f71c64bd658f186330603aac1a9a81bf6273f523fca63673cb7378a8", size = 249342, upload-time = "2025-10-06T14:49:58.368Z" }, + { url = "https://files.pythonhosted.org/packages/5d/cc/84e0585f805cbeaa9cbdaa95f9a3d6aed745b9d25700623ac89a6ecff400/multidict-6.7.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a60a4d75718a5efa473ebd5ab685786ba0c67b8381f781d1be14da49f1a2dc60", size = 257082, upload-time = "2025-10-06T14:49:59.89Z" }, + { url = "https://files.pythonhosted.org/packages/b0/9c/ac851c107c92289acbbf5cfb485694084690c1b17e555f44952c26ddc5bd/multidict-6.7.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:53a42d364f323275126aff81fb67c5ca1b7a04fda0546245730a55c8c5f24bc4", size = 240704, upload-time = "2025-10-06T14:50:01.485Z" }, + { url = "https://files.pythonhosted.org/packages/50/cc/5f93e99427248c09da95b62d64b25748a5f5c98c7c2ab09825a1d6af0e15/multidict-6.7.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3b29b980d0ddbecb736735ee5bef69bb2ddca56eff603c86f3f29a1128299b4f", size = 266355, upload-time = "2025-10-06T14:50:02.955Z" }, + { url = "https://files.pythonhosted.org/packages/ec/0c/2ec1d883ceb79c6f7f6d7ad90c919c898f5d1c6ea96d322751420211e072/multidict-6.7.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f8a93b1c0ed2d04b97a5e9336fd2d33371b9a6e29ab7dd6503d63407c20ffbaf", size = 267259, upload-time = "2025-10-06T14:50:04.446Z" }, + { url = "https://files.pythonhosted.org/packages/c6/2d/f0b184fa88d6630aa267680bdb8623fb69cb0d024b8c6f0d23f9a0f406d3/multidict-6.7.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9ff96e8815eecacc6645da76c413eb3b3d34cfca256c70b16b286a687d013c32", size = 254903, upload-time = "2025-10-06T14:50:05.98Z" }, + { url = "https://files.pythonhosted.org/packages/06/c9/11ea263ad0df7dfabcad404feb3c0dd40b131bc7f232d5537f2fb1356951/multidict-6.7.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7516c579652f6a6be0e266aec0acd0db80829ca305c3d771ed898538804c2036", size = 252365, upload-time = "2025-10-06T14:50:07.511Z" }, + { url = "https://files.pythonhosted.org/packages/41/88/d714b86ee2c17d6e09850c70c9d310abac3d808ab49dfa16b43aba9d53fd/multidict-6.7.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:040f393368e63fb0f3330e70c26bfd336656bed925e5cbe17c9da839a6ab13ec", size = 250062, upload-time = "2025-10-06T14:50:09.074Z" }, + { url = "https://files.pythonhosted.org/packages/15/fe/ad407bb9e818c2b31383f6131ca19ea7e35ce93cf1310fce69f12e89de75/multidict-6.7.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b3bc26a951007b1057a1c543af845f1c7e3e71cc240ed1ace7bf4484aa99196e", size = 249683, upload-time = "2025-10-06T14:50:10.714Z" }, + { url = "https://files.pythonhosted.org/packages/8c/a4/a89abdb0229e533fb925e7c6e5c40201c2873efebc9abaf14046a4536ee6/multidict-6.7.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:7b022717c748dd1992a83e219587aabe45980d88969f01b316e78683e6285f64", size = 261254, upload-time = "2025-10-06T14:50:12.28Z" }, + { url = "https://files.pythonhosted.org/packages/8d/aa/0e2b27bd88b40a4fb8dc53dd74eecac70edaa4c1dd0707eb2164da3675b3/multidict-6.7.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:9600082733859f00d79dee64effc7aef1beb26adb297416a4ad2116fd61374bd", size = 257967, upload-time = "2025-10-06T14:50:14.16Z" }, + { url = "https://files.pythonhosted.org/packages/d0/8e/0c67b7120d5d5f6d874ed85a085f9dc770a7f9d8813e80f44a9fec820bb7/multidict-6.7.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:94218fcec4d72bc61df51c198d098ce2b378e0ccbac41ddbed5ef44092913288", size = 250085, upload-time = "2025-10-06T14:50:15.639Z" }, + { url = "https://files.pythonhosted.org/packages/ba/55/b73e1d624ea4b8fd4dd07a3bb70f6e4c7c6c5d9d640a41c6ffe5cdbd2a55/multidict-6.7.0-cp313-cp313-win32.whl", hash = "sha256:a37bd74c3fa9d00be2d7b8eca074dc56bd8077ddd2917a839bd989612671ed17", size = 41713, upload-time = "2025-10-06T14:50:17.066Z" }, + { url = "https://files.pythonhosted.org/packages/32/31/75c59e7d3b4205075b4c183fa4ca398a2daf2303ddf616b04ae6ef55cffe/multidict-6.7.0-cp313-cp313-win_amd64.whl", hash = "sha256:30d193c6cc6d559db42b6bcec8a5d395d34d60c9877a0b71ecd7c204fcf15390", size = 45915, upload-time = "2025-10-06T14:50:18.264Z" }, + { url = "https://files.pythonhosted.org/packages/31/2a/8987831e811f1184c22bc2e45844934385363ee61c0a2dcfa8f71b87e608/multidict-6.7.0-cp313-cp313-win_arm64.whl", hash = "sha256:ea3334cabe4d41b7ccd01e4d349828678794edbc2d3ae97fc162a3312095092e", size = 43077, upload-time = "2025-10-06T14:50:19.853Z" }, + { url = "https://files.pythonhosted.org/packages/e8/68/7b3a5170a382a340147337b300b9eb25a9ddb573bcdfff19c0fa3f31ffba/multidict-6.7.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:ad9ce259f50abd98a1ca0aa6e490b58c316a0fce0617f609723e40804add2c00", size = 83114, upload-time = "2025-10-06T14:50:21.223Z" }, + { url = "https://files.pythonhosted.org/packages/55/5c/3fa2d07c84df4e302060f555bbf539310980362236ad49f50eeb0a1c1eb9/multidict-6.7.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:07f5594ac6d084cbb5de2df218d78baf55ef150b91f0ff8a21cc7a2e3a5a58eb", size = 48442, upload-time = "2025-10-06T14:50:22.871Z" }, + { url = "https://files.pythonhosted.org/packages/fc/56/67212d33239797f9bd91962bb899d72bb0f4c35a8652dcdb8ed049bef878/multidict-6.7.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:0591b48acf279821a579282444814a2d8d0af624ae0bc600aa4d1b920b6e924b", size = 46885, upload-time = "2025-10-06T14:50:24.258Z" }, + { url = "https://files.pythonhosted.org/packages/46/d1/908f896224290350721597a61a69cd19b89ad8ee0ae1f38b3f5cd12ea2ac/multidict-6.7.0-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:749a72584761531d2b9467cfbdfd29487ee21124c304c4b6cb760d8777b27f9c", size = 242588, upload-time = "2025-10-06T14:50:25.716Z" }, + { url = "https://files.pythonhosted.org/packages/ab/67/8604288bbd68680eee0ab568fdcb56171d8b23a01bcd5cb0c8fedf6e5d99/multidict-6.7.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b4c3d199f953acd5b446bf7c0de1fe25d94e09e79086f8dc2f48a11a129cdf1", size = 249966, upload-time = "2025-10-06T14:50:28.192Z" }, + { url = "https://files.pythonhosted.org/packages/20/33/9228d76339f1ba51e3efef7da3ebd91964d3006217aae13211653193c3ff/multidict-6.7.0-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:9fb0211dfc3b51efea2f349ec92c114d7754dd62c01f81c3e32b765b70c45c9b", size = 228618, upload-time = "2025-10-06T14:50:29.82Z" }, + { url = "https://files.pythonhosted.org/packages/f8/2d/25d9b566d10cab1c42b3b9e5b11ef79c9111eaf4463b8c257a3bd89e0ead/multidict-6.7.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a027ec240fe73a8d6281872690b988eed307cd7d91b23998ff35ff577ca688b5", size = 257539, upload-time = "2025-10-06T14:50:31.731Z" }, + { url = "https://files.pythonhosted.org/packages/b6/b1/8d1a965e6637fc33de3c0d8f414485c2b7e4af00f42cab3d84e7b955c222/multidict-6.7.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d1d964afecdf3a8288789df2f5751dc0a8261138c3768d9af117ed384e538fad", size = 256345, upload-time = "2025-10-06T14:50:33.26Z" }, + { url = "https://files.pythonhosted.org/packages/ba/0c/06b5a8adbdeedada6f4fb8d8f193d44a347223b11939b42953eeb6530b6b/multidict-6.7.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:caf53b15b1b7df9fbd0709aa01409000a2b4dd03a5f6f5cc548183c7c8f8b63c", size = 247934, upload-time = "2025-10-06T14:50:34.808Z" }, + { url = "https://files.pythonhosted.org/packages/8f/31/b2491b5fe167ca044c6eb4b8f2c9f3b8a00b24c432c365358eadac5d7625/multidict-6.7.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:654030da3197d927f05a536a66186070e98765aa5142794c9904555d3a9d8fb5", size = 245243, upload-time = "2025-10-06T14:50:36.436Z" }, + { url = "https://files.pythonhosted.org/packages/61/1a/982913957cb90406c8c94f53001abd9eafc271cb3e70ff6371590bec478e/multidict-6.7.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:2090d3718829d1e484706a2f525e50c892237b2bf9b17a79b059cb98cddc2f10", size = 235878, upload-time = "2025-10-06T14:50:37.953Z" }, + { url = "https://files.pythonhosted.org/packages/be/c0/21435d804c1a1cf7a2608593f4d19bca5bcbd7a81a70b253fdd1c12af9c0/multidict-6.7.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:2d2cfeec3f6f45651b3d408c4acec0ebf3daa9bc8a112a084206f5db5d05b754", size = 243452, upload-time = "2025-10-06T14:50:39.574Z" }, + { url = "https://files.pythonhosted.org/packages/54/0a/4349d540d4a883863191be6eb9a928846d4ec0ea007d3dcd36323bb058ac/multidict-6.7.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:4ef089f985b8c194d341eb2c24ae6e7408c9a0e2e5658699c92f497437d88c3c", size = 252312, upload-time = "2025-10-06T14:50:41.612Z" }, + { url = "https://files.pythonhosted.org/packages/26/64/d5416038dbda1488daf16b676e4dbfd9674dde10a0cc8f4fc2b502d8125d/multidict-6.7.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:e93a0617cd16998784bf4414c7e40f17a35d2350e5c6f0bd900d3a8e02bd3762", size = 246935, upload-time = "2025-10-06T14:50:43.972Z" }, + { url = "https://files.pythonhosted.org/packages/9f/8c/8290c50d14e49f35e0bd4abc25e1bc7711149ca9588ab7d04f886cdf03d9/multidict-6.7.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f0feece2ef8ebc42ed9e2e8c78fc4aa3cf455733b507c09ef7406364c94376c6", size = 243385, upload-time = "2025-10-06T14:50:45.648Z" }, + { url = "https://files.pythonhosted.org/packages/ef/a0/f83ae75e42d694b3fbad3e047670e511c138be747bc713cf1b10d5096416/multidict-6.7.0-cp313-cp313t-win32.whl", hash = "sha256:19a1d55338ec1be74ef62440ca9e04a2f001a04d0cc49a4983dc320ff0f3212d", size = 47777, upload-time = "2025-10-06T14:50:47.154Z" }, + { url = "https://files.pythonhosted.org/packages/dc/80/9b174a92814a3830b7357307a792300f42c9e94664b01dee8e457551fa66/multidict-6.7.0-cp313-cp313t-win_amd64.whl", hash = "sha256:3da4fb467498df97e986af166b12d01f05d2e04f978a9c1c680ea1988e0bc4b6", size = 53104, upload-time = "2025-10-06T14:50:48.851Z" }, + { url = "https://files.pythonhosted.org/packages/cc/28/04baeaf0428d95bb7a7bea0e691ba2f31394338ba424fb0679a9ed0f4c09/multidict-6.7.0-cp313-cp313t-win_arm64.whl", hash = "sha256:b4121773c49a0776461f4a904cdf6264c88e42218aaa8407e803ca8025872792", size = 45503, upload-time = "2025-10-06T14:50:50.16Z" }, + { url = "https://files.pythonhosted.org/packages/e2/b1/3da6934455dd4b261d4c72f897e3a5728eba81db59959f3a639245891baa/multidict-6.7.0-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3bab1e4aff7adaa34410f93b1f8e57c4b36b9af0426a76003f441ee1d3c7e842", size = 75128, upload-time = "2025-10-06T14:50:51.92Z" }, + { url = "https://files.pythonhosted.org/packages/14/2c/f069cab5b51d175a1a2cb4ccdf7a2c2dabd58aa5bd933fa036a8d15e2404/multidict-6.7.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:b8512bac933afc3e45fb2b18da8e59b78d4f408399a960339598374d4ae3b56b", size = 44410, upload-time = "2025-10-06T14:50:53.275Z" }, + { url = "https://files.pythonhosted.org/packages/42/e2/64bb41266427af6642b6b128e8774ed84c11b80a90702c13ac0a86bb10cc/multidict-6.7.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:79dcf9e477bc65414ebfea98ffd013cb39552b5ecd62908752e0e413d6d06e38", size = 43205, upload-time = "2025-10-06T14:50:54.911Z" }, + { url = "https://files.pythonhosted.org/packages/02/68/6b086fef8a3f1a8541b9236c594f0c9245617c29841f2e0395d979485cde/multidict-6.7.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:31bae522710064b5cbeddaf2e9f32b1abab70ac6ac91d42572502299e9953128", size = 245084, upload-time = "2025-10-06T14:50:56.369Z" }, + { url = "https://files.pythonhosted.org/packages/15/ee/f524093232007cd7a75c1d132df70f235cfd590a7c9eaccd7ff422ef4ae8/multidict-6.7.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4a0df7ff02397bb63e2fd22af2c87dfa39e8c7f12947bc524dbdc528282c7e34", size = 252667, upload-time = "2025-10-06T14:50:57.991Z" }, + { url = "https://files.pythonhosted.org/packages/02/a5/eeb3f43ab45878f1895118c3ef157a480db58ede3f248e29b5354139c2c9/multidict-6.7.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:7a0222514e8e4c514660e182d5156a415c13ef0aabbd71682fc714e327b95e99", size = 233590, upload-time = "2025-10-06T14:50:59.589Z" }, + { url = "https://files.pythonhosted.org/packages/6a/1e/76d02f8270b97269d7e3dbd45644b1785bda457b474315f8cf999525a193/multidict-6.7.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2397ab4daaf2698eb51a76721e98db21ce4f52339e535725de03ea962b5a3202", size = 264112, upload-time = "2025-10-06T14:51:01.183Z" }, + { url = "https://files.pythonhosted.org/packages/76/0b/c28a70ecb58963847c2a8efe334904cd254812b10e535aefb3bcce513918/multidict-6.7.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8891681594162635948a636c9fe0ff21746aeb3dd5463f6e25d9bea3a8a39ca1", size = 261194, upload-time = "2025-10-06T14:51:02.794Z" }, + { url = "https://files.pythonhosted.org/packages/b4/63/2ab26e4209773223159b83aa32721b4021ffb08102f8ac7d689c943fded1/multidict-6.7.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:18706cc31dbf402a7945916dd5cddf160251b6dab8a2c5f3d6d5a55949f676b3", size = 248510, upload-time = "2025-10-06T14:51:04.724Z" }, + { url = "https://files.pythonhosted.org/packages/93/cd/06c1fa8282af1d1c46fd55c10a7930af652afdce43999501d4d68664170c/multidict-6.7.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f844a1bbf1d207dd311a56f383f7eda2d0e134921d45751842d8235e7778965d", size = 248395, upload-time = "2025-10-06T14:51:06.306Z" }, + { url = "https://files.pythonhosted.org/packages/99/ac/82cb419dd6b04ccf9e7e61befc00c77614fc8134362488b553402ecd55ce/multidict-6.7.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:d4393e3581e84e5645506923816b9cc81f5609a778c7e7534054091acc64d1c6", size = 239520, upload-time = "2025-10-06T14:51:08.091Z" }, + { url = "https://files.pythonhosted.org/packages/fa/f3/a0f9bf09493421bd8716a362e0cd1d244f5a6550f5beffdd6b47e885b331/multidict-6.7.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:fbd18dc82d7bf274b37aa48d664534330af744e03bccf696d6f4c6042e7d19e7", size = 245479, upload-time = "2025-10-06T14:51:10.365Z" }, + { url = "https://files.pythonhosted.org/packages/8d/01/476d38fc73a212843f43c852b0eee266b6971f0e28329c2184a8df90c376/multidict-6.7.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:b6234e14f9314731ec45c42fc4554b88133ad53a09092cc48a88e771c125dadb", size = 258903, upload-time = "2025-10-06T14:51:12.466Z" }, + { url = "https://files.pythonhosted.org/packages/49/6d/23faeb0868adba613b817d0e69c5f15531b24d462af8012c4f6de4fa8dc3/multidict-6.7.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:08d4379f9744d8f78d98c8673c06e202ffa88296f009c71bbafe8a6bf847d01f", size = 252333, upload-time = "2025-10-06T14:51:14.48Z" }, + { url = "https://files.pythonhosted.org/packages/1e/cc/48d02ac22b30fa247f7dad82866e4b1015431092f4ba6ebc7e77596e0b18/multidict-6.7.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:9fe04da3f79387f450fd0061d4dd2e45a72749d31bf634aecc9e27f24fdc4b3f", size = 243411, upload-time = "2025-10-06T14:51:16.072Z" }, + { url = "https://files.pythonhosted.org/packages/4a/03/29a8bf5a18abf1fe34535c88adbdfa88c9fb869b5a3b120692c64abe8284/multidict-6.7.0-cp314-cp314-win32.whl", hash = "sha256:fbafe31d191dfa7c4c51f7a6149c9fb7e914dcf9ffead27dcfd9f1ae382b3885", size = 40940, upload-time = "2025-10-06T14:51:17.544Z" }, + { url = "https://files.pythonhosted.org/packages/82/16/7ed27b680791b939de138f906d5cf2b4657b0d45ca6f5dd6236fdddafb1a/multidict-6.7.0-cp314-cp314-win_amd64.whl", hash = "sha256:2f67396ec0310764b9222a1728ced1ab638f61aadc6226f17a71dd9324f9a99c", size = 45087, upload-time = "2025-10-06T14:51:18.875Z" }, + { url = "https://files.pythonhosted.org/packages/cd/3c/e3e62eb35a1950292fe39315d3c89941e30a9d07d5d2df42965ab041da43/multidict-6.7.0-cp314-cp314-win_arm64.whl", hash = "sha256:ba672b26069957ee369cfa7fc180dde1fc6f176eaf1e6beaf61fbebbd3d9c000", size = 42368, upload-time = "2025-10-06T14:51:20.225Z" }, + { url = "https://files.pythonhosted.org/packages/8b/40/cd499bd0dbc5f1136726db3153042a735fffd0d77268e2ee20d5f33c010f/multidict-6.7.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:c1dcc7524066fa918c6a27d61444d4ee7900ec635779058571f70d042d86ed63", size = 82326, upload-time = "2025-10-06T14:51:21.588Z" }, + { url = "https://files.pythonhosted.org/packages/13/8a/18e031eca251c8df76daf0288e6790561806e439f5ce99a170b4af30676b/multidict-6.7.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:27e0b36c2d388dc7b6ced3406671b401e84ad7eb0656b8f3a2f46ed0ce483718", size = 48065, upload-time = "2025-10-06T14:51:22.93Z" }, + { url = "https://files.pythonhosted.org/packages/40/71/5e6701277470a87d234e433fb0a3a7deaf3bcd92566e421e7ae9776319de/multidict-6.7.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2a7baa46a22e77f0988e3b23d4ede5513ebec1929e34ee9495be535662c0dfe2", size = 46475, upload-time = "2025-10-06T14:51:24.352Z" }, + { url = "https://files.pythonhosted.org/packages/fe/6a/bab00cbab6d9cfb57afe1663318f72ec28289ea03fd4e8236bb78429893a/multidict-6.7.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:7bf77f54997a9166a2f5675d1201520586439424c2511723a7312bdb4bcc034e", size = 239324, upload-time = "2025-10-06T14:51:25.822Z" }, + { url = "https://files.pythonhosted.org/packages/2a/5f/8de95f629fc22a7769ade8b41028e3e5a822c1f8904f618d175945a81ad3/multidict-6.7.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e011555abada53f1578d63389610ac8a5400fc70ce71156b0aa30d326f1a5064", size = 246877, upload-time = "2025-10-06T14:51:27.604Z" }, + { url = "https://files.pythonhosted.org/packages/23/b4/38881a960458f25b89e9f4a4fdcb02ac101cfa710190db6e5528841e67de/multidict-6.7.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:28b37063541b897fd6a318007373930a75ca6d6ac7c940dbe14731ffdd8d498e", size = 225824, upload-time = "2025-10-06T14:51:29.664Z" }, + { url = "https://files.pythonhosted.org/packages/1e/39/6566210c83f8a261575f18e7144736059f0c460b362e96e9cf797a24b8e7/multidict-6.7.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:05047ada7a2fde2631a0ed706f1fd68b169a681dfe5e4cf0f8e4cb6618bbc2cd", size = 253558, upload-time = "2025-10-06T14:51:31.684Z" }, + { url = "https://files.pythonhosted.org/packages/00/a3/67f18315100f64c269f46e6c0319fa87ba68f0f64f2b8e7fd7c72b913a0b/multidict-6.7.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:716133f7d1d946a4e1b91b1756b23c088881e70ff180c24e864c26192ad7534a", size = 252339, upload-time = "2025-10-06T14:51:33.699Z" }, + { url = "https://files.pythonhosted.org/packages/c8/2a/1cb77266afee2458d82f50da41beba02159b1d6b1f7973afc9a1cad1499b/multidict-6.7.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d1bed1b467ef657f2a0ae62844a607909ef1c6889562de5e1d505f74457d0b96", size = 244895, upload-time = "2025-10-06T14:51:36.189Z" }, + { url = "https://files.pythonhosted.org/packages/dd/72/09fa7dd487f119b2eb9524946ddd36e2067c08510576d43ff68469563b3b/multidict-6.7.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:ca43bdfa5d37bd6aee89d85e1d0831fb86e25541be7e9d376ead1b28974f8e5e", size = 241862, upload-time = "2025-10-06T14:51:41.291Z" }, + { url = "https://files.pythonhosted.org/packages/65/92/bc1f8bd0853d8669300f732c801974dfc3702c3eeadae2f60cef54dc69d7/multidict-6.7.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:44b546bd3eb645fd26fb949e43c02a25a2e632e2ca21a35e2e132c8105dc8599", size = 232376, upload-time = "2025-10-06T14:51:43.55Z" }, + { url = "https://files.pythonhosted.org/packages/09/86/ac39399e5cb9d0c2ac8ef6e10a768e4d3bc933ac808d49c41f9dc23337eb/multidict-6.7.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:a6ef16328011d3f468e7ebc326f24c1445f001ca1dec335b2f8e66bed3006394", size = 240272, upload-time = "2025-10-06T14:51:45.265Z" }, + { url = "https://files.pythonhosted.org/packages/3d/b6/fed5ac6b8563ec72df6cb1ea8dac6d17f0a4a1f65045f66b6d3bf1497c02/multidict-6.7.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:5aa873cbc8e593d361ae65c68f85faadd755c3295ea2c12040ee146802f23b38", size = 248774, upload-time = "2025-10-06T14:51:46.836Z" }, + { url = "https://files.pythonhosted.org/packages/6b/8d/b954d8c0dc132b68f760aefd45870978deec6818897389dace00fcde32ff/multidict-6.7.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:3d7b6ccce016e29df4b7ca819659f516f0bc7a4b3efa3bb2012ba06431b044f9", size = 242731, upload-time = "2025-10-06T14:51:48.541Z" }, + { url = "https://files.pythonhosted.org/packages/16/9d/a2dac7009125d3540c2f54e194829ea18ac53716c61b655d8ed300120b0f/multidict-6.7.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:171b73bd4ee683d307599b66793ac80981b06f069b62eea1c9e29c9241aa66b0", size = 240193, upload-time = "2025-10-06T14:51:50.355Z" }, + { url = "https://files.pythonhosted.org/packages/39/ca/c05f144128ea232ae2178b008d5011d4e2cea86e4ee8c85c2631b1b94802/multidict-6.7.0-cp314-cp314t-win32.whl", hash = "sha256:b2d7f80c4e1fd010b07cb26820aae86b7e73b681ee4889684fb8d2d4537aab13", size = 48023, upload-time = "2025-10-06T14:51:51.883Z" }, + { url = "https://files.pythonhosted.org/packages/ba/8f/0a60e501584145588be1af5cc829265701ba3c35a64aec8e07cbb71d39bb/multidict-6.7.0-cp314-cp314t-win_amd64.whl", hash = "sha256:09929cab6fcb68122776d575e03c6cc64ee0b8fca48d17e135474b042ce515cd", size = 53507, upload-time = "2025-10-06T14:51:53.672Z" }, + { url = "https://files.pythonhosted.org/packages/7f/ae/3148b988a9c6239903e786eac19c889fab607c31d6efa7fb2147e5680f23/multidict-6.7.0-cp314-cp314t-win_arm64.whl", hash = "sha256:cc41db090ed742f32bd2d2c721861725e6109681eddf835d0a82bd3a5c382827", size = 44804, upload-time = "2025-10-06T14:51:55.415Z" }, + { url = "https://files.pythonhosted.org/packages/b7/da/7d22601b625e241d4f23ef1ebff8acfc60da633c9e7e7922e24d10f592b3/multidict-6.7.0-py3-none-any.whl", hash = "sha256:394fc5c42a333c9ffc3e421a4c85e08580d990e08b99f6bf35b4132114c5dcb3", size = 12317, upload-time = "2025-10-06T14:52:29.272Z" }, ] [[package]] @@ -1607,18 +1584,18 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/06/50/95d7bc91f900da5e22662c82d9bf0f72a4b01f2a552708bf2f43807707a1/nexus_rpc-1.2.0.tar.gz", hash = "sha256:b4ddaffa4d3996aaeadf49b80dfcdfbca48fe4cb616defaf3b3c5c2c8fc61890", size = 74142 } +sdist = { url = "https://files.pythonhosted.org/packages/06/50/95d7bc91f900da5e22662c82d9bf0f72a4b01f2a552708bf2f43807707a1/nexus_rpc-1.2.0.tar.gz", hash = "sha256:b4ddaffa4d3996aaeadf49b80dfcdfbca48fe4cb616defaf3b3c5c2c8fc61890", size = 74142, upload-time = "2025-11-17T19:17:06.798Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/13/04/eaac430d0e6bf21265ae989427d37e94be5e41dc216879f1fbb6c5339942/nexus_rpc-1.2.0-py3-none-any.whl", hash = "sha256:977876f3af811ad1a09b2961d3d1ac9233bda43ff0febbb0c9906483b9d9f8a3", size = 28166 }, + { url = "https://files.pythonhosted.org/packages/13/04/eaac430d0e6bf21265ae989427d37e94be5e41dc216879f1fbb6c5339942/nexus_rpc-1.2.0-py3-none-any.whl", hash = "sha256:977876f3af811ad1a09b2961d3d1ac9233bda43ff0febbb0c9906483b9d9f8a3", size = 28166, upload-time = "2025-11-17T19:17:05.64Z" }, ] [[package]] name = "oauthlib" version = "3.3.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/0b/5f/19930f824ffeb0ad4372da4812c50edbd1434f678c90c2733e1188edfc63/oauthlib-3.3.1.tar.gz", hash = "sha256:0f0f8aa759826a193cf66c12ea1af1637f87b9b4622d46e866952bb022e538c9", size = 185918 } +sdist = { url = "https://files.pythonhosted.org/packages/0b/5f/19930f824ffeb0ad4372da4812c50edbd1434f678c90c2733e1188edfc63/oauthlib-3.3.1.tar.gz", hash = "sha256:0f0f8aa759826a193cf66c12ea1af1637f87b9b4622d46e866952bb022e538c9", size = 185918, upload-time = "2025-06-19T22:48:08.269Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/be/9c/92789c596b8df838baa98fa71844d84283302f7604ed565dafe5a6b5041a/oauthlib-3.3.1-py3-none-any.whl", hash = "sha256:88119c938d2b8fb88561af5f6ee0eec8cc8d552b7bb1f712743136eb7523b7a1", size = 160065 }, + { url = "https://files.pythonhosted.org/packages/be/9c/92789c596b8df838baa98fa71844d84283302f7604ed565dafe5a6b5041a/oauthlib-3.3.1-py3-none-any.whl", hash = "sha256:88119c938d2b8fb88561af5f6ee0eec8cc8d552b7bb1f712743136eb7523b7a1", size = 160065, upload-time = "2025-06-19T22:48:06.508Z" }, ] [[package]] @@ -1635,9 +1612,9 @@ dependencies = [ { name = "tqdm" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/d8/b1/12fe1c196bea326261718eb037307c1c1fe1dedc2d2d4de777df822e6238/openai-2.14.0.tar.gz", hash = "sha256:419357bedde9402d23bf8f2ee372fca1985a73348debba94bddff06f19459952", size = 626938 } +sdist = { url = "https://files.pythonhosted.org/packages/d8/b1/12fe1c196bea326261718eb037307c1c1fe1dedc2d2d4de777df822e6238/openai-2.14.0.tar.gz", hash = "sha256:419357bedde9402d23bf8f2ee372fca1985a73348debba94bddff06f19459952", size = 626938, upload-time = "2025-12-19T03:28:45.742Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/27/4b/7c1a00c2c3fbd004253937f7520f692a9650767aa73894d7a34f0d65d3f4/openai-2.14.0-py3-none-any.whl", hash = "sha256:7ea40aca4ffc4c4a776e77679021b47eec1160e341f42ae086ba949c9dcc9183", size = 1067558 }, + { url = "https://files.pythonhosted.org/packages/27/4b/7c1a00c2c3fbd004253937f7520f692a9650767aa73894d7a34f0d65d3f4/openai-2.14.0-py3-none-any.whl", hash = "sha256:7ea40aca4ffc4c4a776e77679021b47eec1160e341f42ae086ba949c9dcc9183", size = 1067558, upload-time = "2025-12-19T03:28:43.727Z" }, ] [[package]] @@ -1647,9 +1624,9 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pydantic" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/02/2e/58d83848dd1a79cb92ed8e63f6ba901ca282c5f09d04af9423ec26c56fd7/openapi_pydantic-0.5.1.tar.gz", hash = "sha256:ff6835af6bde7a459fb93eb93bb92b8749b754fc6e51b2f1590a19dc3005ee0d", size = 60892 } +sdist = { url = "https://files.pythonhosted.org/packages/02/2e/58d83848dd1a79cb92ed8e63f6ba901ca282c5f09d04af9423ec26c56fd7/openapi_pydantic-0.5.1.tar.gz", hash = "sha256:ff6835af6bde7a459fb93eb93bb92b8749b754fc6e51b2f1590a19dc3005ee0d", size = 60892, upload-time = "2025-01-08T19:29:27.083Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/12/cf/03675d8bd8ecbf4445504d8071adab19f5f993676795708e36402ab38263/openapi_pydantic-0.5.1-py3-none-any.whl", hash = "sha256:a3a09ef4586f5bd760a8df7f43028b60cafb6d9f61de2acba9574766255ab146", size = 96381 }, + { url = "https://files.pythonhosted.org/packages/12/cf/03675d8bd8ecbf4445504d8071adab19f5f993676795708e36402ab38263/openapi_pydantic-0.5.1-py3-none-any.whl", hash = "sha256:a3a09ef4586f5bd760a8df7f43028b60cafb6d9f61de2acba9574766255ab146", size = 96381, upload-time = "2025-01-08T19:29:25.275Z" }, ] [[package]] @@ -1660,9 +1637,9 @@ dependencies = [ { name = "importlib-metadata" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/97/b9/3161be15bb8e3ad01be8be5a968a9237c3027c5be504362ff800fca3e442/opentelemetry_api-1.39.1.tar.gz", hash = "sha256:fbde8c80e1b937a2c61f20347e91c0c18a1940cecf012d62e65a7caf08967c9c", size = 65767 } +sdist = { url = "https://files.pythonhosted.org/packages/97/b9/3161be15bb8e3ad01be8be5a968a9237c3027c5be504362ff800fca3e442/opentelemetry_api-1.39.1.tar.gz", hash = "sha256:fbde8c80e1b937a2c61f20347e91c0c18a1940cecf012d62e65a7caf08967c9c", size = 65767, upload-time = "2025-12-11T13:32:39.182Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/cf/df/d3f1ddf4bb4cb50ed9b1139cc7b1c54c34a1e7ce8fd1b9a37c0d1551a6bd/opentelemetry_api-1.39.1-py3-none-any.whl", hash = "sha256:2edd8463432a7f8443edce90972169b195e7d6a05500cd29e6d13898187c9950", size = 66356 }, + { url = "https://files.pythonhosted.org/packages/cf/df/d3f1ddf4bb4cb50ed9b1139cc7b1c54c34a1e7ce8fd1b9a37c0d1551a6bd/opentelemetry_api-1.39.1-py3-none-any.whl", hash = "sha256:2edd8463432a7f8443edce90972169b195e7d6a05500cd29e6d13898187c9950", size = 66356, upload-time = "2025-12-11T13:32:17.304Z" }, ] [[package]] @@ -1672,9 +1649,9 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "opentelemetry-proto" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/e9/9d/22d241b66f7bbde88a3bfa6847a351d2c46b84de23e71222c6aae25c7050/opentelemetry_exporter_otlp_proto_common-1.39.1.tar.gz", hash = "sha256:763370d4737a59741c89a67b50f9e39271639ee4afc999dadfe768541c027464", size = 20409 } +sdist = { url = "https://files.pythonhosted.org/packages/e9/9d/22d241b66f7bbde88a3bfa6847a351d2c46b84de23e71222c6aae25c7050/opentelemetry_exporter_otlp_proto_common-1.39.1.tar.gz", hash = "sha256:763370d4737a59741c89a67b50f9e39271639ee4afc999dadfe768541c027464", size = 20409, upload-time = "2025-12-11T13:32:40.885Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/8c/02/ffc3e143d89a27ac21fd557365b98bd0653b98de8a101151d5805b5d4c33/opentelemetry_exporter_otlp_proto_common-1.39.1-py3-none-any.whl", hash = "sha256:08f8a5862d64cc3435105686d0216c1365dc5701f86844a8cd56597d0c764fde", size = 18366 }, + { url = "https://files.pythonhosted.org/packages/8c/02/ffc3e143d89a27ac21fd557365b98bd0653b98de8a101151d5805b5d4c33/opentelemetry_exporter_otlp_proto_common-1.39.1-py3-none-any.whl", hash = "sha256:08f8a5862d64cc3435105686d0216c1365dc5701f86844a8cd56597d0c764fde", size = 18366, upload-time = "2025-12-11T13:32:20.2Z" }, ] [[package]] @@ -1690,9 +1667,9 @@ dependencies = [ { name = "requests" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/80/04/2a08fa9c0214ae38880df01e8bfae12b067ec0793446578575e5080d6545/opentelemetry_exporter_otlp_proto_http-1.39.1.tar.gz", hash = "sha256:31bdab9745c709ce90a49a0624c2bd445d31a28ba34275951a6a362d16a0b9cb", size = 17288 } +sdist = { url = "https://files.pythonhosted.org/packages/80/04/2a08fa9c0214ae38880df01e8bfae12b067ec0793446578575e5080d6545/opentelemetry_exporter_otlp_proto_http-1.39.1.tar.gz", hash = "sha256:31bdab9745c709ce90a49a0624c2bd445d31a28ba34275951a6a362d16a0b9cb", size = 17288, upload-time = "2025-12-11T13:32:42.029Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/95/f1/b27d3e2e003cd9a3592c43d099d2ed8d0a947c15281bf8463a256db0b46c/opentelemetry_exporter_otlp_proto_http-1.39.1-py3-none-any.whl", hash = "sha256:d9f5207183dd752a412c4cd564ca8875ececba13be6e9c6c370ffb752fd59985", size = 19641 }, + { url = "https://files.pythonhosted.org/packages/95/f1/b27d3e2e003cd9a3592c43d099d2ed8d0a947c15281bf8463a256db0b46c/opentelemetry_exporter_otlp_proto_http-1.39.1-py3-none-any.whl", hash = "sha256:d9f5207183dd752a412c4cd564ca8875ececba13be6e9c6c370ffb752fd59985", size = 19641, upload-time = "2025-12-11T13:32:22.248Z" }, ] [[package]] @@ -1704,9 +1681,9 @@ dependencies = [ { name = "opentelemetry-sdk" }, { name = "prometheus-client" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/14/39/7dafa6fff210737267bed35a8855b6ac7399b9e582b8cf1f25f842517012/opentelemetry_exporter_prometheus-0.60b1.tar.gz", hash = "sha256:a4011b46906323f71724649d301b4dc188aaa068852e814f4df38cc76eac616b", size = 14976 } +sdist = { url = "https://files.pythonhosted.org/packages/14/39/7dafa6fff210737267bed35a8855b6ac7399b9e582b8cf1f25f842517012/opentelemetry_exporter_prometheus-0.60b1.tar.gz", hash = "sha256:a4011b46906323f71724649d301b4dc188aaa068852e814f4df38cc76eac616b", size = 14976, upload-time = "2025-12-11T13:32:42.944Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/9b/0d/4be6bf5477a3eb3d917d2f17d3c0b6720cd6cb97898444a61d43cc983f5c/opentelemetry_exporter_prometheus-0.60b1-py3-none-any.whl", hash = "sha256:49f59178de4f4590e3cef0b8b95cf6e071aae70e1f060566df5546fad773b8fd", size = 13019 }, + { url = "https://files.pythonhosted.org/packages/9b/0d/4be6bf5477a3eb3d917d2f17d3c0b6720cd6cb97898444a61d43cc983f5c/opentelemetry_exporter_prometheus-0.60b1-py3-none-any.whl", hash = "sha256:49f59178de4f4590e3cef0b8b95cf6e071aae70e1f060566df5546fad773b8fd", size = 13019, upload-time = "2025-12-11T13:32:23.974Z" }, ] [[package]] @@ -1719,9 +1696,9 @@ dependencies = [ { name = "packaging" }, { name = "wrapt" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/41/0f/7e6b713ac117c1f5e4e3300748af699b9902a2e5e34c9cf443dde25a01fa/opentelemetry_instrumentation-0.60b1.tar.gz", hash = "sha256:57ddc7974c6eb35865af0426d1a17132b88b2ed8586897fee187fd5b8944bd6a", size = 31706 } +sdist = { url = "https://files.pythonhosted.org/packages/41/0f/7e6b713ac117c1f5e4e3300748af699b9902a2e5e34c9cf443dde25a01fa/opentelemetry_instrumentation-0.60b1.tar.gz", hash = "sha256:57ddc7974c6eb35865af0426d1a17132b88b2ed8586897fee187fd5b8944bd6a", size = 31706, upload-time = "2025-12-11T13:36:42.515Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/77/d2/6788e83c5c86a2690101681aeef27eeb2a6bf22df52d3f263a22cee20915/opentelemetry_instrumentation-0.60b1-py3-none-any.whl", hash = "sha256:04480db952b48fb1ed0073f822f0ee26012b7be7c3eac1a3793122737c78632d", size = 33096 }, + { url = "https://files.pythonhosted.org/packages/77/d2/6788e83c5c86a2690101681aeef27eeb2a6bf22df52d3f263a22cee20915/opentelemetry_instrumentation-0.60b1-py3-none-any.whl", hash = "sha256:04480db952b48fb1ed0073f822f0ee26012b7be7c3eac1a3793122737c78632d", size = 33096, upload-time = "2025-12-11T13:35:33.067Z" }, ] [[package]] @@ -1735,9 +1712,9 @@ dependencies = [ { name = "opentelemetry-util-http" }, { name = "wrapt" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/86/08/11208bcfcab4fc2023252c3f322aa397fd9ad948355fea60f5fc98648603/opentelemetry_instrumentation_httpx-0.60b1.tar.gz", hash = "sha256:a506ebaf28c60112cbe70ad4f0338f8603f148938cb7b6794ce1051cd2b270ae", size = 20611 } +sdist = { url = "https://files.pythonhosted.org/packages/86/08/11208bcfcab4fc2023252c3f322aa397fd9ad948355fea60f5fc98648603/opentelemetry_instrumentation_httpx-0.60b1.tar.gz", hash = "sha256:a506ebaf28c60112cbe70ad4f0338f8603f148938cb7b6794ce1051cd2b270ae", size = 20611, upload-time = "2025-12-11T13:37:01.661Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/43/59/b98e84eebf745ffc75397eaad4763795bff8a30cbf2373a50ed4e70646c5/opentelemetry_instrumentation_httpx-0.60b1-py3-none-any.whl", hash = "sha256:f37636dd742ad2af83d896ba69601ed28da51fa4e25d1ab62fde89ce413e275b", size = 15701 }, + { url = "https://files.pythonhosted.org/packages/43/59/b98e84eebf745ffc75397eaad4763795bff8a30cbf2373a50ed4e70646c5/opentelemetry_instrumentation_httpx-0.60b1-py3-none-any.whl", hash = "sha256:f37636dd742ad2af83d896ba69601ed28da51fa4e25d1ab62fde89ce413e275b", size = 15701, upload-time = "2025-12-11T13:36:04.56Z" }, ] [[package]] @@ -1747,9 +1724,9 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "protobuf" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/49/1d/f25d76d8260c156c40c97c9ed4511ec0f9ce353f8108ca6e7561f82a06b2/opentelemetry_proto-1.39.1.tar.gz", hash = "sha256:6c8e05144fc0d3ed4d22c2289c6b126e03bcd0e6a7da0f16cedd2e1c2772e2c8", size = 46152 } +sdist = { url = "https://files.pythonhosted.org/packages/49/1d/f25d76d8260c156c40c97c9ed4511ec0f9ce353f8108ca6e7561f82a06b2/opentelemetry_proto-1.39.1.tar.gz", hash = "sha256:6c8e05144fc0d3ed4d22c2289c6b126e03bcd0e6a7da0f16cedd2e1c2772e2c8", size = 46152, upload-time = "2025-12-11T13:32:48.681Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/51/95/b40c96a7b5203005a0b03d8ce8cd212ff23f1793d5ba289c87a097571b18/opentelemetry_proto-1.39.1-py3-none-any.whl", hash = "sha256:22cdc78efd3b3765d09e68bfbd010d4fc254c9818afd0b6b423387d9dee46007", size = 72535 }, + { url = "https://files.pythonhosted.org/packages/51/95/b40c96a7b5203005a0b03d8ce8cd212ff23f1793d5ba289c87a097571b18/opentelemetry_proto-1.39.1-py3-none-any.whl", hash = "sha256:22cdc78efd3b3765d09e68bfbd010d4fc254c9818afd0b6b423387d9dee46007", size = 72535, upload-time = "2025-12-11T13:32:33.866Z" }, ] [[package]] @@ -1761,9 +1738,9 @@ dependencies = [ { name = "opentelemetry-semantic-conventions" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/eb/fb/c76080c9ba07e1e8235d24cdcc4d125ef7aa3edf23eb4e497c2e50889adc/opentelemetry_sdk-1.39.1.tar.gz", hash = "sha256:cf4d4563caf7bff906c9f7967e2be22d0d6b349b908be0d90fb21c8e9c995cc6", size = 171460 } +sdist = { url = "https://files.pythonhosted.org/packages/eb/fb/c76080c9ba07e1e8235d24cdcc4d125ef7aa3edf23eb4e497c2e50889adc/opentelemetry_sdk-1.39.1.tar.gz", hash = "sha256:cf4d4563caf7bff906c9f7967e2be22d0d6b349b908be0d90fb21c8e9c995cc6", size = 171460, upload-time = "2025-12-11T13:32:49.369Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/7c/98/e91cf858f203d86f4eccdf763dcf01cf03f1dae80c3750f7e635bfa206b6/opentelemetry_sdk-1.39.1-py3-none-any.whl", hash = "sha256:4d5482c478513ecb0a5d938dcc61394e647066e0cc2676bee9f3af3f3f45f01c", size = 132565 }, + { url = "https://files.pythonhosted.org/packages/7c/98/e91cf858f203d86f4eccdf763dcf01cf03f1dae80c3750f7e635bfa206b6/opentelemetry_sdk-1.39.1-py3-none-any.whl", hash = "sha256:4d5482c478513ecb0a5d938dcc61394e647066e0cc2676bee9f3af3f3f45f01c", size = 132565, upload-time = "2025-12-11T13:32:35.069Z" }, ] [[package]] @@ -1774,63 +1751,63 @@ dependencies = [ { name = "opentelemetry-api" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/91/df/553f93ed38bf22f4b999d9be9c185adb558982214f33eae539d3b5cd0858/opentelemetry_semantic_conventions-0.60b1.tar.gz", hash = "sha256:87c228b5a0669b748c76d76df6c364c369c28f1c465e50f661e39737e84bc953", size = 137935 } +sdist = { url = "https://files.pythonhosted.org/packages/91/df/553f93ed38bf22f4b999d9be9c185adb558982214f33eae539d3b5cd0858/opentelemetry_semantic_conventions-0.60b1.tar.gz", hash = "sha256:87c228b5a0669b748c76d76df6c364c369c28f1c465e50f661e39737e84bc953", size = 137935, upload-time = "2025-12-11T13:32:50.487Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/7a/5e/5958555e09635d09b75de3c4f8b9cae7335ca545d77392ffe7331534c402/opentelemetry_semantic_conventions-0.60b1-py3-none-any.whl", hash = "sha256:9fa8c8b0c110da289809292b0591220d3a7b53c1526a23021e977d68597893fb", size = 219982 }, + { url = "https://files.pythonhosted.org/packages/7a/5e/5958555e09635d09b75de3c4f8b9cae7335ca545d77392ffe7331534c402/opentelemetry_semantic_conventions-0.60b1-py3-none-any.whl", hash = "sha256:9fa8c8b0c110da289809292b0591220d3a7b53c1526a23021e977d68597893fb", size = 219982, upload-time = "2025-12-11T13:32:36.955Z" }, ] [[package]] name = "opentelemetry-util-http" version = "0.60b1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/50/fc/c47bb04a1d8a941a4061307e1eddfa331ed4d0ab13d8a9781e6db256940a/opentelemetry_util_http-0.60b1.tar.gz", hash = "sha256:0d97152ca8c8a41ced7172d29d3622a219317f74ae6bb3027cfbdcf22c3cc0d6", size = 11053 } +sdist = { url = "https://files.pythonhosted.org/packages/50/fc/c47bb04a1d8a941a4061307e1eddfa331ed4d0ab13d8a9781e6db256940a/opentelemetry_util_http-0.60b1.tar.gz", hash = "sha256:0d97152ca8c8a41ced7172d29d3622a219317f74ae6bb3027cfbdcf22c3cc0d6", size = 11053, upload-time = "2025-12-11T13:37:25.115Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/16/5c/d3f1733665f7cd582ef0842fb1d2ed0bc1fba10875160593342d22bba375/opentelemetry_util_http-0.60b1-py3-none-any.whl", hash = "sha256:66381ba28550c91bee14dcba8979ace443444af1ed609226634596b4b0faf199", size = 8947 }, + { url = "https://files.pythonhosted.org/packages/16/5c/d3f1733665f7cd582ef0842fb1d2ed0bc1fba10875160593342d22bba375/opentelemetry_util_http-0.60b1-py3-none-any.whl", hash = "sha256:66381ba28550c91bee14dcba8979ace443444af1ed609226634596b4b0faf199", size = 8947, upload-time = "2025-12-11T13:36:37.151Z" }, ] [[package]] name = "packaging" version = "25.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727 } +sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469 }, + { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" }, ] [[package]] name = "pathable" version = "0.4.4" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/67/93/8f2c2075b180c12c1e9f6a09d1a985bc2036906b13dff1d8917e395f2048/pathable-0.4.4.tar.gz", hash = "sha256:6905a3cd17804edfac7875b5f6c9142a218c7caef78693c2dbbbfbac186d88b2", size = 8124 } +sdist = { url = "https://files.pythonhosted.org/packages/67/93/8f2c2075b180c12c1e9f6a09d1a985bc2036906b13dff1d8917e395f2048/pathable-0.4.4.tar.gz", hash = "sha256:6905a3cd17804edfac7875b5f6c9142a218c7caef78693c2dbbbfbac186d88b2", size = 8124, upload-time = "2025-01-10T18:43:13.247Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/7d/eb/b6260b31b1a96386c0a880edebe26f89669098acea8e0318bff6adb378fd/pathable-0.4.4-py3-none-any.whl", hash = "sha256:5ae9e94793b6ef5a4cbe0a7ce9dbbefc1eec38df253763fd0aeeacf2762dbbc2", size = 9592 }, + { url = "https://files.pythonhosted.org/packages/7d/eb/b6260b31b1a96386c0a880edebe26f89669098acea8e0318bff6adb378fd/pathable-0.4.4-py3-none-any.whl", hash = "sha256:5ae9e94793b6ef5a4cbe0a7ce9dbbefc1eec38df253763fd0aeeacf2762dbbc2", size = 9592, upload-time = "2025-01-10T18:43:11.88Z" }, ] [[package]] name = "pathvalidate" version = "3.3.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/fa/2a/52a8da6fe965dea6192eb716b357558e103aea0a1e9a8352ad575a8406ca/pathvalidate-3.3.1.tar.gz", hash = "sha256:b18c07212bfead624345bb8e1d6141cdcf15a39736994ea0b94035ad2b1ba177", size = 63262 } +sdist = { url = "https://files.pythonhosted.org/packages/fa/2a/52a8da6fe965dea6192eb716b357558e103aea0a1e9a8352ad575a8406ca/pathvalidate-3.3.1.tar.gz", hash = "sha256:b18c07212bfead624345bb8e1d6141cdcf15a39736994ea0b94035ad2b1ba177", size = 63262, upload-time = "2025-06-15T09:07:20.736Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/9a/70/875f4a23bfc4731703a5835487d0d2fb999031bd415e7d17c0ae615c18b7/pathvalidate-3.3.1-py3-none-any.whl", hash = "sha256:5263baab691f8e1af96092fa5137ee17df5bdfbd6cff1fcac4d6ef4bc2e1735f", size = 24305 }, + { url = "https://files.pythonhosted.org/packages/9a/70/875f4a23bfc4731703a5835487d0d2fb999031bd415e7d17c0ae615c18b7/pathvalidate-3.3.1-py3-none-any.whl", hash = "sha256:5263baab691f8e1af96092fa5137ee17df5bdfbd6cff1fcac4d6ef4bc2e1735f", size = 24305, upload-time = "2025-06-15T09:07:19.117Z" }, ] [[package]] name = "platformdirs" version = "4.5.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/cf/86/0248f086a84f01b37aaec0fa567b397df1a119f73c16f6c7a9aac73ea309/platformdirs-4.5.1.tar.gz", hash = "sha256:61d5cdcc6065745cdd94f0f878977f8de9437be93de97c1c12f853c9c0cdcbda", size = 21715 } +sdist = { url = "https://files.pythonhosted.org/packages/cf/86/0248f086a84f01b37aaec0fa567b397df1a119f73c16f6c7a9aac73ea309/platformdirs-4.5.1.tar.gz", hash = "sha256:61d5cdcc6065745cdd94f0f878977f8de9437be93de97c1c12f853c9c0cdcbda", size = 21715, upload-time = "2025-12-05T13:52:58.638Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/cb/28/3bfe2fa5a7b9c46fe7e13c97bda14c895fb10fa2ebf1d0abb90e0cea7ee1/platformdirs-4.5.1-py3-none-any.whl", hash = "sha256:d03afa3963c806a9bed9d5125c8f4cb2fdaf74a55ab60e5d59b3fde758104d31", size = 18731 }, + { url = "https://files.pythonhosted.org/packages/cb/28/3bfe2fa5a7b9c46fe7e13c97bda14c895fb10fa2ebf1d0abb90e0cea7ee1/platformdirs-4.5.1-py3-none-any.whl", hash = "sha256:d03afa3963c806a9bed9d5125c8f4cb2fdaf74a55ab60e5d59b3fde758104d31", size = 18731, upload-time = "2025-12-05T13:52:56.823Z" }, ] [[package]] name = "prometheus-client" version = "0.23.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/23/53/3edb5d68ecf6b38fcbcc1ad28391117d2a322d9a1a3eff04bfdb184d8c3b/prometheus_client-0.23.1.tar.gz", hash = "sha256:6ae8f9081eaaaf153a2e959d2e6c4f4fb57b12ef76c8c7980202f1e57b48b2ce", size = 80481 } +sdist = { url = "https://files.pythonhosted.org/packages/23/53/3edb5d68ecf6b38fcbcc1ad28391117d2a322d9a1a3eff04bfdb184d8c3b/prometheus_client-0.23.1.tar.gz", hash = "sha256:6ae8f9081eaaaf153a2e959d2e6c4f4fb57b12ef76c8c7980202f1e57b48b2ce", size = 80481, upload-time = "2025-09-18T20:47:25.043Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b8/db/14bafcb4af2139e046d03fd00dea7873e48eafe18b7d2797e73d6681f210/prometheus_client-0.23.1-py3-none-any.whl", hash = "sha256:dd1913e6e76b59cfe44e7a4b83e01afc9873c1bdfd2ed8739f1e76aeca115f99", size = 61145 }, + { url = "https://files.pythonhosted.org/packages/b8/db/14bafcb4af2139e046d03fd00dea7873e48eafe18b7d2797e73d6681f210/prometheus_client-0.23.1-py3-none-any.whl", hash = "sha256:dd1913e6e76b59cfe44e7a4b83e01afc9873c1bdfd2ed8739f1e76aeca115f99", size = 61145, upload-time = "2025-09-18T20:47:23.875Z" }, ] [[package]] @@ -1840,93 +1817,93 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "wcwidth" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a1/96/06e01a7b38dce6fe1db213e061a4602dd6032a8a97ef6c1a862537732421/prompt_toolkit-3.0.52.tar.gz", hash = "sha256:28cde192929c8e7321de85de1ddbe736f1375148b02f2e17edd840042b1be855", size = 434198 } +sdist = { url = "https://files.pythonhosted.org/packages/a1/96/06e01a7b38dce6fe1db213e061a4602dd6032a8a97ef6c1a862537732421/prompt_toolkit-3.0.52.tar.gz", hash = "sha256:28cde192929c8e7321de85de1ddbe736f1375148b02f2e17edd840042b1be855", size = 434198, upload-time = "2025-08-27T15:24:02.057Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/84/03/0d3ce49e2505ae70cf43bc5bb3033955d2fc9f932163e84dc0779cc47f48/prompt_toolkit-3.0.52-py3-none-any.whl", hash = "sha256:9aac639a3bbd33284347de5ad8d68ecc044b91a762dc39b7c21095fcd6a19955", size = 391431 }, + { url = "https://files.pythonhosted.org/packages/84/03/0d3ce49e2505ae70cf43bc5bb3033955d2fc9f932163e84dc0779cc47f48/prompt_toolkit-3.0.52-py3-none-any.whl", hash = "sha256:9aac639a3bbd33284347de5ad8d68ecc044b91a762dc39b7c21095fcd6a19955", size = 391431, upload-time = "2025-08-27T15:23:59.498Z" }, ] [[package]] name = "propcache" version = "0.4.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/9e/da/e9fc233cf63743258bff22b3dfa7ea5baef7b5bc324af47a0ad89b8ffc6f/propcache-0.4.1.tar.gz", hash = "sha256:f48107a8c637e80362555f37ecf49abe20370e557cc4ab374f04ec4423c97c3d", size = 46442 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/bf/df/6d9c1b6ac12b003837dde8a10231a7344512186e87b36e855bef32241942/propcache-0.4.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:43eedf29202c08550aac1d14e0ee619b0430aaef78f85864c1a892294fbc28cf", size = 77750 }, - { url = "https://files.pythonhosted.org/packages/8b/e8/677a0025e8a2acf07d3418a2e7ba529c9c33caf09d3c1f25513023c1db56/propcache-0.4.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d62cdfcfd89ccb8de04e0eda998535c406bf5e060ffd56be6c586cbcc05b3311", size = 44780 }, - { url = "https://files.pythonhosted.org/packages/89/a4/92380f7ca60f99ebae761936bc48a72a639e8a47b29050615eef757cb2a7/propcache-0.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cae65ad55793da34db5f54e4029b89d3b9b9490d8abe1b4c7ab5d4b8ec7ebf74", size = 46308 }, - { url = "https://files.pythonhosted.org/packages/2d/48/c5ac64dee5262044348d1d78a5f85dd1a57464a60d30daee946699963eb3/propcache-0.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:333ddb9031d2704a301ee3e506dc46b1fe5f294ec198ed6435ad5b6a085facfe", size = 208182 }, - { url = "https://files.pythonhosted.org/packages/c6/0c/cd762dd011a9287389a6a3eb43aa30207bde253610cca06824aeabfe9653/propcache-0.4.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:fd0858c20f078a32cf55f7e81473d96dcf3b93fd2ccdb3d40fdf54b8573df3af", size = 211215 }, - { url = "https://files.pythonhosted.org/packages/30/3e/49861e90233ba36890ae0ca4c660e95df565b2cd15d4a68556ab5865974e/propcache-0.4.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:678ae89ebc632c5c204c794f8dab2837c5f159aeb59e6ed0539500400577298c", size = 218112 }, - { url = "https://files.pythonhosted.org/packages/f1/8b/544bc867e24e1bd48f3118cecd3b05c694e160a168478fa28770f22fd094/propcache-0.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d472aeb4fbf9865e0c6d622d7f4d54a4e101a89715d8904282bb5f9a2f476c3f", size = 204442 }, - { url = "https://files.pythonhosted.org/packages/50/a6/4282772fd016a76d3e5c0df58380a5ea64900afd836cec2c2f662d1b9bb3/propcache-0.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4d3df5fa7e36b3225954fba85589da77a0fe6a53e3976de39caf04a0db4c36f1", size = 199398 }, - { url = "https://files.pythonhosted.org/packages/3e/ec/d8a7cd406ee1ddb705db2139f8a10a8a427100347bd698e7014351c7af09/propcache-0.4.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:ee17f18d2498f2673e432faaa71698032b0127ebf23ae5974eeaf806c279df24", size = 196920 }, - { url = "https://files.pythonhosted.org/packages/f6/6c/f38ab64af3764f431e359f8baf9e0a21013e24329e8b85d2da32e8ed07ca/propcache-0.4.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:580e97762b950f993ae618e167e7be9256b8353c2dcd8b99ec100eb50f5286aa", size = 203748 }, - { url = "https://files.pythonhosted.org/packages/d6/e3/fa846bd70f6534d647886621388f0a265254d30e3ce47e5c8e6e27dbf153/propcache-0.4.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:501d20b891688eb8e7aa903021f0b72d5a55db40ffaab27edefd1027caaafa61", size = 205877 }, - { url = "https://files.pythonhosted.org/packages/e2/39/8163fc6f3133fea7b5f2827e8eba2029a0277ab2c5beee6c1db7b10fc23d/propcache-0.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9a0bd56e5b100aef69bd8562b74b46254e7c8812918d3baa700c8a8009b0af66", size = 199437 }, - { url = "https://files.pythonhosted.org/packages/93/89/caa9089970ca49c7c01662bd0eeedfe85494e863e8043565aeb6472ce8fe/propcache-0.4.1-cp313-cp313-win32.whl", hash = "sha256:bcc9aaa5d80322bc2fb24bb7accb4a30f81e90ab8d6ba187aec0744bc302ad81", size = 37586 }, - { url = "https://files.pythonhosted.org/packages/f5/ab/f76ec3c3627c883215b5c8080debb4394ef5a7a29be811f786415fc1e6fd/propcache-0.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:381914df18634f5494334d201e98245c0596067504b9372d8cf93f4bb23e025e", size = 40790 }, - { url = "https://files.pythonhosted.org/packages/59/1b/e71ae98235f8e2ba5004d8cb19765a74877abf189bc53fc0c80d799e56c3/propcache-0.4.1-cp313-cp313-win_arm64.whl", hash = "sha256:8873eb4460fd55333ea49b7d189749ecf6e55bf85080f11b1c4530ed3034cba1", size = 37158 }, - { url = "https://files.pythonhosted.org/packages/83/ce/a31bbdfc24ee0dcbba458c8175ed26089cf109a55bbe7b7640ed2470cfe9/propcache-0.4.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:92d1935ee1f8d7442da9c0c4fa7ac20d07e94064184811b685f5c4fada64553b", size = 81451 }, - { url = "https://files.pythonhosted.org/packages/25/9c/442a45a470a68456e710d96cacd3573ef26a1d0a60067e6a7d5e655621ed/propcache-0.4.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:473c61b39e1460d386479b9b2f337da492042447c9b685f28be4f74d3529e566", size = 46374 }, - { url = "https://files.pythonhosted.org/packages/f4/bf/b1d5e21dbc3b2e889ea4327044fb16312a736d97640fb8b6aa3f9c7b3b65/propcache-0.4.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:c0ef0aaafc66fbd87842a3fe3902fd889825646bc21149eafe47be6072725835", size = 48396 }, - { url = "https://files.pythonhosted.org/packages/f4/04/5b4c54a103d480e978d3c8a76073502b18db0c4bc17ab91b3cb5092ad949/propcache-0.4.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f95393b4d66bfae908c3ca8d169d5f79cd65636ae15b5e7a4f6e67af675adb0e", size = 275950 }, - { url = "https://files.pythonhosted.org/packages/b4/c1/86f846827fb969c4b78b0af79bba1d1ea2156492e1b83dea8b8a6ae27395/propcache-0.4.1-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c07fda85708bc48578467e85099645167a955ba093be0a2dcba962195676e859", size = 273856 }, - { url = "https://files.pythonhosted.org/packages/36/1d/fc272a63c8d3bbad6878c336c7a7dea15e8f2d23a544bda43205dfa83ada/propcache-0.4.1-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:af223b406d6d000830c6f65f1e6431783fc3f713ba3e6cc8c024d5ee96170a4b", size = 280420 }, - { url = "https://files.pythonhosted.org/packages/07/0c/01f2219d39f7e53d52e5173bcb09c976609ba30209912a0680adfb8c593a/propcache-0.4.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a78372c932c90ee474559c5ddfffd718238e8673c340dc21fe45c5b8b54559a0", size = 263254 }, - { url = "https://files.pythonhosted.org/packages/2d/18/cd28081658ce597898f0c4d174d4d0f3c5b6d4dc27ffafeef835c95eb359/propcache-0.4.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:564d9f0d4d9509e1a870c920a89b2fec951b44bf5ba7d537a9e7c1ccec2c18af", size = 261205 }, - { url = "https://files.pythonhosted.org/packages/7a/71/1f9e22eb8b8316701c2a19fa1f388c8a3185082607da8e406a803c9b954e/propcache-0.4.1-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:17612831fda0138059cc5546f4d12a2aacfb9e47068c06af35c400ba58ba7393", size = 247873 }, - { url = "https://files.pythonhosted.org/packages/4a/65/3d4b61f36af2b4eddba9def857959f1016a51066b4f1ce348e0cf7881f58/propcache-0.4.1-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:41a89040cb10bd345b3c1a873b2bf36413d48da1def52f268a055f7398514874", size = 262739 }, - { url = "https://files.pythonhosted.org/packages/2a/42/26746ab087faa77c1c68079b228810436ccd9a5ce9ac85e2b7307195fd06/propcache-0.4.1-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:e35b88984e7fa64aacecea39236cee32dd9bd8c55f57ba8a75cf2399553f9bd7", size = 263514 }, - { url = "https://files.pythonhosted.org/packages/94/13/630690fe201f5502d2403dd3cfd451ed8858fe3c738ee88d095ad2ff407b/propcache-0.4.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6f8b465489f927b0df505cbe26ffbeed4d6d8a2bbc61ce90eb074ff129ef0ab1", size = 257781 }, - { url = "https://files.pythonhosted.org/packages/92/f7/1d4ec5841505f423469efbfc381d64b7b467438cd5a4bbcbb063f3b73d27/propcache-0.4.1-cp313-cp313t-win32.whl", hash = "sha256:2ad890caa1d928c7c2965b48f3a3815c853180831d0e5503d35cf00c472f4717", size = 41396 }, - { url = "https://files.pythonhosted.org/packages/48/f0/615c30622316496d2cbbc29f5985f7777d3ada70f23370608c1d3e081c1f/propcache-0.4.1-cp313-cp313t-win_amd64.whl", hash = "sha256:f7ee0e597f495cf415bcbd3da3caa3bd7e816b74d0d52b8145954c5e6fd3ff37", size = 44897 }, - { url = "https://files.pythonhosted.org/packages/fd/ca/6002e46eccbe0e33dcd4069ef32f7f1c9e243736e07adca37ae8c4830ec3/propcache-0.4.1-cp313-cp313t-win_arm64.whl", hash = "sha256:929d7cbe1f01bb7baffb33dc14eb5691c95831450a26354cd210a8155170c93a", size = 39789 }, - { url = "https://files.pythonhosted.org/packages/8e/5c/bca52d654a896f831b8256683457ceddd490ec18d9ec50e97dfd8fc726a8/propcache-0.4.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3f7124c9d820ba5548d431afb4632301acf965db49e666aa21c305cbe8c6de12", size = 78152 }, - { url = "https://files.pythonhosted.org/packages/65/9b/03b04e7d82a5f54fb16113d839f5ea1ede58a61e90edf515f6577c66fa8f/propcache-0.4.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:c0d4b719b7da33599dfe3b22d3db1ef789210a0597bc650b7cee9c77c2be8c5c", size = 44869 }, - { url = "https://files.pythonhosted.org/packages/b2/fa/89a8ef0468d5833a23fff277b143d0573897cf75bd56670a6d28126c7d68/propcache-0.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:9f302f4783709a78240ebc311b793f123328716a60911d667e0c036bc5dcbded", size = 46596 }, - { url = "https://files.pythonhosted.org/packages/86/bd/47816020d337f4a746edc42fe8d53669965138f39ee117414c7d7a340cfe/propcache-0.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c80ee5802e3fb9ea37938e7eecc307fb984837091d5fd262bb37238b1ae97641", size = 206981 }, - { url = "https://files.pythonhosted.org/packages/df/f6/c5fa1357cc9748510ee55f37173eb31bfde6d94e98ccd9e6f033f2fc06e1/propcache-0.4.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ed5a841e8bb29a55fb8159ed526b26adc5bdd7e8bd7bf793ce647cb08656cdf4", size = 211490 }, - { url = "https://files.pythonhosted.org/packages/80/1e/e5889652a7c4a3846683401a48f0f2e5083ce0ec1a8a5221d8058fbd1adf/propcache-0.4.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:55c72fd6ea2da4c318e74ffdf93c4fe4e926051133657459131a95c846d16d44", size = 215371 }, - { url = "https://files.pythonhosted.org/packages/b2/f2/889ad4b2408f72fe1a4f6a19491177b30ea7bf1a0fd5f17050ca08cfc882/propcache-0.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8326e144341460402713f91df60ade3c999d601e7eb5ff8f6f7862d54de0610d", size = 201424 }, - { url = "https://files.pythonhosted.org/packages/27/73/033d63069b57b0812c8bd19f311faebeceb6ba31b8f32b73432d12a0b826/propcache-0.4.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:060b16ae65bc098da7f6d25bf359f1f31f688384858204fe5d652979e0015e5b", size = 197566 }, - { url = "https://files.pythonhosted.org/packages/dc/89/ce24f3dc182630b4e07aa6d15f0ff4b14ed4b9955fae95a0b54c58d66c05/propcache-0.4.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:89eb3fa9524f7bec9de6e83cf3faed9d79bffa560672c118a96a171a6f55831e", size = 193130 }, - { url = "https://files.pythonhosted.org/packages/a9/24/ef0d5fd1a811fb5c609278d0209c9f10c35f20581fcc16f818da959fc5b4/propcache-0.4.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:dee69d7015dc235f526fe80a9c90d65eb0039103fe565776250881731f06349f", size = 202625 }, - { url = "https://files.pythonhosted.org/packages/f5/02/98ec20ff5546f68d673df2f7a69e8c0d076b5abd05ca882dc7ee3a83653d/propcache-0.4.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:5558992a00dfd54ccbc64a32726a3357ec93825a418a401f5cc67df0ac5d9e49", size = 204209 }, - { url = "https://files.pythonhosted.org/packages/a0/87/492694f76759b15f0467a2a93ab68d32859672b646aa8a04ce4864e7932d/propcache-0.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c9b822a577f560fbd9554812526831712c1436d2c046cedee4c3796d3543b144", size = 197797 }, - { url = "https://files.pythonhosted.org/packages/ee/36/66367de3575db1d2d3f3d177432bd14ee577a39d3f5d1b3d5df8afe3b6e2/propcache-0.4.1-cp314-cp314-win32.whl", hash = "sha256:ab4c29b49d560fe48b696cdcb127dd36e0bc2472548f3bf56cc5cb3da2b2984f", size = 38140 }, - { url = "https://files.pythonhosted.org/packages/0c/2a/a758b47de253636e1b8aef181c0b4f4f204bf0dd964914fb2af90a95b49b/propcache-0.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:5a103c3eb905fcea0ab98be99c3a9a5ab2de60228aa5aceedc614c0281cf6153", size = 41257 }, - { url = "https://files.pythonhosted.org/packages/34/5e/63bd5896c3fec12edcbd6f12508d4890d23c265df28c74b175e1ef9f4f3b/propcache-0.4.1-cp314-cp314-win_arm64.whl", hash = "sha256:74c1fb26515153e482e00177a1ad654721bf9207da8a494a0c05e797ad27b992", size = 38097 }, - { url = "https://files.pythonhosted.org/packages/99/85/9ff785d787ccf9bbb3f3106f79884a130951436f58392000231b4c737c80/propcache-0.4.1-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:824e908bce90fb2743bd6b59db36eb4f45cd350a39637c9f73b1c1ea66f5b75f", size = 81455 }, - { url = "https://files.pythonhosted.org/packages/90/85/2431c10c8e7ddb1445c1f7c4b54d886e8ad20e3c6307e7218f05922cad67/propcache-0.4.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:c2b5e7db5328427c57c8e8831abda175421b709672f6cfc3d630c3b7e2146393", size = 46372 }, - { url = "https://files.pythonhosted.org/packages/01/20/b0972d902472da9bcb683fa595099911f4d2e86e5683bcc45de60dd05dc3/propcache-0.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6f6ff873ed40292cd4969ef5310179afd5db59fdf055897e282485043fc80ad0", size = 48411 }, - { url = "https://files.pythonhosted.org/packages/e2/e3/7dc89f4f21e8f99bad3d5ddb3a3389afcf9da4ac69e3deb2dcdc96e74169/propcache-0.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:49a2dc67c154db2c1463013594c458881a069fcf98940e61a0569016a583020a", size = 275712 }, - { url = "https://files.pythonhosted.org/packages/20/67/89800c8352489b21a8047c773067644e3897f02ecbbd610f4d46b7f08612/propcache-0.4.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:005f08e6a0529984491e37d8dbc3dd86f84bd78a8ceb5fa9a021f4c48d4984be", size = 273557 }, - { url = "https://files.pythonhosted.org/packages/e2/a1/b52b055c766a54ce6d9c16d9aca0cad8059acd9637cdf8aa0222f4a026ef/propcache-0.4.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5c3310452e0d31390da9035c348633b43d7e7feb2e37be252be6da45abd1abcc", size = 280015 }, - { url = "https://files.pythonhosted.org/packages/48/c8/33cee30bd890672c63743049f3c9e4be087e6780906bfc3ec58528be59c1/propcache-0.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4c3c70630930447f9ef1caac7728c8ad1c56bc5015338b20fed0d08ea2480b3a", size = 262880 }, - { url = "https://files.pythonhosted.org/packages/0c/b1/8f08a143b204b418285c88b83d00edbd61afbc2c6415ffafc8905da7038b/propcache-0.4.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8e57061305815dfc910a3634dcf584f08168a8836e6999983569f51a8544cd89", size = 260938 }, - { url = "https://files.pythonhosted.org/packages/cf/12/96e4664c82ca2f31e1c8dff86afb867348979eb78d3cb8546a680287a1e9/propcache-0.4.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:521a463429ef54143092c11a77e04056dd00636f72e8c45b70aaa3140d639726", size = 247641 }, - { url = "https://files.pythonhosted.org/packages/18/ed/e7a9cfca28133386ba52278136d42209d3125db08d0a6395f0cba0c0285c/propcache-0.4.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:120c964da3fdc75e3731aa392527136d4ad35868cc556fd09bb6d09172d9a367", size = 262510 }, - { url = "https://files.pythonhosted.org/packages/f5/76/16d8bf65e8845dd62b4e2b57444ab81f07f40caa5652b8969b87ddcf2ef6/propcache-0.4.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:d8f353eb14ee3441ee844ade4277d560cdd68288838673273b978e3d6d2c8f36", size = 263161 }, - { url = "https://files.pythonhosted.org/packages/e7/70/c99e9edb5d91d5ad8a49fa3c1e8285ba64f1476782fed10ab251ff413ba1/propcache-0.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ab2943be7c652f09638800905ee1bab2c544e537edb57d527997a24c13dc1455", size = 257393 }, - { url = "https://files.pythonhosted.org/packages/08/02/87b25304249a35c0915d236575bc3574a323f60b47939a2262b77632a3ee/propcache-0.4.1-cp314-cp314t-win32.whl", hash = "sha256:05674a162469f31358c30bcaa8883cb7829fa3110bf9c0991fe27d7896c42d85", size = 42546 }, - { url = "https://files.pythonhosted.org/packages/cb/ef/3c6ecf8b317aa982f309835e8f96987466123c6e596646d4e6a1dfcd080f/propcache-0.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:990f6b3e2a27d683cb7602ed6c86f15ee6b43b1194736f9baaeb93d0016633b1", size = 46259 }, - { url = "https://files.pythonhosted.org/packages/c4/2d/346e946d4951f37eca1e4f55be0f0174c52cd70720f84029b02f296f4a38/propcache-0.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:ecef2343af4cc68e05131e45024ba34f6095821988a9d0a02aa7c73fcc448aa9", size = 40428 }, - { url = "https://files.pythonhosted.org/packages/5b/5a/bc7b4a4ef808fa59a816c17b20c4bef6884daebbdf627ff2a161da67da19/propcache-0.4.1-py3-none-any.whl", hash = "sha256:af2a6052aeb6cf17d3e46ee169099044fd8224cbaf75c76a2ef596e8163e2237", size = 13305 }, +sdist = { url = "https://files.pythonhosted.org/packages/9e/da/e9fc233cf63743258bff22b3dfa7ea5baef7b5bc324af47a0ad89b8ffc6f/propcache-0.4.1.tar.gz", hash = "sha256:f48107a8c637e80362555f37ecf49abe20370e557cc4ab374f04ec4423c97c3d", size = 46442, upload-time = "2025-10-08T19:49:02.291Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bf/df/6d9c1b6ac12b003837dde8a10231a7344512186e87b36e855bef32241942/propcache-0.4.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:43eedf29202c08550aac1d14e0ee619b0430aaef78f85864c1a892294fbc28cf", size = 77750, upload-time = "2025-10-08T19:47:07.648Z" }, + { url = "https://files.pythonhosted.org/packages/8b/e8/677a0025e8a2acf07d3418a2e7ba529c9c33caf09d3c1f25513023c1db56/propcache-0.4.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d62cdfcfd89ccb8de04e0eda998535c406bf5e060ffd56be6c586cbcc05b3311", size = 44780, upload-time = "2025-10-08T19:47:08.851Z" }, + { url = "https://files.pythonhosted.org/packages/89/a4/92380f7ca60f99ebae761936bc48a72a639e8a47b29050615eef757cb2a7/propcache-0.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cae65ad55793da34db5f54e4029b89d3b9b9490d8abe1b4c7ab5d4b8ec7ebf74", size = 46308, upload-time = "2025-10-08T19:47:09.982Z" }, + { url = "https://files.pythonhosted.org/packages/2d/48/c5ac64dee5262044348d1d78a5f85dd1a57464a60d30daee946699963eb3/propcache-0.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:333ddb9031d2704a301ee3e506dc46b1fe5f294ec198ed6435ad5b6a085facfe", size = 208182, upload-time = "2025-10-08T19:47:11.319Z" }, + { url = "https://files.pythonhosted.org/packages/c6/0c/cd762dd011a9287389a6a3eb43aa30207bde253610cca06824aeabfe9653/propcache-0.4.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:fd0858c20f078a32cf55f7e81473d96dcf3b93fd2ccdb3d40fdf54b8573df3af", size = 211215, upload-time = "2025-10-08T19:47:13.146Z" }, + { url = "https://files.pythonhosted.org/packages/30/3e/49861e90233ba36890ae0ca4c660e95df565b2cd15d4a68556ab5865974e/propcache-0.4.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:678ae89ebc632c5c204c794f8dab2837c5f159aeb59e6ed0539500400577298c", size = 218112, upload-time = "2025-10-08T19:47:14.913Z" }, + { url = "https://files.pythonhosted.org/packages/f1/8b/544bc867e24e1bd48f3118cecd3b05c694e160a168478fa28770f22fd094/propcache-0.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d472aeb4fbf9865e0c6d622d7f4d54a4e101a89715d8904282bb5f9a2f476c3f", size = 204442, upload-time = "2025-10-08T19:47:16.277Z" }, + { url = "https://files.pythonhosted.org/packages/50/a6/4282772fd016a76d3e5c0df58380a5ea64900afd836cec2c2f662d1b9bb3/propcache-0.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4d3df5fa7e36b3225954fba85589da77a0fe6a53e3976de39caf04a0db4c36f1", size = 199398, upload-time = "2025-10-08T19:47:17.962Z" }, + { url = "https://files.pythonhosted.org/packages/3e/ec/d8a7cd406ee1ddb705db2139f8a10a8a427100347bd698e7014351c7af09/propcache-0.4.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:ee17f18d2498f2673e432faaa71698032b0127ebf23ae5974eeaf806c279df24", size = 196920, upload-time = "2025-10-08T19:47:19.355Z" }, + { url = "https://files.pythonhosted.org/packages/f6/6c/f38ab64af3764f431e359f8baf9e0a21013e24329e8b85d2da32e8ed07ca/propcache-0.4.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:580e97762b950f993ae618e167e7be9256b8353c2dcd8b99ec100eb50f5286aa", size = 203748, upload-time = "2025-10-08T19:47:21.338Z" }, + { url = "https://files.pythonhosted.org/packages/d6/e3/fa846bd70f6534d647886621388f0a265254d30e3ce47e5c8e6e27dbf153/propcache-0.4.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:501d20b891688eb8e7aa903021f0b72d5a55db40ffaab27edefd1027caaafa61", size = 205877, upload-time = "2025-10-08T19:47:23.059Z" }, + { url = "https://files.pythonhosted.org/packages/e2/39/8163fc6f3133fea7b5f2827e8eba2029a0277ab2c5beee6c1db7b10fc23d/propcache-0.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9a0bd56e5b100aef69bd8562b74b46254e7c8812918d3baa700c8a8009b0af66", size = 199437, upload-time = "2025-10-08T19:47:24.445Z" }, + { url = "https://files.pythonhosted.org/packages/93/89/caa9089970ca49c7c01662bd0eeedfe85494e863e8043565aeb6472ce8fe/propcache-0.4.1-cp313-cp313-win32.whl", hash = "sha256:bcc9aaa5d80322bc2fb24bb7accb4a30f81e90ab8d6ba187aec0744bc302ad81", size = 37586, upload-time = "2025-10-08T19:47:25.736Z" }, + { url = "https://files.pythonhosted.org/packages/f5/ab/f76ec3c3627c883215b5c8080debb4394ef5a7a29be811f786415fc1e6fd/propcache-0.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:381914df18634f5494334d201e98245c0596067504b9372d8cf93f4bb23e025e", size = 40790, upload-time = "2025-10-08T19:47:26.847Z" }, + { url = "https://files.pythonhosted.org/packages/59/1b/e71ae98235f8e2ba5004d8cb19765a74877abf189bc53fc0c80d799e56c3/propcache-0.4.1-cp313-cp313-win_arm64.whl", hash = "sha256:8873eb4460fd55333ea49b7d189749ecf6e55bf85080f11b1c4530ed3034cba1", size = 37158, upload-time = "2025-10-08T19:47:27.961Z" }, + { url = "https://files.pythonhosted.org/packages/83/ce/a31bbdfc24ee0dcbba458c8175ed26089cf109a55bbe7b7640ed2470cfe9/propcache-0.4.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:92d1935ee1f8d7442da9c0c4fa7ac20d07e94064184811b685f5c4fada64553b", size = 81451, upload-time = "2025-10-08T19:47:29.445Z" }, + { url = "https://files.pythonhosted.org/packages/25/9c/442a45a470a68456e710d96cacd3573ef26a1d0a60067e6a7d5e655621ed/propcache-0.4.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:473c61b39e1460d386479b9b2f337da492042447c9b685f28be4f74d3529e566", size = 46374, upload-time = "2025-10-08T19:47:30.579Z" }, + { url = "https://files.pythonhosted.org/packages/f4/bf/b1d5e21dbc3b2e889ea4327044fb16312a736d97640fb8b6aa3f9c7b3b65/propcache-0.4.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:c0ef0aaafc66fbd87842a3fe3902fd889825646bc21149eafe47be6072725835", size = 48396, upload-time = "2025-10-08T19:47:31.79Z" }, + { url = "https://files.pythonhosted.org/packages/f4/04/5b4c54a103d480e978d3c8a76073502b18db0c4bc17ab91b3cb5092ad949/propcache-0.4.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f95393b4d66bfae908c3ca8d169d5f79cd65636ae15b5e7a4f6e67af675adb0e", size = 275950, upload-time = "2025-10-08T19:47:33.481Z" }, + { url = "https://files.pythonhosted.org/packages/b4/c1/86f846827fb969c4b78b0af79bba1d1ea2156492e1b83dea8b8a6ae27395/propcache-0.4.1-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c07fda85708bc48578467e85099645167a955ba093be0a2dcba962195676e859", size = 273856, upload-time = "2025-10-08T19:47:34.906Z" }, + { url = "https://files.pythonhosted.org/packages/36/1d/fc272a63c8d3bbad6878c336c7a7dea15e8f2d23a544bda43205dfa83ada/propcache-0.4.1-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:af223b406d6d000830c6f65f1e6431783fc3f713ba3e6cc8c024d5ee96170a4b", size = 280420, upload-time = "2025-10-08T19:47:36.338Z" }, + { url = "https://files.pythonhosted.org/packages/07/0c/01f2219d39f7e53d52e5173bcb09c976609ba30209912a0680adfb8c593a/propcache-0.4.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a78372c932c90ee474559c5ddfffd718238e8673c340dc21fe45c5b8b54559a0", size = 263254, upload-time = "2025-10-08T19:47:37.692Z" }, + { url = "https://files.pythonhosted.org/packages/2d/18/cd28081658ce597898f0c4d174d4d0f3c5b6d4dc27ffafeef835c95eb359/propcache-0.4.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:564d9f0d4d9509e1a870c920a89b2fec951b44bf5ba7d537a9e7c1ccec2c18af", size = 261205, upload-time = "2025-10-08T19:47:39.659Z" }, + { url = "https://files.pythonhosted.org/packages/7a/71/1f9e22eb8b8316701c2a19fa1f388c8a3185082607da8e406a803c9b954e/propcache-0.4.1-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:17612831fda0138059cc5546f4d12a2aacfb9e47068c06af35c400ba58ba7393", size = 247873, upload-time = "2025-10-08T19:47:41.084Z" }, + { url = "https://files.pythonhosted.org/packages/4a/65/3d4b61f36af2b4eddba9def857959f1016a51066b4f1ce348e0cf7881f58/propcache-0.4.1-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:41a89040cb10bd345b3c1a873b2bf36413d48da1def52f268a055f7398514874", size = 262739, upload-time = "2025-10-08T19:47:42.51Z" }, + { url = "https://files.pythonhosted.org/packages/2a/42/26746ab087faa77c1c68079b228810436ccd9a5ce9ac85e2b7307195fd06/propcache-0.4.1-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:e35b88984e7fa64aacecea39236cee32dd9bd8c55f57ba8a75cf2399553f9bd7", size = 263514, upload-time = "2025-10-08T19:47:43.927Z" }, + { url = "https://files.pythonhosted.org/packages/94/13/630690fe201f5502d2403dd3cfd451ed8858fe3c738ee88d095ad2ff407b/propcache-0.4.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6f8b465489f927b0df505cbe26ffbeed4d6d8a2bbc61ce90eb074ff129ef0ab1", size = 257781, upload-time = "2025-10-08T19:47:45.448Z" }, + { url = "https://files.pythonhosted.org/packages/92/f7/1d4ec5841505f423469efbfc381d64b7b467438cd5a4bbcbb063f3b73d27/propcache-0.4.1-cp313-cp313t-win32.whl", hash = "sha256:2ad890caa1d928c7c2965b48f3a3815c853180831d0e5503d35cf00c472f4717", size = 41396, upload-time = "2025-10-08T19:47:47.202Z" }, + { url = "https://files.pythonhosted.org/packages/48/f0/615c30622316496d2cbbc29f5985f7777d3ada70f23370608c1d3e081c1f/propcache-0.4.1-cp313-cp313t-win_amd64.whl", hash = "sha256:f7ee0e597f495cf415bcbd3da3caa3bd7e816b74d0d52b8145954c5e6fd3ff37", size = 44897, upload-time = "2025-10-08T19:47:48.336Z" }, + { url = "https://files.pythonhosted.org/packages/fd/ca/6002e46eccbe0e33dcd4069ef32f7f1c9e243736e07adca37ae8c4830ec3/propcache-0.4.1-cp313-cp313t-win_arm64.whl", hash = "sha256:929d7cbe1f01bb7baffb33dc14eb5691c95831450a26354cd210a8155170c93a", size = 39789, upload-time = "2025-10-08T19:47:49.876Z" }, + { url = "https://files.pythonhosted.org/packages/8e/5c/bca52d654a896f831b8256683457ceddd490ec18d9ec50e97dfd8fc726a8/propcache-0.4.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3f7124c9d820ba5548d431afb4632301acf965db49e666aa21c305cbe8c6de12", size = 78152, upload-time = "2025-10-08T19:47:51.051Z" }, + { url = "https://files.pythonhosted.org/packages/65/9b/03b04e7d82a5f54fb16113d839f5ea1ede58a61e90edf515f6577c66fa8f/propcache-0.4.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:c0d4b719b7da33599dfe3b22d3db1ef789210a0597bc650b7cee9c77c2be8c5c", size = 44869, upload-time = "2025-10-08T19:47:52.594Z" }, + { url = "https://files.pythonhosted.org/packages/b2/fa/89a8ef0468d5833a23fff277b143d0573897cf75bd56670a6d28126c7d68/propcache-0.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:9f302f4783709a78240ebc311b793f123328716a60911d667e0c036bc5dcbded", size = 46596, upload-time = "2025-10-08T19:47:54.073Z" }, + { url = "https://files.pythonhosted.org/packages/86/bd/47816020d337f4a746edc42fe8d53669965138f39ee117414c7d7a340cfe/propcache-0.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c80ee5802e3fb9ea37938e7eecc307fb984837091d5fd262bb37238b1ae97641", size = 206981, upload-time = "2025-10-08T19:47:55.715Z" }, + { url = "https://files.pythonhosted.org/packages/df/f6/c5fa1357cc9748510ee55f37173eb31bfde6d94e98ccd9e6f033f2fc06e1/propcache-0.4.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ed5a841e8bb29a55fb8159ed526b26adc5bdd7e8bd7bf793ce647cb08656cdf4", size = 211490, upload-time = "2025-10-08T19:47:57.499Z" }, + { url = "https://files.pythonhosted.org/packages/80/1e/e5889652a7c4a3846683401a48f0f2e5083ce0ec1a8a5221d8058fbd1adf/propcache-0.4.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:55c72fd6ea2da4c318e74ffdf93c4fe4e926051133657459131a95c846d16d44", size = 215371, upload-time = "2025-10-08T19:47:59.317Z" }, + { url = "https://files.pythonhosted.org/packages/b2/f2/889ad4b2408f72fe1a4f6a19491177b30ea7bf1a0fd5f17050ca08cfc882/propcache-0.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8326e144341460402713f91df60ade3c999d601e7eb5ff8f6f7862d54de0610d", size = 201424, upload-time = "2025-10-08T19:48:00.67Z" }, + { url = "https://files.pythonhosted.org/packages/27/73/033d63069b57b0812c8bd19f311faebeceb6ba31b8f32b73432d12a0b826/propcache-0.4.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:060b16ae65bc098da7f6d25bf359f1f31f688384858204fe5d652979e0015e5b", size = 197566, upload-time = "2025-10-08T19:48:02.604Z" }, + { url = "https://files.pythonhosted.org/packages/dc/89/ce24f3dc182630b4e07aa6d15f0ff4b14ed4b9955fae95a0b54c58d66c05/propcache-0.4.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:89eb3fa9524f7bec9de6e83cf3faed9d79bffa560672c118a96a171a6f55831e", size = 193130, upload-time = "2025-10-08T19:48:04.499Z" }, + { url = "https://files.pythonhosted.org/packages/a9/24/ef0d5fd1a811fb5c609278d0209c9f10c35f20581fcc16f818da959fc5b4/propcache-0.4.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:dee69d7015dc235f526fe80a9c90d65eb0039103fe565776250881731f06349f", size = 202625, upload-time = "2025-10-08T19:48:06.213Z" }, + { url = "https://files.pythonhosted.org/packages/f5/02/98ec20ff5546f68d673df2f7a69e8c0d076b5abd05ca882dc7ee3a83653d/propcache-0.4.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:5558992a00dfd54ccbc64a32726a3357ec93825a418a401f5cc67df0ac5d9e49", size = 204209, upload-time = "2025-10-08T19:48:08.432Z" }, + { url = "https://files.pythonhosted.org/packages/a0/87/492694f76759b15f0467a2a93ab68d32859672b646aa8a04ce4864e7932d/propcache-0.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c9b822a577f560fbd9554812526831712c1436d2c046cedee4c3796d3543b144", size = 197797, upload-time = "2025-10-08T19:48:09.968Z" }, + { url = "https://files.pythonhosted.org/packages/ee/36/66367de3575db1d2d3f3d177432bd14ee577a39d3f5d1b3d5df8afe3b6e2/propcache-0.4.1-cp314-cp314-win32.whl", hash = "sha256:ab4c29b49d560fe48b696cdcb127dd36e0bc2472548f3bf56cc5cb3da2b2984f", size = 38140, upload-time = "2025-10-08T19:48:11.232Z" }, + { url = "https://files.pythonhosted.org/packages/0c/2a/a758b47de253636e1b8aef181c0b4f4f204bf0dd964914fb2af90a95b49b/propcache-0.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:5a103c3eb905fcea0ab98be99c3a9a5ab2de60228aa5aceedc614c0281cf6153", size = 41257, upload-time = "2025-10-08T19:48:12.707Z" }, + { url = "https://files.pythonhosted.org/packages/34/5e/63bd5896c3fec12edcbd6f12508d4890d23c265df28c74b175e1ef9f4f3b/propcache-0.4.1-cp314-cp314-win_arm64.whl", hash = "sha256:74c1fb26515153e482e00177a1ad654721bf9207da8a494a0c05e797ad27b992", size = 38097, upload-time = "2025-10-08T19:48:13.923Z" }, + { url = "https://files.pythonhosted.org/packages/99/85/9ff785d787ccf9bbb3f3106f79884a130951436f58392000231b4c737c80/propcache-0.4.1-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:824e908bce90fb2743bd6b59db36eb4f45cd350a39637c9f73b1c1ea66f5b75f", size = 81455, upload-time = "2025-10-08T19:48:15.16Z" }, + { url = "https://files.pythonhosted.org/packages/90/85/2431c10c8e7ddb1445c1f7c4b54d886e8ad20e3c6307e7218f05922cad67/propcache-0.4.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:c2b5e7db5328427c57c8e8831abda175421b709672f6cfc3d630c3b7e2146393", size = 46372, upload-time = "2025-10-08T19:48:16.424Z" }, + { url = "https://files.pythonhosted.org/packages/01/20/b0972d902472da9bcb683fa595099911f4d2e86e5683bcc45de60dd05dc3/propcache-0.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6f6ff873ed40292cd4969ef5310179afd5db59fdf055897e282485043fc80ad0", size = 48411, upload-time = "2025-10-08T19:48:17.577Z" }, + { url = "https://files.pythonhosted.org/packages/e2/e3/7dc89f4f21e8f99bad3d5ddb3a3389afcf9da4ac69e3deb2dcdc96e74169/propcache-0.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:49a2dc67c154db2c1463013594c458881a069fcf98940e61a0569016a583020a", size = 275712, upload-time = "2025-10-08T19:48:18.901Z" }, + { url = "https://files.pythonhosted.org/packages/20/67/89800c8352489b21a8047c773067644e3897f02ecbbd610f4d46b7f08612/propcache-0.4.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:005f08e6a0529984491e37d8dbc3dd86f84bd78a8ceb5fa9a021f4c48d4984be", size = 273557, upload-time = "2025-10-08T19:48:20.762Z" }, + { url = "https://files.pythonhosted.org/packages/e2/a1/b52b055c766a54ce6d9c16d9aca0cad8059acd9637cdf8aa0222f4a026ef/propcache-0.4.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5c3310452e0d31390da9035c348633b43d7e7feb2e37be252be6da45abd1abcc", size = 280015, upload-time = "2025-10-08T19:48:22.592Z" }, + { url = "https://files.pythonhosted.org/packages/48/c8/33cee30bd890672c63743049f3c9e4be087e6780906bfc3ec58528be59c1/propcache-0.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4c3c70630930447f9ef1caac7728c8ad1c56bc5015338b20fed0d08ea2480b3a", size = 262880, upload-time = "2025-10-08T19:48:23.947Z" }, + { url = "https://files.pythonhosted.org/packages/0c/b1/8f08a143b204b418285c88b83d00edbd61afbc2c6415ffafc8905da7038b/propcache-0.4.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8e57061305815dfc910a3634dcf584f08168a8836e6999983569f51a8544cd89", size = 260938, upload-time = "2025-10-08T19:48:25.656Z" }, + { url = "https://files.pythonhosted.org/packages/cf/12/96e4664c82ca2f31e1c8dff86afb867348979eb78d3cb8546a680287a1e9/propcache-0.4.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:521a463429ef54143092c11a77e04056dd00636f72e8c45b70aaa3140d639726", size = 247641, upload-time = "2025-10-08T19:48:27.207Z" }, + { url = "https://files.pythonhosted.org/packages/18/ed/e7a9cfca28133386ba52278136d42209d3125db08d0a6395f0cba0c0285c/propcache-0.4.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:120c964da3fdc75e3731aa392527136d4ad35868cc556fd09bb6d09172d9a367", size = 262510, upload-time = "2025-10-08T19:48:28.65Z" }, + { url = "https://files.pythonhosted.org/packages/f5/76/16d8bf65e8845dd62b4e2b57444ab81f07f40caa5652b8969b87ddcf2ef6/propcache-0.4.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:d8f353eb14ee3441ee844ade4277d560cdd68288838673273b978e3d6d2c8f36", size = 263161, upload-time = "2025-10-08T19:48:30.133Z" }, + { url = "https://files.pythonhosted.org/packages/e7/70/c99e9edb5d91d5ad8a49fa3c1e8285ba64f1476782fed10ab251ff413ba1/propcache-0.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ab2943be7c652f09638800905ee1bab2c544e537edb57d527997a24c13dc1455", size = 257393, upload-time = "2025-10-08T19:48:31.567Z" }, + { url = "https://files.pythonhosted.org/packages/08/02/87b25304249a35c0915d236575bc3574a323f60b47939a2262b77632a3ee/propcache-0.4.1-cp314-cp314t-win32.whl", hash = "sha256:05674a162469f31358c30bcaa8883cb7829fa3110bf9c0991fe27d7896c42d85", size = 42546, upload-time = "2025-10-08T19:48:32.872Z" }, + { url = "https://files.pythonhosted.org/packages/cb/ef/3c6ecf8b317aa982f309835e8f96987466123c6e596646d4e6a1dfcd080f/propcache-0.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:990f6b3e2a27d683cb7602ed6c86f15ee6b43b1194736f9baaeb93d0016633b1", size = 46259, upload-time = "2025-10-08T19:48:34.226Z" }, + { url = "https://files.pythonhosted.org/packages/c4/2d/346e946d4951f37eca1e4f55be0f0174c52cd70720f84029b02f296f4a38/propcache-0.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:ecef2343af4cc68e05131e45024ba34f6095821988a9d0a02aa7c73fcc448aa9", size = 40428, upload-time = "2025-10-08T19:48:35.441Z" }, + { 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 = "protobuf" version = "6.33.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/34/44/e49ecff446afeec9d1a66d6bbf9adc21e3c7cea7803a920ca3773379d4f6/protobuf-6.33.2.tar.gz", hash = "sha256:56dc370c91fbb8ac85bc13582c9e373569668a290aa2e66a590c2a0d35ddb9e4", size = 444296 } +sdist = { url = "https://files.pythonhosted.org/packages/34/44/e49ecff446afeec9d1a66d6bbf9adc21e3c7cea7803a920ca3773379d4f6/protobuf-6.33.2.tar.gz", hash = "sha256:56dc370c91fbb8ac85bc13582c9e373569668a290aa2e66a590c2a0d35ddb9e4", size = 444296, upload-time = "2025-12-06T00:17:53.311Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/bc/91/1e3a34881a88697a7354ffd177e8746e97a722e5e8db101544b47e84afb1/protobuf-6.33.2-cp310-abi3-win32.whl", hash = "sha256:87eb388bd2d0f78febd8f4c8779c79247b26a5befad525008e49a6955787ff3d", size = 425603 }, - { url = "https://files.pythonhosted.org/packages/64/20/4d50191997e917ae13ad0a235c8b42d8c1ab9c3e6fd455ca16d416944355/protobuf-6.33.2-cp310-abi3-win_amd64.whl", hash = "sha256:fc2a0e8b05b180e5fc0dd1559fe8ebdae21a27e81ac77728fb6c42b12c7419b4", size = 436930 }, - { url = "https://files.pythonhosted.org/packages/b2/ca/7e485da88ba45c920fb3f50ae78de29ab925d9e54ef0de678306abfbb497/protobuf-6.33.2-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:d9b19771ca75935b3a4422957bc518b0cecb978b31d1dd12037b088f6bcc0e43", size = 427621 }, - { url = "https://files.pythonhosted.org/packages/7d/4f/f743761e41d3b2b2566748eb76bbff2b43e14d5fcab694f494a16458b05f/protobuf-6.33.2-cp39-abi3-manylinux2014_aarch64.whl", hash = "sha256:b5d3b5625192214066d99b2b605f5783483575656784de223f00a8d00754fc0e", size = 324460 }, - { url = "https://files.pythonhosted.org/packages/b1/fa/26468d00a92824020f6f2090d827078c09c9c587e34cbfd2d0c7911221f8/protobuf-6.33.2-cp39-abi3-manylinux2014_s390x.whl", hash = "sha256:8cd7640aee0b7828b6d03ae518b5b4806fdfc1afe8de82f79c3454f8aef29872", size = 339168 }, - { url = "https://files.pythonhosted.org/packages/56/13/333b8f421738f149d4fe5e49553bc2a2ab75235486259f689b4b91f96cec/protobuf-6.33.2-cp39-abi3-manylinux2014_x86_64.whl", hash = "sha256:1f8017c48c07ec5859106533b682260ba3d7c5567b1ca1f24297ce03384d1b4f", size = 323270 }, - { url = "https://files.pythonhosted.org/packages/0e/15/4f02896cc3df04fc465010a4c6a0cd89810f54617a32a70ef531ed75d61c/protobuf-6.33.2-py3-none-any.whl", hash = "sha256:7636aad9bb01768870266de5dc009de2d1b936771b38a793f73cbbf279c91c5c", size = 170501 }, + { url = "https://files.pythonhosted.org/packages/bc/91/1e3a34881a88697a7354ffd177e8746e97a722e5e8db101544b47e84afb1/protobuf-6.33.2-cp310-abi3-win32.whl", hash = "sha256:87eb388bd2d0f78febd8f4c8779c79247b26a5befad525008e49a6955787ff3d", size = 425603, upload-time = "2025-12-06T00:17:41.114Z" }, + { url = "https://files.pythonhosted.org/packages/64/20/4d50191997e917ae13ad0a235c8b42d8c1ab9c3e6fd455ca16d416944355/protobuf-6.33.2-cp310-abi3-win_amd64.whl", hash = "sha256:fc2a0e8b05b180e5fc0dd1559fe8ebdae21a27e81ac77728fb6c42b12c7419b4", size = 436930, upload-time = "2025-12-06T00:17:43.278Z" }, + { url = "https://files.pythonhosted.org/packages/b2/ca/7e485da88ba45c920fb3f50ae78de29ab925d9e54ef0de678306abfbb497/protobuf-6.33.2-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:d9b19771ca75935b3a4422957bc518b0cecb978b31d1dd12037b088f6bcc0e43", size = 427621, upload-time = "2025-12-06T00:17:44.445Z" }, + { url = "https://files.pythonhosted.org/packages/7d/4f/f743761e41d3b2b2566748eb76bbff2b43e14d5fcab694f494a16458b05f/protobuf-6.33.2-cp39-abi3-manylinux2014_aarch64.whl", hash = "sha256:b5d3b5625192214066d99b2b605f5783483575656784de223f00a8d00754fc0e", size = 324460, upload-time = "2025-12-06T00:17:45.678Z" }, + { url = "https://files.pythonhosted.org/packages/b1/fa/26468d00a92824020f6f2090d827078c09c9c587e34cbfd2d0c7911221f8/protobuf-6.33.2-cp39-abi3-manylinux2014_s390x.whl", hash = "sha256:8cd7640aee0b7828b6d03ae518b5b4806fdfc1afe8de82f79c3454f8aef29872", size = 339168, upload-time = "2025-12-06T00:17:46.813Z" }, + { url = "https://files.pythonhosted.org/packages/56/13/333b8f421738f149d4fe5e49553bc2a2ab75235486259f689b4b91f96cec/protobuf-6.33.2-cp39-abi3-manylinux2014_x86_64.whl", hash = "sha256:1f8017c48c07ec5859106533b682260ba3d7c5567b1ca1f24297ce03384d1b4f", size = 323270, upload-time = "2025-12-06T00:17:48.253Z" }, + { url = "https://files.pythonhosted.org/packages/0e/15/4f02896cc3df04fc465010a4c6a0cd89810f54617a32a70ef531ed75d61c/protobuf-6.33.2-py3-none-any.whl", hash = "sha256:7636aad9bb01768870266de5dc009de2d1b936771b38a793f73cbbf279c91c5c", size = 170501, upload-time = "2025-12-06T00:17:52.211Z" }, ] [[package]] @@ -1936,9 +1913,9 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "tzdata", marker = "sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/e0/1a/7d9ef4fdc13ef7f15b934c393edc97a35c281bb7d3c3329fbfcbe915a7c2/psycopg-3.3.2.tar.gz", hash = "sha256:707a67975ee214d200511177a6a80e56e654754c9afca06a7194ea6bbfde9ca7", size = 165630 } +sdist = { url = "https://files.pythonhosted.org/packages/e0/1a/7d9ef4fdc13ef7f15b934c393edc97a35c281bb7d3c3329fbfcbe915a7c2/psycopg-3.3.2.tar.gz", hash = "sha256:707a67975ee214d200511177a6a80e56e654754c9afca06a7194ea6bbfde9ca7", size = 165630, upload-time = "2025-12-06T17:34:53.899Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/8c/51/2779ccdf9305981a06b21a6b27e8547c948d85c41c76ff434192784a4c93/psycopg-3.3.2-py3-none-any.whl", hash = "sha256:3e94bc5f4690247d734599af56e51bae8e0db8e4311ea413f801fef82b14a99b", size = 212774 }, + { url = "https://files.pythonhosted.org/packages/8c/51/2779ccdf9305981a06b21a6b27e8547c948d85c41c76ff434192784a4c93/psycopg-3.3.2-py3-none-any.whl", hash = "sha256:3e94bc5f4690247d734599af56e51bae8e0db8e4311ea413f801fef82b14a99b", size = 212774, upload-time = "2025-12-06T17:31:41.414Z" }, ] [package.optional-dependencies] @@ -1951,28 +1928,28 @@ name = "psycopg-binary" version = "3.3.2" source = { registry = "https://pypi.org/simple" } wheels = [ - { url = "https://files.pythonhosted.org/packages/14/73/7ca7cb22b9ac7393fb5de7d28ca97e8347c375c8498b3bff2c99c1f38038/psycopg_binary-3.3.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:fc5a189e89cbfff174588665bb18d28d2d0428366cc9dae5864afcaa2e57380b", size = 4579068 }, - { url = "https://files.pythonhosted.org/packages/f5/42/0cf38ff6c62c792fc5b55398a853a77663210ebd51ed6f0c4a05b06f95a6/psycopg_binary-3.3.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:083c2e182be433f290dc2c516fd72b9b47054fcd305cce791e0a50d9e93e06f2", size = 4657520 }, - { url = "https://files.pythonhosted.org/packages/3b/60/df846bc84cbf2231e01b0fff48b09841fe486fa177665e50f4995b1bfa44/psycopg_binary-3.3.2-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:ac230e3643d1c436a2dfb59ca84357dfc6862c9f372fc5dbd96bafecae581f9f", size = 5452086 }, - { url = "https://files.pythonhosted.org/packages/ab/85/30c846a00db86b1b53fd5bfd4b4edfbd0c00de8f2c75dd105610bd7568fc/psycopg_binary-3.3.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d8c899a540f6c7585cee53cddc929dd4d2db90fd828e37f5d4017b63acbc1a5d", size = 5131125 }, - { url = "https://files.pythonhosted.org/packages/6d/15/9968732013373f36f8a2a3fb76104dffc8efd9db78709caa5ae1a87b1f80/psycopg_binary-3.3.2-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:50ff10ab8c0abdb5a5451b9315538865b50ba64c907742a1385fdf5f5772b73e", size = 6722914 }, - { url = "https://files.pythonhosted.org/packages/b2/ba/29e361fe02143ac5ff5a1ca3e45697344cfbebe2eaf8c4e7eec164bff9a0/psycopg_binary-3.3.2-cp313-cp313-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:23d2594af848c1fd3d874a9364bef50730124e72df7bb145a20cb45e728c50ed", size = 4966081 }, - { url = "https://files.pythonhosted.org/packages/99/45/1be90c8f1a1a237046903e91202fb06708745c179f220b361d6333ed7641/psycopg_binary-3.3.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ea4fe6b4ead3bbbe27244ea224fcd1f53cb119afc38b71a2f3ce570149a03e30", size = 4493332 }, - { url = "https://files.pythonhosted.org/packages/2e/b5/bbdc07d5f0a5e90c617abd624368182aa131485e18038b2c6c85fc054aed/psycopg_binary-3.3.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:742ce48cde825b8e52fb1a658253d6d1ff66d152081cbc76aa45e2986534858d", size = 4170781 }, - { url = "https://files.pythonhosted.org/packages/d1/2a/0d45e4f4da2bd78c3237ffa03475ef3751f69a81919c54a6e610eb1a7c96/psycopg_binary-3.3.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:e22bf6b54df994aff37ab52695d635f1ef73155e781eee1f5fa75bc08b58c8da", size = 3910544 }, - { url = "https://files.pythonhosted.org/packages/3a/62/a8e0f092f4dbef9a94b032fb71e214cf0a375010692fbe7493a766339e47/psycopg_binary-3.3.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8db9034cde3bcdafc66980f0130813f5c5d19e74b3f2a19fb3cfbc25ad113121", size = 4220070 }, - { url = "https://files.pythonhosted.org/packages/09/e6/5fc8d8aff8afa114bb4a94a0341b9309311e8bf3ab32d816032f8b984d4e/psycopg_binary-3.3.2-cp313-cp313-win_amd64.whl", hash = "sha256:df65174c7cf6b05ea273ce955927d3270b3a6e27b0b12762b009ce6082b8d3fc", size = 3540922 }, - { url = "https://files.pythonhosted.org/packages/bd/75/ad18c0b97b852aba286d06befb398cc6d383e9dfd0a518369af275a5a526/psycopg_binary-3.3.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:9ca24062cd9b2270e4d77576042e9cc2b1d543f09da5aba1f1a3d016cea28390", size = 4596371 }, - { url = "https://files.pythonhosted.org/packages/5a/79/91649d94c8d89f84af5da7c9d474bfba35b08eb8f492ca3422b08f0a6427/psycopg_binary-3.3.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c749770da0947bc972e512f35366dd4950c0e34afad89e60b9787a37e97cb443", size = 4675139 }, - { url = "https://files.pythonhosted.org/packages/56/ac/b26e004880f054549ec9396594e1ffe435810b0673e428e619ed722e4244/psycopg_binary-3.3.2-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:03b7cd73fb8c45d272a34ae7249713e32492891492681e3cf11dff9531cf37e9", size = 5456120 }, - { url = "https://files.pythonhosted.org/packages/4b/8d/410681dccd6f2999fb115cc248521ec50dd2b0aba66ae8de7e81efdebbee/psycopg_binary-3.3.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:43b130e3b6edcb5ee856c7167ccb8561b473308c870ed83978ae478613764f1c", size = 5133484 }, - { url = "https://files.pythonhosted.org/packages/66/30/ebbab99ea2cfa099d7b11b742ce13415d44f800555bfa4ad2911dc645b71/psycopg_binary-3.3.2-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7c1feba5a8c617922321aef945865334e468337b8fc5c73074f5e63143013b5a", size = 6731818 }, - { url = "https://files.pythonhosted.org/packages/70/02/d260646253b7ad805d60e0de47f9b811d6544078452579466a098598b6f4/psycopg_binary-3.3.2-cp314-cp314-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cabb2a554d9a0a6bf84037d86ca91782f087dfff2a61298d0b00c19c0bc43f6d", size = 4983859 }, - { url = "https://files.pythonhosted.org/packages/72/8d/e778d7bad1a7910aa36281f092bd85c5702f508fd9bb0ea2020ffbb6585c/psycopg_binary-3.3.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:74bc306c4b4df35b09bc8cecf806b271e1c5d708f7900145e4e54a2e5dedfed0", size = 4516388 }, - { url = "https://files.pythonhosted.org/packages/bd/f1/64e82098722e2ab3521797584caf515284be09c1e08a872551b6edbb0074/psycopg_binary-3.3.2-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:d79b0093f0fbf7a962d6a46ae292dc056c65d16a8ee9361f3cfbafd4c197ab14", size = 4192382 }, - { url = "https://files.pythonhosted.org/packages/fa/d0/c20f4e668e89494972e551c31be2a0016e3f50d552d7ae9ac07086407599/psycopg_binary-3.3.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:1586e220be05547c77afc326741dd41cc7fba38a81f9931f616ae98865439678", size = 3928660 }, - { url = "https://files.pythonhosted.org/packages/0f/e1/99746c171de22539fd5eb1c9ca21dc805b54cfae502d7451d237d1dbc349/psycopg_binary-3.3.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:458696a5fa5dad5b6fb5d5862c22454434ce4fe1cf66ca6c0de5f904cbc1ae3e", size = 4239169 }, - { url = "https://files.pythonhosted.org/packages/72/f7/212343c1c9cfac35fd943c527af85e9091d633176e2a407a0797856ff7b9/psycopg_binary-3.3.2-cp314-cp314-win_amd64.whl", hash = "sha256:04bb2de4ba69d6f8395b446ede795e8884c040ec71d01dd07ac2b2d18d4153d1", size = 3642122 }, + { url = "https://files.pythonhosted.org/packages/14/73/7ca7cb22b9ac7393fb5de7d28ca97e8347c375c8498b3bff2c99c1f38038/psycopg_binary-3.3.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:fc5a189e89cbfff174588665bb18d28d2d0428366cc9dae5864afcaa2e57380b", size = 4579068, upload-time = "2025-12-06T17:33:39.303Z" }, + { url = "https://files.pythonhosted.org/packages/f5/42/0cf38ff6c62c792fc5b55398a853a77663210ebd51ed6f0c4a05b06f95a6/psycopg_binary-3.3.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:083c2e182be433f290dc2c516fd72b9b47054fcd305cce791e0a50d9e93e06f2", size = 4657520, upload-time = "2025-12-06T17:33:42.536Z" }, + { url = "https://files.pythonhosted.org/packages/3b/60/df846bc84cbf2231e01b0fff48b09841fe486fa177665e50f4995b1bfa44/psycopg_binary-3.3.2-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:ac230e3643d1c436a2dfb59ca84357dfc6862c9f372fc5dbd96bafecae581f9f", size = 5452086, upload-time = "2025-12-06T17:33:46.54Z" }, + { url = "https://files.pythonhosted.org/packages/ab/85/30c846a00db86b1b53fd5bfd4b4edfbd0c00de8f2c75dd105610bd7568fc/psycopg_binary-3.3.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d8c899a540f6c7585cee53cddc929dd4d2db90fd828e37f5d4017b63acbc1a5d", size = 5131125, upload-time = "2025-12-06T17:33:50.413Z" }, + { url = "https://files.pythonhosted.org/packages/6d/15/9968732013373f36f8a2a3fb76104dffc8efd9db78709caa5ae1a87b1f80/psycopg_binary-3.3.2-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:50ff10ab8c0abdb5a5451b9315538865b50ba64c907742a1385fdf5f5772b73e", size = 6722914, upload-time = "2025-12-06T17:33:54.544Z" }, + { url = "https://files.pythonhosted.org/packages/b2/ba/29e361fe02143ac5ff5a1ca3e45697344cfbebe2eaf8c4e7eec164bff9a0/psycopg_binary-3.3.2-cp313-cp313-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:23d2594af848c1fd3d874a9364bef50730124e72df7bb145a20cb45e728c50ed", size = 4966081, upload-time = "2025-12-06T17:33:58.477Z" }, + { url = "https://files.pythonhosted.org/packages/99/45/1be90c8f1a1a237046903e91202fb06708745c179f220b361d6333ed7641/psycopg_binary-3.3.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ea4fe6b4ead3bbbe27244ea224fcd1f53cb119afc38b71a2f3ce570149a03e30", size = 4493332, upload-time = "2025-12-06T17:34:02.011Z" }, + { url = "https://files.pythonhosted.org/packages/2e/b5/bbdc07d5f0a5e90c617abd624368182aa131485e18038b2c6c85fc054aed/psycopg_binary-3.3.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:742ce48cde825b8e52fb1a658253d6d1ff66d152081cbc76aa45e2986534858d", size = 4170781, upload-time = "2025-12-06T17:34:05.298Z" }, + { url = "https://files.pythonhosted.org/packages/d1/2a/0d45e4f4da2bd78c3237ffa03475ef3751f69a81919c54a6e610eb1a7c96/psycopg_binary-3.3.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:e22bf6b54df994aff37ab52695d635f1ef73155e781eee1f5fa75bc08b58c8da", size = 3910544, upload-time = "2025-12-06T17:34:08.251Z" }, + { url = "https://files.pythonhosted.org/packages/3a/62/a8e0f092f4dbef9a94b032fb71e214cf0a375010692fbe7493a766339e47/psycopg_binary-3.3.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8db9034cde3bcdafc66980f0130813f5c5d19e74b3f2a19fb3cfbc25ad113121", size = 4220070, upload-time = "2025-12-06T17:34:11.392Z" }, + { url = "https://files.pythonhosted.org/packages/09/e6/5fc8d8aff8afa114bb4a94a0341b9309311e8bf3ab32d816032f8b984d4e/psycopg_binary-3.3.2-cp313-cp313-win_amd64.whl", hash = "sha256:df65174c7cf6b05ea273ce955927d3270b3a6e27b0b12762b009ce6082b8d3fc", size = 3540922, upload-time = "2025-12-06T17:34:14.88Z" }, + { url = "https://files.pythonhosted.org/packages/bd/75/ad18c0b97b852aba286d06befb398cc6d383e9dfd0a518369af275a5a526/psycopg_binary-3.3.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:9ca24062cd9b2270e4d77576042e9cc2b1d543f09da5aba1f1a3d016cea28390", size = 4596371, upload-time = "2025-12-06T17:34:18.007Z" }, + { url = "https://files.pythonhosted.org/packages/5a/79/91649d94c8d89f84af5da7c9d474bfba35b08eb8f492ca3422b08f0a6427/psycopg_binary-3.3.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c749770da0947bc972e512f35366dd4950c0e34afad89e60b9787a37e97cb443", size = 4675139, upload-time = "2025-12-06T17:34:21.374Z" }, + { url = "https://files.pythonhosted.org/packages/56/ac/b26e004880f054549ec9396594e1ffe435810b0673e428e619ed722e4244/psycopg_binary-3.3.2-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:03b7cd73fb8c45d272a34ae7249713e32492891492681e3cf11dff9531cf37e9", size = 5456120, upload-time = "2025-12-06T17:34:25.102Z" }, + { url = "https://files.pythonhosted.org/packages/4b/8d/410681dccd6f2999fb115cc248521ec50dd2b0aba66ae8de7e81efdebbee/psycopg_binary-3.3.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:43b130e3b6edcb5ee856c7167ccb8561b473308c870ed83978ae478613764f1c", size = 5133484, upload-time = "2025-12-06T17:34:28.933Z" }, + { url = "https://files.pythonhosted.org/packages/66/30/ebbab99ea2cfa099d7b11b742ce13415d44f800555bfa4ad2911dc645b71/psycopg_binary-3.3.2-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7c1feba5a8c617922321aef945865334e468337b8fc5c73074f5e63143013b5a", size = 6731818, upload-time = "2025-12-06T17:34:33.094Z" }, + { url = "https://files.pythonhosted.org/packages/70/02/d260646253b7ad805d60e0de47f9b811d6544078452579466a098598b6f4/psycopg_binary-3.3.2-cp314-cp314-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cabb2a554d9a0a6bf84037d86ca91782f087dfff2a61298d0b00c19c0bc43f6d", size = 4983859, upload-time = "2025-12-06T17:34:36.457Z" }, + { url = "https://files.pythonhosted.org/packages/72/8d/e778d7bad1a7910aa36281f092bd85c5702f508fd9bb0ea2020ffbb6585c/psycopg_binary-3.3.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:74bc306c4b4df35b09bc8cecf806b271e1c5d708f7900145e4e54a2e5dedfed0", size = 4516388, upload-time = "2025-12-06T17:34:40.129Z" }, + { url = "https://files.pythonhosted.org/packages/bd/f1/64e82098722e2ab3521797584caf515284be09c1e08a872551b6edbb0074/psycopg_binary-3.3.2-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:d79b0093f0fbf7a962d6a46ae292dc056c65d16a8ee9361f3cfbafd4c197ab14", size = 4192382, upload-time = "2025-12-06T17:34:43.279Z" }, + { url = "https://files.pythonhosted.org/packages/fa/d0/c20f4e668e89494972e551c31be2a0016e3f50d552d7ae9ac07086407599/psycopg_binary-3.3.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:1586e220be05547c77afc326741dd41cc7fba38a81f9931f616ae98865439678", size = 3928660, upload-time = "2025-12-06T17:34:46.757Z" }, + { url = "https://files.pythonhosted.org/packages/0f/e1/99746c171de22539fd5eb1c9ca21dc805b54cfae502d7451d237d1dbc349/psycopg_binary-3.3.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:458696a5fa5dad5b6fb5d5862c22454434ce4fe1cf66ca6c0de5f904cbc1ae3e", size = 4239169, upload-time = "2025-12-06T17:34:49.751Z" }, + { url = "https://files.pythonhosted.org/packages/72/f7/212343c1c9cfac35fd943c527af85e9091d633176e2a407a0797856ff7b9/psycopg_binary-3.3.2-cp314-cp314-win_amd64.whl", hash = "sha256:04bb2de4ba69d6f8395b446ede795e8884c040ec71d01dd07ac2b2d18d4153d1", size = 3642122, upload-time = "2025-12-06T17:34:52.506Z" }, ] [[package]] @@ -1983,9 +1960,9 @@ dependencies = [ { name = "beartype" }, { name = "py-key-value-shared" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/93/ce/3136b771dddf5ac905cc193b461eb67967cf3979688c6696e1f2cdcde7ea/py_key_value_aio-0.3.0.tar.gz", hash = "sha256:858e852fcf6d696d231266da66042d3355a7f9871650415feef9fca7a6cd4155", size = 50801 } +sdist = { url = "https://files.pythonhosted.org/packages/93/ce/3136b771dddf5ac905cc193b461eb67967cf3979688c6696e1f2cdcde7ea/py_key_value_aio-0.3.0.tar.gz", hash = "sha256:858e852fcf6d696d231266da66042d3355a7f9871650415feef9fca7a6cd4155", size = 50801, upload-time = "2025-11-17T16:50:04.711Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/99/10/72f6f213b8f0bce36eff21fda0a13271834e9eeff7f9609b01afdc253c79/py_key_value_aio-0.3.0-py3-none-any.whl", hash = "sha256:1c781915766078bfd608daa769fefb97e65d1d73746a3dfb640460e322071b64", size = 96342 }, + { url = "https://files.pythonhosted.org/packages/99/10/72f6f213b8f0bce36eff21fda0a13271834e9eeff7f9609b01afdc253c79/py_key_value_aio-0.3.0-py3-none-any.whl", hash = "sha256:1c781915766078bfd608daa769fefb97e65d1d73746a3dfb640460e322071b64", size = 96342, upload-time = "2025-11-17T16:50:03.801Z" }, ] [package.optional-dependencies] @@ -2011,18 +1988,18 @@ dependencies = [ { name = "beartype" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/7b/e4/1971dfc4620a3a15b4579fe99e024f5edd6e0967a71154771a059daff4db/py_key_value_shared-0.3.0.tar.gz", hash = "sha256:8fdd786cf96c3e900102945f92aa1473138ebe960ef49da1c833790160c28a4b", size = 11666 } +sdist = { url = "https://files.pythonhosted.org/packages/7b/e4/1971dfc4620a3a15b4579fe99e024f5edd6e0967a71154771a059daff4db/py_key_value_shared-0.3.0.tar.gz", hash = "sha256:8fdd786cf96c3e900102945f92aa1473138ebe960ef49da1c833790160c28a4b", size = 11666, upload-time = "2025-11-17T16:50:06.849Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/51/e4/b8b0a03ece72f47dce2307d36e1c34725b7223d209fc679315ffe6a4e2c3/py_key_value_shared-0.3.0-py3-none-any.whl", hash = "sha256:5b0efba7ebca08bb158b1e93afc2f07d30b8f40c2fc12ce24a4c0d84f42f9298", size = 19560 }, + { url = "https://files.pythonhosted.org/packages/51/e4/b8b0a03ece72f47dce2307d36e1c34725b7223d209fc679315ffe6a4e2c3/py_key_value_shared-0.3.0-py3-none-any.whl", hash = "sha256:5b0efba7ebca08bb158b1e93afc2f07d30b8f40c2fc12ce24a4c0d84f42f9298", size = 19560, upload-time = "2025-11-17T16:50:05.954Z" }, ] [[package]] name = "pyasn1" version = "0.6.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ba/e9/01f1a64245b89f039897cb0130016d79f77d52669aae6ee7b159a6c4c018/pyasn1-0.6.1.tar.gz", hash = "sha256:6f580d2bdd84365380830acf45550f2511469f673cb4a5ae3857a3170128b034", size = 145322 } +sdist = { url = "https://files.pythonhosted.org/packages/ba/e9/01f1a64245b89f039897cb0130016d79f77d52669aae6ee7b159a6c4c018/pyasn1-0.6.1.tar.gz", hash = "sha256:6f580d2bdd84365380830acf45550f2511469f673cb4a5ae3857a3170128b034", size = 145322, upload-time = "2024-09-10T22:41:42.55Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c8/f1/d6a797abb14f6283c0ddff96bbdd46937f64122b8c925cab503dd37f8214/pyasn1-0.6.1-py3-none-any.whl", hash = "sha256:0d632f46f2ba09143da3a8afe9e33fb6f92fa2320ab7e886e2d0f7672af84629", size = 83135 }, + { url = "https://files.pythonhosted.org/packages/c8/f1/d6a797abb14f6283c0ddff96bbdd46937f64122b8c925cab503dd37f8214/pyasn1-0.6.1-py3-none-any.whl", hash = "sha256:0d632f46f2ba09143da3a8afe9e33fb6f92fa2320ab7e886e2d0f7672af84629", size = 83135, upload-time = "2024-09-11T16:00:36.122Z" }, ] [[package]] @@ -2032,18 +2009,18 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pyasn1" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/e9/e6/78ebbb10a8c8e4b61a59249394a4a594c1a7af95593dc933a349c8d00964/pyasn1_modules-0.4.2.tar.gz", hash = "sha256:677091de870a80aae844b1ca6134f54652fa2c8c5a52aa396440ac3106e941e6", size = 307892 } +sdist = { url = "https://files.pythonhosted.org/packages/e9/e6/78ebbb10a8c8e4b61a59249394a4a594c1a7af95593dc933a349c8d00964/pyasn1_modules-0.4.2.tar.gz", hash = "sha256:677091de870a80aae844b1ca6134f54652fa2c8c5a52aa396440ac3106e941e6", size = 307892, upload-time = "2025-03-28T02:41:22.17Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/47/8d/d529b5d697919ba8c11ad626e835d4039be708a35b0d22de83a269a6682c/pyasn1_modules-0.4.2-py3-none-any.whl", hash = "sha256:29253a9207ce32b64c3ac6600edc75368f98473906e8fd1043bd6b5b1de2c14a", size = 181259 }, + { url = "https://files.pythonhosted.org/packages/47/8d/d529b5d697919ba8c11ad626e835d4039be708a35b0d22de83a269a6682c/pyasn1_modules-0.4.2-py3-none-any.whl", hash = "sha256:29253a9207ce32b64c3ac6600edc75368f98473906e8fd1043bd6b5b1de2c14a", size = 181259, upload-time = "2025-03-28T02:41:19.028Z" }, ] [[package]] name = "pycparser" version = "2.23" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/fe/cf/d2d3b9f5699fb1e4615c8e32ff220203e43b248e1dfcc6736ad9057731ca/pycparser-2.23.tar.gz", hash = "sha256:78816d4f24add8f10a06d6f05b4d424ad9e96cfebf68a4ddc99c65c0720d00c2", size = 173734 } +sdist = { url = "https://files.pythonhosted.org/packages/fe/cf/d2d3b9f5699fb1e4615c8e32ff220203e43b248e1dfcc6736ad9057731ca/pycparser-2.23.tar.gz", hash = "sha256:78816d4f24add8f10a06d6f05b4d424ad9e96cfebf68a4ddc99c65c0720d00c2", size = 173734, upload-time = "2025-09-09T13:23:47.91Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a0/e3/59cd50310fc9b59512193629e1984c1f95e5c8ae6e5d8c69532ccc65a7fe/pycparser-2.23-py3-none-any.whl", hash = "sha256:e5c6e8d3fbad53479cab09ac03729e0a9faf2bee3db8208a550daf5af81a5934", size = 118140 }, + { url = "https://files.pythonhosted.org/packages/a0/e3/59cd50310fc9b59512193629e1984c1f95e5c8ae6e5d8c69532ccc65a7fe/pycparser-2.23-py3-none-any.whl", hash = "sha256:e5c6e8d3fbad53479cab09ac03729e0a9faf2bee3db8208a550daf5af81a5934", size = 118140, upload-time = "2025-09-09T13:23:46.651Z" }, ] [[package]] @@ -2056,9 +2033,9 @@ dependencies = [ { name = "typing-extensions" }, { name = "typing-inspection" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/69/44/36f1a6e523abc58ae5f928898e4aca2e0ea509b5aa6f6f392a5d882be928/pydantic-2.12.5.tar.gz", hash = "sha256:4d351024c75c0f085a9febbb665ce8c0c6ec5d30e903bdb6394b7ede26aebb49", size = 821591 } +sdist = { url = "https://files.pythonhosted.org/packages/69/44/36f1a6e523abc58ae5f928898e4aca2e0ea509b5aa6f6f392a5d882be928/pydantic-2.12.5.tar.gz", hash = "sha256:4d351024c75c0f085a9febbb665ce8c0c6ec5d30e903bdb6394b7ede26aebb49", size = 821591, upload-time = "2025-11-26T15:11:46.471Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/5a/87/b70ad306ebb6f9b585f114d0ac2137d792b48be34d732d60e597c2f8465a/pydantic-2.12.5-py3-none-any.whl", hash = "sha256:e561593fccf61e8a20fc46dfc2dfe075b8be7d0188df33f221ad1f0139180f9d", size = 463580 }, + { url = "https://files.pythonhosted.org/packages/5a/87/b70ad306ebb6f9b585f114d0ac2137d792b48be34d732d60e597c2f8465a/pydantic-2.12.5-py3-none-any.whl", hash = "sha256:e561593fccf61e8a20fc46dfc2dfe075b8be7d0188df33f221ad1f0139180f9d", size = 463580, upload-time = "2025-11-26T15:11:44.605Z" }, ] [package.optional-dependencies] @@ -2073,9 +2050,9 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pydantic-ai-slim", extra = ["ag-ui", "anthropic", "bedrock", "cli", "cohere", "evals", "fastmcp", "google", "groq", "huggingface", "logfire", "mcp", "mistral", "openai", "retries", "temporal", "ui", "vertexai"] }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a5/da/5b4f63442a0af545f979e9ef70fc0382d23f2707392dff2bb75ad1234e08/pydantic_ai-1.39.0.tar.gz", hash = "sha256:3aa2ca2de0c71bef342acef9ac11665d2a20c241b2a4a3d4111d0d0d7b3416f4", size = 11630 } +sdist = { url = "https://files.pythonhosted.org/packages/a5/da/5b4f63442a0af545f979e9ef70fc0382d23f2707392dff2bb75ad1234e08/pydantic_ai-1.39.0.tar.gz", hash = "sha256:3aa2ca2de0c71bef342acef9ac11665d2a20c241b2a4a3d4111d0d0d7b3416f4", size = 11630, upload-time = "2025-12-24T03:34:09.044Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/4e/88/7ae680c16e08cb2b1f2b343f49fdf77c590dc542aebd0de25f0ebfae77e2/pydantic_ai-1.39.0-py3-none-any.whl", hash = "sha256:234bc1dd69a391cfe98888e3c1ab5e2b3ef027aa255a8fdc1df261d3c2852170", size = 7191 }, + { url = "https://files.pythonhosted.org/packages/4e/88/7ae680c16e08cb2b1f2b343f49fdf77c590dc542aebd0de25f0ebfae77e2/pydantic_ai-1.39.0-py3-none-any.whl", hash = "sha256:234bc1dd69a391cfe98888e3c1ab5e2b3ef027aa255a8fdc1df261d3c2852170", size = 7191, upload-time = "2025-12-24T03:33:59.844Z" }, ] [package.optional-dependencies] @@ -2096,9 +2073,9 @@ dependencies = [ { name = "pydantic-graph" }, { name = "typing-inspection" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/91/cb/542ad43e06da09104ef3443556e629d9aa260f9d584da8f7a410fb3a07e5/pydantic_ai_slim-1.39.0.tar.gz", hash = "sha256:e8cea9fc8f6149347c3e1d489b0ed2d541b4789e0583819f116284145d22fa69", size = 368962 } +sdist = { url = "https://files.pythonhosted.org/packages/91/cb/542ad43e06da09104ef3443556e629d9aa260f9d584da8f7a410fb3a07e5/pydantic_ai_slim-1.39.0.tar.gz", hash = "sha256:e8cea9fc8f6149347c3e1d489b0ed2d541b4789e0583819f116284145d22fa69", size = 368962, upload-time = "2025-12-24T03:34:11.306Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/2a/df/86381632be07b7df2e8e5880a1f18c6ee98122adf5848d329fee239a03b2/pydantic_ai_slim-1.39.0-py3-none-any.whl", hash = "sha256:8669d1781eba7713870bf76783e1e853577d5e55eb2986a27d49bc600889aaaf", size = 484906 }, + { url = "https://files.pythonhosted.org/packages/2a/df/86381632be07b7df2e8e5880a1f18c6ee98122adf5848d329fee239a03b2/pydantic_ai_slim-1.39.0-py3-none-any.whl", hash = "sha256:8669d1781eba7713870bf76783e1e853577d5e55eb2986a27d49bc600889aaaf", size = 484906, upload-time = "2025-12-24T03:34:03.179Z" }, ] [package.optional-dependencies] @@ -2173,50 +2150,50 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/71/70/23b021c950c2addd24ec408e9ab05d59b035b39d97cdc1130e1bce647bb6/pydantic_core-2.41.5.tar.gz", hash = "sha256:08daa51ea16ad373ffd5e7606252cc32f07bc72b28284b6bc9c6df804816476e", size = 460952 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/87/06/8806241ff1f70d9939f9af039c6c35f2360cf16e93c2ca76f184e76b1564/pydantic_core-2.41.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:941103c9be18ac8daf7b7adca8228f8ed6bb7a1849020f643b3a14d15b1924d9", size = 2120403 }, - { url = "https://files.pythonhosted.org/packages/94/02/abfa0e0bda67faa65fef1c84971c7e45928e108fe24333c81f3bfe35d5f5/pydantic_core-2.41.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:112e305c3314f40c93998e567879e887a3160bb8689ef3d2c04b6cc62c33ac34", size = 1896206 }, - { url = "https://files.pythonhosted.org/packages/15/df/a4c740c0943e93e6500f9eb23f4ca7ec9bf71b19e608ae5b579678c8d02f/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cbaad15cb0c90aa221d43c00e77bb33c93e8d36e0bf74760cd00e732d10a6a0", size = 1919307 }, - { url = "https://files.pythonhosted.org/packages/9a/e3/6324802931ae1d123528988e0e86587c2072ac2e5394b4bc2bc34b61ff6e/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:03ca43e12fab6023fc79d28ca6b39b05f794ad08ec2feccc59a339b02f2b3d33", size = 2063258 }, - { url = "https://files.pythonhosted.org/packages/c9/d4/2230d7151d4957dd79c3044ea26346c148c98fbf0ee6ebd41056f2d62ab5/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dc799088c08fa04e43144b164feb0c13f9a0bc40503f8df3e9fde58a3c0c101e", size = 2214917 }, - { url = "https://files.pythonhosted.org/packages/e6/9f/eaac5df17a3672fef0081b6c1bb0b82b33ee89aa5cec0d7b05f52fd4a1fa/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:97aeba56665b4c3235a0e52b2c2f5ae9cd071b8a8310ad27bddb3f7fb30e9aa2", size = 2332186 }, - { url = "https://files.pythonhosted.org/packages/cf/4e/35a80cae583a37cf15604b44240e45c05e04e86f9cfd766623149297e971/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:406bf18d345822d6c21366031003612b9c77b3e29ffdb0f612367352aab7d586", size = 2073164 }, - { url = "https://files.pythonhosted.org/packages/bf/e3/f6e262673c6140dd3305d144d032f7bd5f7497d3871c1428521f19f9efa2/pydantic_core-2.41.5-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b93590ae81f7010dbe380cdeab6f515902ebcbefe0b9327cc4804d74e93ae69d", size = 2179146 }, - { url = "https://files.pythonhosted.org/packages/75/c7/20bd7fc05f0c6ea2056a4565c6f36f8968c0924f19b7d97bbfea55780e73/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:01a3d0ab748ee531f4ea6c3e48ad9dac84ddba4b0d82291f87248f2f9de8d740", size = 2137788 }, - { url = "https://files.pythonhosted.org/packages/3a/8d/34318ef985c45196e004bc46c6eab2eda437e744c124ef0dbe1ff2c9d06b/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:6561e94ba9dacc9c61bce40e2d6bdc3bfaa0259d3ff36ace3b1e6901936d2e3e", size = 2340133 }, - { url = "https://files.pythonhosted.org/packages/9c/59/013626bf8c78a5a5d9350d12e7697d3d4de951a75565496abd40ccd46bee/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:915c3d10f81bec3a74fbd4faebe8391013ba61e5a1a8d48c4455b923bdda7858", size = 2324852 }, - { url = "https://files.pythonhosted.org/packages/1a/d9/c248c103856f807ef70c18a4f986693a46a8ffe1602e5d361485da502d20/pydantic_core-2.41.5-cp313-cp313-win32.whl", hash = "sha256:650ae77860b45cfa6e2cdafc42618ceafab3a2d9a3811fcfbd3bbf8ac3c40d36", size = 1994679 }, - { url = "https://files.pythonhosted.org/packages/9e/8b/341991b158ddab181cff136acd2552c9f35bd30380422a639c0671e99a91/pydantic_core-2.41.5-cp313-cp313-win_amd64.whl", hash = "sha256:79ec52ec461e99e13791ec6508c722742ad745571f234ea6255bed38c6480f11", size = 2019766 }, - { url = "https://files.pythonhosted.org/packages/73/7d/f2f9db34af103bea3e09735bb40b021788a5e834c81eedb541991badf8f5/pydantic_core-2.41.5-cp313-cp313-win_arm64.whl", hash = "sha256:3f84d5c1b4ab906093bdc1ff10484838aca54ef08de4afa9de0f5f14d69639cd", size = 1981005 }, - { url = "https://files.pythonhosted.org/packages/ea/28/46b7c5c9635ae96ea0fbb779e271a38129df2550f763937659ee6c5dbc65/pydantic_core-2.41.5-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:3f37a19d7ebcdd20b96485056ba9e8b304e27d9904d233d7b1015db320e51f0a", size = 2119622 }, - { url = "https://files.pythonhosted.org/packages/74/1a/145646e5687e8d9a1e8d09acb278c8535ebe9e972e1f162ed338a622f193/pydantic_core-2.41.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1d1d9764366c73f996edd17abb6d9d7649a7eb690006ab6adbda117717099b14", size = 1891725 }, - { url = "https://files.pythonhosted.org/packages/23/04/e89c29e267b8060b40dca97bfc64a19b2a3cf99018167ea1677d96368273/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25e1c2af0fce638d5f1988b686f3b3ea8cd7de5f244ca147c777769e798a9cd1", size = 1915040 }, - { url = "https://files.pythonhosted.org/packages/84/a3/15a82ac7bd97992a82257f777b3583d3e84bdb06ba6858f745daa2ec8a85/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:506d766a8727beef16b7adaeb8ee6217c64fc813646b424d0804d67c16eddb66", size = 2063691 }, - { url = "https://files.pythonhosted.org/packages/74/9b/0046701313c6ef08c0c1cf0e028c67c770a4e1275ca73131563c5f2a310a/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4819fa52133c9aa3c387b3328f25c1facc356491e6135b459f1de698ff64d869", size = 2213897 }, - { url = "https://files.pythonhosted.org/packages/8a/cd/6bac76ecd1b27e75a95ca3a9a559c643b3afcd2dd62086d4b7a32a18b169/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2b761d210c9ea91feda40d25b4efe82a1707da2ef62901466a42492c028553a2", size = 2333302 }, - { url = "https://files.pythonhosted.org/packages/4c/d2/ef2074dc020dd6e109611a8be4449b98cd25e1b9b8a303c2f0fca2f2bcf7/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22f0fb8c1c583a3b6f24df2470833b40207e907b90c928cc8d3594b76f874375", size = 2064877 }, - { url = "https://files.pythonhosted.org/packages/18/66/e9db17a9a763d72f03de903883c057b2592c09509ccfe468187f2a2eef29/pydantic_core-2.41.5-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2782c870e99878c634505236d81e5443092fba820f0373997ff75f90f68cd553", size = 2180680 }, - { url = "https://files.pythonhosted.org/packages/d3/9e/3ce66cebb929f3ced22be85d4c2399b8e85b622db77dad36b73c5387f8f8/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:0177272f88ab8312479336e1d777f6b124537d47f2123f89cb37e0accea97f90", size = 2138960 }, - { url = "https://files.pythonhosted.org/packages/a6/62/205a998f4327d2079326b01abee48e502ea739d174f0a89295c481a2272e/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:63510af5e38f8955b8ee5687740d6ebf7c2a0886d15a6d65c32814613681bc07", size = 2339102 }, - { url = "https://files.pythonhosted.org/packages/3c/0d/f05e79471e889d74d3d88f5bd20d0ed189ad94c2423d81ff8d0000aab4ff/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:e56ba91f47764cc14f1daacd723e3e82d1a89d783f0f5afe9c364b8bb491ccdb", size = 2326039 }, - { url = "https://files.pythonhosted.org/packages/ec/e1/e08a6208bb100da7e0c4b288eed624a703f4d129bde2da475721a80cab32/pydantic_core-2.41.5-cp314-cp314-win32.whl", hash = "sha256:aec5cf2fd867b4ff45b9959f8b20ea3993fc93e63c7363fe6851424c8a7e7c23", size = 1995126 }, - { url = "https://files.pythonhosted.org/packages/48/5d/56ba7b24e9557f99c9237e29f5c09913c81eeb2f3217e40e922353668092/pydantic_core-2.41.5-cp314-cp314-win_amd64.whl", hash = "sha256:8e7c86f27c585ef37c35e56a96363ab8de4e549a95512445b85c96d3e2f7c1bf", size = 2015489 }, - { url = "https://files.pythonhosted.org/packages/4e/bb/f7a190991ec9e3e0ba22e4993d8755bbc4a32925c0b5b42775c03e8148f9/pydantic_core-2.41.5-cp314-cp314-win_arm64.whl", hash = "sha256:e672ba74fbc2dc8eea59fb6d4aed6845e6905fc2a8afe93175d94a83ba2a01a0", size = 1977288 }, - { url = "https://files.pythonhosted.org/packages/92/ed/77542d0c51538e32e15afe7899d79efce4b81eee631d99850edc2f5e9349/pydantic_core-2.41.5-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8566def80554c3faa0e65ac30ab0932b9e3a5cd7f8323764303d468e5c37595a", size = 2120255 }, - { url = "https://files.pythonhosted.org/packages/bb/3d/6913dde84d5be21e284439676168b28d8bbba5600d838b9dca99de0fad71/pydantic_core-2.41.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b80aa5095cd3109962a298ce14110ae16b8c1aece8b72f9dafe81cf597ad80b3", size = 1863760 }, - { url = "https://files.pythonhosted.org/packages/5a/f0/e5e6b99d4191da102f2b0eb9687aaa7f5bea5d9964071a84effc3e40f997/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3006c3dd9ba34b0c094c544c6006cc79e87d8612999f1a5d43b769b89181f23c", size = 1878092 }, - { url = "https://files.pythonhosted.org/packages/71/48/36fb760642d568925953bcc8116455513d6e34c4beaa37544118c36aba6d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:72f6c8b11857a856bcfa48c86f5368439f74453563f951e473514579d44aa612", size = 2053385 }, - { url = "https://files.pythonhosted.org/packages/20/25/92dc684dd8eb75a234bc1c764b4210cf2646479d54b47bf46061657292a8/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5cb1b2f9742240e4bb26b652a5aeb840aa4b417c7748b6f8387927bc6e45e40d", size = 2218832 }, - { url = "https://files.pythonhosted.org/packages/e2/09/f53e0b05023d3e30357d82eb35835d0f6340ca344720a4599cd663dca599/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd3d54f38609ff308209bd43acea66061494157703364ae40c951f83ba99a1a9", size = 2327585 }, - { url = "https://files.pythonhosted.org/packages/aa/4e/2ae1aa85d6af35a39b236b1b1641de73f5a6ac4d5a7509f77b814885760c/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ff4321e56e879ee8d2a879501c8e469414d948f4aba74a2d4593184eb326660", size = 2041078 }, - { url = "https://files.pythonhosted.org/packages/cd/13/2e215f17f0ef326fc72afe94776edb77525142c693767fc347ed6288728d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d0d2568a8c11bf8225044aa94409e21da0cb09dcdafe9ecd10250b2baad531a9", size = 2173914 }, - { url = "https://files.pythonhosted.org/packages/02/7a/f999a6dcbcd0e5660bc348a3991c8915ce6599f4f2c6ac22f01d7a10816c/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:a39455728aabd58ceabb03c90e12f71fd30fa69615760a075b9fec596456ccc3", size = 2129560 }, - { url = "https://files.pythonhosted.org/packages/3a/b1/6c990ac65e3b4c079a4fb9f5b05f5b013afa0f4ed6780a3dd236d2cbdc64/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:239edca560d05757817c13dc17c50766136d21f7cd0fac50295499ae24f90fdf", size = 2329244 }, - { url = "https://files.pythonhosted.org/packages/d9/02/3c562f3a51afd4d88fff8dffb1771b30cfdfd79befd9883ee094f5b6c0d8/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:2a5e06546e19f24c6a96a129142a75cee553cc018ffee48a460059b1185f4470", size = 2331955 }, - { url = "https://files.pythonhosted.org/packages/5c/96/5fb7d8c3c17bc8c62fdb031c47d77a1af698f1d7a406b0f79aaa1338f9ad/pydantic_core-2.41.5-cp314-cp314t-win32.whl", hash = "sha256:b4ececa40ac28afa90871c2cc2b9ffd2ff0bf749380fbdf57d165fd23da353aa", size = 1988906 }, - { url = "https://files.pythonhosted.org/packages/22/ed/182129d83032702912c2e2d8bbe33c036f342cc735737064668585dac28f/pydantic_core-2.41.5-cp314-cp314t-win_amd64.whl", hash = "sha256:80aa89cad80b32a912a65332f64a4450ed00966111b6615ca6816153d3585a8c", size = 1981607 }, - { url = "https://files.pythonhosted.org/packages/9f/ed/068e41660b832bb0b1aa5b58011dea2a3fe0ba7861ff38c4d4904c1c1a99/pydantic_core-2.41.5-cp314-cp314t-win_arm64.whl", hash = "sha256:35b44f37a3199f771c3eaa53051bc8a70cd7b54f333531c59e29fd4db5d15008", size = 1974769 }, +sdist = { url = "https://files.pythonhosted.org/packages/71/70/23b021c950c2addd24ec408e9ab05d59b035b39d97cdc1130e1bce647bb6/pydantic_core-2.41.5.tar.gz", hash = "sha256:08daa51ea16ad373ffd5e7606252cc32f07bc72b28284b6bc9c6df804816476e", size = 460952, upload-time = "2025-11-04T13:43:49.098Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/87/06/8806241ff1f70d9939f9af039c6c35f2360cf16e93c2ca76f184e76b1564/pydantic_core-2.41.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:941103c9be18ac8daf7b7adca8228f8ed6bb7a1849020f643b3a14d15b1924d9", size = 2120403, upload-time = "2025-11-04T13:40:25.248Z" }, + { url = "https://files.pythonhosted.org/packages/94/02/abfa0e0bda67faa65fef1c84971c7e45928e108fe24333c81f3bfe35d5f5/pydantic_core-2.41.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:112e305c3314f40c93998e567879e887a3160bb8689ef3d2c04b6cc62c33ac34", size = 1896206, upload-time = "2025-11-04T13:40:27.099Z" }, + { url = "https://files.pythonhosted.org/packages/15/df/a4c740c0943e93e6500f9eb23f4ca7ec9bf71b19e608ae5b579678c8d02f/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cbaad15cb0c90aa221d43c00e77bb33c93e8d36e0bf74760cd00e732d10a6a0", size = 1919307, upload-time = "2025-11-04T13:40:29.806Z" }, + { url = "https://files.pythonhosted.org/packages/9a/e3/6324802931ae1d123528988e0e86587c2072ac2e5394b4bc2bc34b61ff6e/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:03ca43e12fab6023fc79d28ca6b39b05f794ad08ec2feccc59a339b02f2b3d33", size = 2063258, upload-time = "2025-11-04T13:40:33.544Z" }, + { url = "https://files.pythonhosted.org/packages/c9/d4/2230d7151d4957dd79c3044ea26346c148c98fbf0ee6ebd41056f2d62ab5/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dc799088c08fa04e43144b164feb0c13f9a0bc40503f8df3e9fde58a3c0c101e", size = 2214917, upload-time = "2025-11-04T13:40:35.479Z" }, + { url = "https://files.pythonhosted.org/packages/e6/9f/eaac5df17a3672fef0081b6c1bb0b82b33ee89aa5cec0d7b05f52fd4a1fa/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:97aeba56665b4c3235a0e52b2c2f5ae9cd071b8a8310ad27bddb3f7fb30e9aa2", size = 2332186, upload-time = "2025-11-04T13:40:37.436Z" }, + { url = "https://files.pythonhosted.org/packages/cf/4e/35a80cae583a37cf15604b44240e45c05e04e86f9cfd766623149297e971/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:406bf18d345822d6c21366031003612b9c77b3e29ffdb0f612367352aab7d586", size = 2073164, upload-time = "2025-11-04T13:40:40.289Z" }, + { url = "https://files.pythonhosted.org/packages/bf/e3/f6e262673c6140dd3305d144d032f7bd5f7497d3871c1428521f19f9efa2/pydantic_core-2.41.5-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b93590ae81f7010dbe380cdeab6f515902ebcbefe0b9327cc4804d74e93ae69d", size = 2179146, upload-time = "2025-11-04T13:40:42.809Z" }, + { url = "https://files.pythonhosted.org/packages/75/c7/20bd7fc05f0c6ea2056a4565c6f36f8968c0924f19b7d97bbfea55780e73/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:01a3d0ab748ee531f4ea6c3e48ad9dac84ddba4b0d82291f87248f2f9de8d740", size = 2137788, upload-time = "2025-11-04T13:40:44.752Z" }, + { url = "https://files.pythonhosted.org/packages/3a/8d/34318ef985c45196e004bc46c6eab2eda437e744c124ef0dbe1ff2c9d06b/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:6561e94ba9dacc9c61bce40e2d6bdc3bfaa0259d3ff36ace3b1e6901936d2e3e", size = 2340133, upload-time = "2025-11-04T13:40:46.66Z" }, + { url = "https://files.pythonhosted.org/packages/9c/59/013626bf8c78a5a5d9350d12e7697d3d4de951a75565496abd40ccd46bee/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:915c3d10f81bec3a74fbd4faebe8391013ba61e5a1a8d48c4455b923bdda7858", size = 2324852, upload-time = "2025-11-04T13:40:48.575Z" }, + { url = "https://files.pythonhosted.org/packages/1a/d9/c248c103856f807ef70c18a4f986693a46a8ffe1602e5d361485da502d20/pydantic_core-2.41.5-cp313-cp313-win32.whl", hash = "sha256:650ae77860b45cfa6e2cdafc42618ceafab3a2d9a3811fcfbd3bbf8ac3c40d36", size = 1994679, upload-time = "2025-11-04T13:40:50.619Z" }, + { url = "https://files.pythonhosted.org/packages/9e/8b/341991b158ddab181cff136acd2552c9f35bd30380422a639c0671e99a91/pydantic_core-2.41.5-cp313-cp313-win_amd64.whl", hash = "sha256:79ec52ec461e99e13791ec6508c722742ad745571f234ea6255bed38c6480f11", size = 2019766, upload-time = "2025-11-04T13:40:52.631Z" }, + { url = "https://files.pythonhosted.org/packages/73/7d/f2f9db34af103bea3e09735bb40b021788a5e834c81eedb541991badf8f5/pydantic_core-2.41.5-cp313-cp313-win_arm64.whl", hash = "sha256:3f84d5c1b4ab906093bdc1ff10484838aca54ef08de4afa9de0f5f14d69639cd", size = 1981005, upload-time = "2025-11-04T13:40:54.734Z" }, + { url = "https://files.pythonhosted.org/packages/ea/28/46b7c5c9635ae96ea0fbb779e271a38129df2550f763937659ee6c5dbc65/pydantic_core-2.41.5-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:3f37a19d7ebcdd20b96485056ba9e8b304e27d9904d233d7b1015db320e51f0a", size = 2119622, upload-time = "2025-11-04T13:40:56.68Z" }, + { url = "https://files.pythonhosted.org/packages/74/1a/145646e5687e8d9a1e8d09acb278c8535ebe9e972e1f162ed338a622f193/pydantic_core-2.41.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1d1d9764366c73f996edd17abb6d9d7649a7eb690006ab6adbda117717099b14", size = 1891725, upload-time = "2025-11-04T13:40:58.807Z" }, + { url = "https://files.pythonhosted.org/packages/23/04/e89c29e267b8060b40dca97bfc64a19b2a3cf99018167ea1677d96368273/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25e1c2af0fce638d5f1988b686f3b3ea8cd7de5f244ca147c777769e798a9cd1", size = 1915040, upload-time = "2025-11-04T13:41:00.853Z" }, + { url = "https://files.pythonhosted.org/packages/84/a3/15a82ac7bd97992a82257f777b3583d3e84bdb06ba6858f745daa2ec8a85/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:506d766a8727beef16b7adaeb8ee6217c64fc813646b424d0804d67c16eddb66", size = 2063691, upload-time = "2025-11-04T13:41:03.504Z" }, + { url = "https://files.pythonhosted.org/packages/74/9b/0046701313c6ef08c0c1cf0e028c67c770a4e1275ca73131563c5f2a310a/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4819fa52133c9aa3c387b3328f25c1facc356491e6135b459f1de698ff64d869", size = 2213897, upload-time = "2025-11-04T13:41:05.804Z" }, + { url = "https://files.pythonhosted.org/packages/8a/cd/6bac76ecd1b27e75a95ca3a9a559c643b3afcd2dd62086d4b7a32a18b169/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2b761d210c9ea91feda40d25b4efe82a1707da2ef62901466a42492c028553a2", size = 2333302, upload-time = "2025-11-04T13:41:07.809Z" }, + { url = "https://files.pythonhosted.org/packages/4c/d2/ef2074dc020dd6e109611a8be4449b98cd25e1b9b8a303c2f0fca2f2bcf7/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22f0fb8c1c583a3b6f24df2470833b40207e907b90c928cc8d3594b76f874375", size = 2064877, upload-time = "2025-11-04T13:41:09.827Z" }, + { url = "https://files.pythonhosted.org/packages/18/66/e9db17a9a763d72f03de903883c057b2592c09509ccfe468187f2a2eef29/pydantic_core-2.41.5-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2782c870e99878c634505236d81e5443092fba820f0373997ff75f90f68cd553", size = 2180680, upload-time = "2025-11-04T13:41:12.379Z" }, + { url = "https://files.pythonhosted.org/packages/d3/9e/3ce66cebb929f3ced22be85d4c2399b8e85b622db77dad36b73c5387f8f8/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:0177272f88ab8312479336e1d777f6b124537d47f2123f89cb37e0accea97f90", size = 2138960, upload-time = "2025-11-04T13:41:14.627Z" }, + { url = "https://files.pythonhosted.org/packages/a6/62/205a998f4327d2079326b01abee48e502ea739d174f0a89295c481a2272e/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:63510af5e38f8955b8ee5687740d6ebf7c2a0886d15a6d65c32814613681bc07", size = 2339102, upload-time = "2025-11-04T13:41:16.868Z" }, + { url = "https://files.pythonhosted.org/packages/3c/0d/f05e79471e889d74d3d88f5bd20d0ed189ad94c2423d81ff8d0000aab4ff/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:e56ba91f47764cc14f1daacd723e3e82d1a89d783f0f5afe9c364b8bb491ccdb", size = 2326039, upload-time = "2025-11-04T13:41:18.934Z" }, + { url = "https://files.pythonhosted.org/packages/ec/e1/e08a6208bb100da7e0c4b288eed624a703f4d129bde2da475721a80cab32/pydantic_core-2.41.5-cp314-cp314-win32.whl", hash = "sha256:aec5cf2fd867b4ff45b9959f8b20ea3993fc93e63c7363fe6851424c8a7e7c23", size = 1995126, upload-time = "2025-11-04T13:41:21.418Z" }, + { url = "https://files.pythonhosted.org/packages/48/5d/56ba7b24e9557f99c9237e29f5c09913c81eeb2f3217e40e922353668092/pydantic_core-2.41.5-cp314-cp314-win_amd64.whl", hash = "sha256:8e7c86f27c585ef37c35e56a96363ab8de4e549a95512445b85c96d3e2f7c1bf", size = 2015489, upload-time = "2025-11-04T13:41:24.076Z" }, + { url = "https://files.pythonhosted.org/packages/4e/bb/f7a190991ec9e3e0ba22e4993d8755bbc4a32925c0b5b42775c03e8148f9/pydantic_core-2.41.5-cp314-cp314-win_arm64.whl", hash = "sha256:e672ba74fbc2dc8eea59fb6d4aed6845e6905fc2a8afe93175d94a83ba2a01a0", size = 1977288, upload-time = "2025-11-04T13:41:26.33Z" }, + { url = "https://files.pythonhosted.org/packages/92/ed/77542d0c51538e32e15afe7899d79efce4b81eee631d99850edc2f5e9349/pydantic_core-2.41.5-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8566def80554c3faa0e65ac30ab0932b9e3a5cd7f8323764303d468e5c37595a", size = 2120255, upload-time = "2025-11-04T13:41:28.569Z" }, + { url = "https://files.pythonhosted.org/packages/bb/3d/6913dde84d5be21e284439676168b28d8bbba5600d838b9dca99de0fad71/pydantic_core-2.41.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b80aa5095cd3109962a298ce14110ae16b8c1aece8b72f9dafe81cf597ad80b3", size = 1863760, upload-time = "2025-11-04T13:41:31.055Z" }, + { url = "https://files.pythonhosted.org/packages/5a/f0/e5e6b99d4191da102f2b0eb9687aaa7f5bea5d9964071a84effc3e40f997/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3006c3dd9ba34b0c094c544c6006cc79e87d8612999f1a5d43b769b89181f23c", size = 1878092, upload-time = "2025-11-04T13:41:33.21Z" }, + { url = "https://files.pythonhosted.org/packages/71/48/36fb760642d568925953bcc8116455513d6e34c4beaa37544118c36aba6d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:72f6c8b11857a856bcfa48c86f5368439f74453563f951e473514579d44aa612", size = 2053385, upload-time = "2025-11-04T13:41:35.508Z" }, + { url = "https://files.pythonhosted.org/packages/20/25/92dc684dd8eb75a234bc1c764b4210cf2646479d54b47bf46061657292a8/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5cb1b2f9742240e4bb26b652a5aeb840aa4b417c7748b6f8387927bc6e45e40d", size = 2218832, upload-time = "2025-11-04T13:41:37.732Z" }, + { url = "https://files.pythonhosted.org/packages/e2/09/f53e0b05023d3e30357d82eb35835d0f6340ca344720a4599cd663dca599/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd3d54f38609ff308209bd43acea66061494157703364ae40c951f83ba99a1a9", size = 2327585, upload-time = "2025-11-04T13:41:40Z" }, + { url = "https://files.pythonhosted.org/packages/aa/4e/2ae1aa85d6af35a39b236b1b1641de73f5a6ac4d5a7509f77b814885760c/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ff4321e56e879ee8d2a879501c8e469414d948f4aba74a2d4593184eb326660", size = 2041078, upload-time = "2025-11-04T13:41:42.323Z" }, + { url = "https://files.pythonhosted.org/packages/cd/13/2e215f17f0ef326fc72afe94776edb77525142c693767fc347ed6288728d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d0d2568a8c11bf8225044aa94409e21da0cb09dcdafe9ecd10250b2baad531a9", size = 2173914, upload-time = "2025-11-04T13:41:45.221Z" }, + { url = "https://files.pythonhosted.org/packages/02/7a/f999a6dcbcd0e5660bc348a3991c8915ce6599f4f2c6ac22f01d7a10816c/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:a39455728aabd58ceabb03c90e12f71fd30fa69615760a075b9fec596456ccc3", size = 2129560, upload-time = "2025-11-04T13:41:47.474Z" }, + { url = "https://files.pythonhosted.org/packages/3a/b1/6c990ac65e3b4c079a4fb9f5b05f5b013afa0f4ed6780a3dd236d2cbdc64/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:239edca560d05757817c13dc17c50766136d21f7cd0fac50295499ae24f90fdf", size = 2329244, upload-time = "2025-11-04T13:41:49.992Z" }, + { url = "https://files.pythonhosted.org/packages/d9/02/3c562f3a51afd4d88fff8dffb1771b30cfdfd79befd9883ee094f5b6c0d8/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:2a5e06546e19f24c6a96a129142a75cee553cc018ffee48a460059b1185f4470", size = 2331955, upload-time = "2025-11-04T13:41:54.079Z" }, + { url = "https://files.pythonhosted.org/packages/5c/96/5fb7d8c3c17bc8c62fdb031c47d77a1af698f1d7a406b0f79aaa1338f9ad/pydantic_core-2.41.5-cp314-cp314t-win32.whl", hash = "sha256:b4ececa40ac28afa90871c2cc2b9ffd2ff0bf749380fbdf57d165fd23da353aa", size = 1988906, upload-time = "2025-11-04T13:41:56.606Z" }, + { url = "https://files.pythonhosted.org/packages/22/ed/182129d83032702912c2e2d8bbe33c036f342cc735737064668585dac28f/pydantic_core-2.41.5-cp314-cp314t-win_amd64.whl", hash = "sha256:80aa89cad80b32a912a65332f64a4450ed00966111b6615ca6816153d3585a8c", size = 1981607, upload-time = "2025-11-04T13:41:58.889Z" }, + { url = "https://files.pythonhosted.org/packages/9f/ed/068e41660b832bb0b1aa5b58011dea2a3fe0ba7861ff38c4d4904c1c1a99/pydantic_core-2.41.5-cp314-cp314t-win_arm64.whl", hash = "sha256:35b44f37a3199f771c3eaa53051bc8a70cd7b54f333531c59e29fd4db5d15008", size = 1974769, upload-time = "2025-11-04T13:42:01.186Z" }, ] [[package]] @@ -2231,9 +2208,9 @@ dependencies = [ { name = "pyyaml" }, { name = "rich" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/20/20/ec455c7d32fde2022805870daf78581c9c493a4fcae6f32204fae5025658/pydantic_evals-1.39.0.tar.gz", hash = "sha256:6f8a754ca84afff3f2b2de9802fb0e12f69d9fc0a0411e2f7c9709fc09fb43b3", size = 47179 } +sdist = { url = "https://files.pythonhosted.org/packages/20/20/ec455c7d32fde2022805870daf78581c9c493a4fcae6f32204fae5025658/pydantic_evals-1.39.0.tar.gz", hash = "sha256:6f8a754ca84afff3f2b2de9802fb0e12f69d9fc0a0411e2f7c9709fc09fb43b3", size = 47179, upload-time = "2025-12-24T03:34:12.477Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f6/c1/6d43ecd3f7acb78a3f683178d40008d486c08583ca848891f000d62c142e/pydantic_evals-1.39.0-py3-none-any.whl", hash = "sha256:18470ade5fea15d17911a517e37ea98700702d9ba011ef2facb707e87eae0564", size = 56347 }, + { url = "https://files.pythonhosted.org/packages/f6/c1/6d43ecd3f7acb78a3f683178d40008d486c08583ca848891f000d62c142e/pydantic_evals-1.39.0-py3-none-any.whl", hash = "sha256:18470ade5fea15d17911a517e37ea98700702d9ba011ef2facb707e87eae0564", size = 56347, upload-time = "2025-12-24T03:34:05.111Z" }, ] [[package]] @@ -2246,9 +2223,9 @@ dependencies = [ { name = "pydantic" }, { name = "typing-inspection" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/09/d5/2f45d1fd2ae0ba89b5a70b3bec8c2e910c4891fe0ed7e4fc896ca7e126a0/pydantic_graph-1.39.0.tar.gz", hash = "sha256:08c6f349dbbade6f4cdaaed02de4e8d75b9a37d44f8238e40a14f94f6a31761f", size = 58453 } +sdist = { url = "https://files.pythonhosted.org/packages/09/d5/2f45d1fd2ae0ba89b5a70b3bec8c2e910c4891fe0ed7e4fc896ca7e126a0/pydantic_graph-1.39.0.tar.gz", hash = "sha256:08c6f349dbbade6f4cdaaed02de4e8d75b9a37d44f8238e40a14f94f6a31761f", size = 58453, upload-time = "2025-12-24T03:34:13.766Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/65/e2/719de1af767863359278e8b69c538dba9a7dbd19bb94111206e57ca34648/pydantic_graph-1.39.0-py3-none-any.whl", hash = "sha256:e0f89fc2c7ab111ae5f38dd2d88c5d26a0784eaabe95735c2b4087b0b512cc2d", size = 72327 }, + { url = "https://files.pythonhosted.org/packages/65/e2/719de1af767863359278e8b69c538dba9a7dbd19bb94111206e57ca34648/pydantic_graph-1.39.0-py3-none-any.whl", hash = "sha256:e0f89fc2c7ab111ae5f38dd2d88c5d26a0784eaabe95735c2b4087b0b512cc2d", size = 72327, upload-time = "2025-12-24T03:34:06.476Z" }, ] [[package]] @@ -2260,9 +2237,9 @@ dependencies = [ { name = "python-dotenv" }, { name = "typing-inspection" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/43/4b/ac7e0aae12027748076d72a8764ff1c9d82ca75a7a52622e67ed3f765c54/pydantic_settings-2.12.0.tar.gz", hash = "sha256:005538ef951e3c2a68e1c08b292b5f2e71490def8589d4221b95dab00dafcfd0", size = 194184 } +sdist = { url = "https://files.pythonhosted.org/packages/43/4b/ac7e0aae12027748076d72a8764ff1c9d82ca75a7a52622e67ed3f765c54/pydantic_settings-2.12.0.tar.gz", hash = "sha256:005538ef951e3c2a68e1c08b292b5f2e71490def8589d4221b95dab00dafcfd0", size = 194184, upload-time = "2025-11-10T14:25:47.013Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c1/60/5d4751ba3f4a40a6891f24eec885f51afd78d208498268c734e256fb13c4/pydantic_settings-2.12.0-py3-none-any.whl", hash = "sha256:fddb9fd99a5b18da837b29710391e945b1e30c135477f484084ee513adb93809", size = 51880 }, + { url = "https://files.pythonhosted.org/packages/c1/60/5d4751ba3f4a40a6891f24eec885f51afd78d208498268c734e256fb13c4/pydantic_settings-2.12.0-py3-none-any.whl", hash = "sha256:fddb9fd99a5b18da837b29710391e945b1e30c135477f484084ee513adb93809", size = 51880, upload-time = "2025-11-10T14:25:45.546Z" }, ] [[package]] @@ -2283,27 +2260,27 @@ dependencies = [ { name = "typer" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/e0/c5/61dcfce4d50b66a3f09743294d37fab598b81bb0975054b7f732da9243ec/pydocket-0.16.3.tar.gz", hash = "sha256:78e9da576de09e9f3f410d2471ef1c679b7741ddd21b586c97a13872b69bd265", size = 297080 } +sdist = { url = "https://files.pythonhosted.org/packages/e0/c5/61dcfce4d50b66a3f09743294d37fab598b81bb0975054b7f732da9243ec/pydocket-0.16.3.tar.gz", hash = "sha256:78e9da576de09e9f3f410d2471ef1c679b7741ddd21b586c97a13872b69bd265", size = 297080, upload-time = "2025-12-23T23:37:33.32Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/2c/94/93b7f5981aa04f922e0d9ce7326a4587866ec7e39f7c180ffcf408e66ee8/pydocket-0.16.3-py3-none-any.whl", hash = "sha256:e2b50925356e7cd535286255195458ac7bba15f25293356651b36d223db5dd7c", size = 67087 }, + { url = "https://files.pythonhosted.org/packages/2c/94/93b7f5981aa04f922e0d9ce7326a4587866ec7e39f7c180ffcf408e66ee8/pydocket-0.16.3-py3-none-any.whl", hash = "sha256:e2b50925356e7cd535286255195458ac7bba15f25293356651b36d223db5dd7c", size = 67087, upload-time = "2025-12-23T23:37:31.829Z" }, ] [[package]] name = "pygments" version = "2.19.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631 } +sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217 }, + { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, ] [[package]] name = "pyjwt" version = "2.10.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e7/46/bd74733ff231675599650d3e47f361794b22ef3e3770998dda30d3b63726/pyjwt-2.10.1.tar.gz", hash = "sha256:3cc5772eb20009233caf06e9d8a0577824723b44e6648ee0a2aedb6cf9381953", size = 87785 } +sdist = { url = "https://files.pythonhosted.org/packages/e7/46/bd74733ff231675599650d3e47f361794b22ef3e3770998dda30d3b63726/pyjwt-2.10.1.tar.gz", hash = "sha256:3cc5772eb20009233caf06e9d8a0577824723b44e6648ee0a2aedb6cf9381953", size = 87785, upload-time = "2024-11-28T03:43:29.933Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/61/ad/689f02752eeec26aed679477e80e632ef1b682313be70793d798c1d5fc8f/PyJWT-2.10.1-py3-none-any.whl", hash = "sha256:dcdd193e30abefd5debf142f9adfcdd2b58004e644f25406ffaebd50bd98dacb", size = 22997 }, + { url = "https://files.pythonhosted.org/packages/61/ad/689f02752eeec26aed679477e80e632ef1b682313be70793d798c1d5fc8f/PyJWT-2.10.1-py3-none-any.whl", hash = "sha256:dcdd193e30abefd5debf142f9adfcdd2b58004e644f25406ffaebd50bd98dacb", size = 22997, upload-time = "2024-11-28T03:43:27.893Z" }, ] [package.optional-dependencies] @@ -2315,9 +2292,9 @@ crypto = [ name = "pyperclip" version = "1.11.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e8/52/d87eba7cb129b81563019d1679026e7a112ef76855d6159d24754dbd2a51/pyperclip-1.11.0.tar.gz", hash = "sha256:244035963e4428530d9e3a6101a1ef97209c6825edab1567beac148ccc1db1b6", size = 12185 } +sdist = { url = "https://files.pythonhosted.org/packages/e8/52/d87eba7cb129b81563019d1679026e7a112ef76855d6159d24754dbd2a51/pyperclip-1.11.0.tar.gz", hash = "sha256:244035963e4428530d9e3a6101a1ef97209c6825edab1567beac148ccc1db1b6", size = 12185, upload-time = "2025-09-26T14:40:37.245Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/df/80/fc9d01d5ed37ba4c42ca2b55b4339ae6e200b456be3a1aaddf4a9fa99b8c/pyperclip-1.11.0-py3-none-any.whl", hash = "sha256:299403e9ff44581cb9ba2ffeed69c7aa96a008622ad0c46cb575ca75b5b84273", size = 11063 }, + { url = "https://files.pythonhosted.org/packages/df/80/fc9d01d5ed37ba4c42ca2b55b4339ae6e200b456be3a1aaddf4a9fa99b8c/pyperclip-1.11.0-py3-none-any.whl", hash = "sha256:299403e9ff44581cb9ba2ffeed69c7aa96a008622ad0c46cb575ca75b5b84273", size = 11063, upload-time = "2025-09-26T14:40:36.069Z" }, ] [[package]] @@ -2327,36 +2304,36 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "six" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432 } +sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892 }, + { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" }, ] [[package]] name = "python-dotenv" version = "1.2.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f0/26/19cadc79a718c5edbec86fd4919a6b6d3f681039a2f6d66d14be94e75fb9/python_dotenv-1.2.1.tar.gz", hash = "sha256:42667e897e16ab0d66954af0e60a9caa94f0fd4ecf3aaf6d2d260eec1aa36ad6", size = 44221 } +sdist = { url = "https://files.pythonhosted.org/packages/f0/26/19cadc79a718c5edbec86fd4919a6b6d3f681039a2f6d66d14be94e75fb9/python_dotenv-1.2.1.tar.gz", hash = "sha256:42667e897e16ab0d66954af0e60a9caa94f0fd4ecf3aaf6d2d260eec1aa36ad6", size = 44221, upload-time = "2025-10-26T15:12:10.434Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/14/1b/a298b06749107c305e1fe0f814c6c74aea7b2f1e10989cb30f544a1b3253/python_dotenv-1.2.1-py3-none-any.whl", hash = "sha256:b81ee9561e9ca4004139c6cbba3a238c32b03e4894671e181b671e8cb8425d61", size = 21230 }, + { url = "https://files.pythonhosted.org/packages/14/1b/a298b06749107c305e1fe0f814c6c74aea7b2f1e10989cb30f544a1b3253/python_dotenv-1.2.1-py3-none-any.whl", hash = "sha256:b81ee9561e9ca4004139c6cbba3a238c32b03e4894671e181b671e8cb8425d61", size = 21230, upload-time = "2025-10-26T15:12:09.109Z" }, ] [[package]] name = "python-json-logger" version = "4.0.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/29/bf/eca6a3d43db1dae7070f70e160ab20b807627ba953663ba07928cdd3dc58/python_json_logger-4.0.0.tar.gz", hash = "sha256:f58e68eb46e1faed27e0f574a55a0455eecd7b8a5b88b85a784519ba3cff047f", size = 17683 } +sdist = { url = "https://files.pythonhosted.org/packages/29/bf/eca6a3d43db1dae7070f70e160ab20b807627ba953663ba07928cdd3dc58/python_json_logger-4.0.0.tar.gz", hash = "sha256:f58e68eb46e1faed27e0f574a55a0455eecd7b8a5b88b85a784519ba3cff047f", size = 17683, upload-time = "2025-10-06T04:15:18.984Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/51/e5/fecf13f06e5e5f67e8837d777d1bc43fac0ed2b77a676804df5c34744727/python_json_logger-4.0.0-py3-none-any.whl", hash = "sha256:af09c9daf6a813aa4cc7180395f50f2a9e5fa056034c9953aec92e381c5ba1e2", size = 15548 }, + { url = "https://files.pythonhosted.org/packages/51/e5/fecf13f06e5e5f67e8837d777d1bc43fac0ed2b77a676804df5c34744727/python_json_logger-4.0.0-py3-none-any.whl", hash = "sha256:af09c9daf6a813aa4cc7180395f50f2a9e5fa056034c9953aec92e381c5ba1e2", size = 15548, upload-time = "2025-10-06T04:15:17.553Z" }, ] [[package]] name = "python-multipart" version = "0.0.21" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/78/96/804520d0850c7db98e5ccb70282e29208723f0964e88ffd9d0da2f52ea09/python_multipart-0.0.21.tar.gz", hash = "sha256:7137ebd4d3bbf70ea1622998f902b97a29434a9e8dc40eb203bbcf7c2a2cba92", size = 37196 } +sdist = { url = "https://files.pythonhosted.org/packages/78/96/804520d0850c7db98e5ccb70282e29208723f0964e88ffd9d0da2f52ea09/python_multipart-0.0.21.tar.gz", hash = "sha256:7137ebd4d3bbf70ea1622998f902b97a29434a9e8dc40eb203bbcf7c2a2cba92", size = 37196, upload-time = "2025-12-17T09:24:22.446Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/aa/76/03af049af4dcee5d27442f71b6924f01f3efb5d2bd34f23fcd563f2cc5f5/python_multipart-0.0.21-py3-none-any.whl", hash = "sha256:cf7a6713e01c87aa35387f4774e812c4361150938d20d232800f75ffcf266090", size = 24541 }, + { url = "https://files.pythonhosted.org/packages/aa/76/03af049af4dcee5d27442f71b6924f01f3efb5d2bd34f23fcd563f2cc5f5/python_multipart-0.0.21-py3-none-any.whl", hash = "sha256:cf7a6713e01c87aa35387f4774e812c4361150938d20d232800f75ffcf266090", size = 24541, upload-time = "2025-12-17T09:24:21.153Z" }, ] [[package]] @@ -2364,66 +2341,66 @@ name = "pywin32" version = "311" source = { registry = "https://pypi.org/simple" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a5/be/3fd5de0979fcb3994bfee0d65ed8ca9506a8a1260651b86174f6a86f52b3/pywin32-311-cp313-cp313-win32.whl", hash = "sha256:f95ba5a847cba10dd8c4d8fefa9f2a6cf283b8b88ed6178fa8a6c1ab16054d0d", size = 8705700 }, - { url = "https://files.pythonhosted.org/packages/e3/28/e0a1909523c6890208295a29e05c2adb2126364e289826c0a8bc7297bd5c/pywin32-311-cp313-cp313-win_amd64.whl", hash = "sha256:718a38f7e5b058e76aee1c56ddd06908116d35147e133427e59a3983f703a20d", size = 9494700 }, - { url = "https://files.pythonhosted.org/packages/04/bf/90339ac0f55726dce7d794e6d79a18a91265bdf3aa70b6b9ca52f35e022a/pywin32-311-cp313-cp313-win_arm64.whl", hash = "sha256:7b4075d959648406202d92a2310cb990fea19b535c7f4a78d3f5e10b926eeb8a", size = 8709318 }, - { url = "https://files.pythonhosted.org/packages/c9/31/097f2e132c4f16d99a22bfb777e0fd88bd8e1c634304e102f313af69ace5/pywin32-311-cp314-cp314-win32.whl", hash = "sha256:b7a2c10b93f8986666d0c803ee19b5990885872a7de910fc460f9b0c2fbf92ee", size = 8840714 }, - { url = "https://files.pythonhosted.org/packages/90/4b/07c77d8ba0e01349358082713400435347df8426208171ce297da32c313d/pywin32-311-cp314-cp314-win_amd64.whl", hash = "sha256:3aca44c046bd2ed8c90de9cb8427f581c479e594e99b5c0bb19b29c10fd6cb87", size = 9656800 }, - { url = "https://files.pythonhosted.org/packages/c0/d2/21af5c535501a7233e734b8af901574572da66fcc254cb35d0609c9080dd/pywin32-311-cp314-cp314-win_arm64.whl", hash = "sha256:a508e2d9025764a8270f93111a970e1d0fbfc33f4153b388bb649b7eec4f9b42", size = 8932540 }, + { url = "https://files.pythonhosted.org/packages/a5/be/3fd5de0979fcb3994bfee0d65ed8ca9506a8a1260651b86174f6a86f52b3/pywin32-311-cp313-cp313-win32.whl", hash = "sha256:f95ba5a847cba10dd8c4d8fefa9f2a6cf283b8b88ed6178fa8a6c1ab16054d0d", size = 8705700, upload-time = "2025-07-14T20:13:26.471Z" }, + { url = "https://files.pythonhosted.org/packages/e3/28/e0a1909523c6890208295a29e05c2adb2126364e289826c0a8bc7297bd5c/pywin32-311-cp313-cp313-win_amd64.whl", hash = "sha256:718a38f7e5b058e76aee1c56ddd06908116d35147e133427e59a3983f703a20d", size = 9494700, upload-time = "2025-07-14T20:13:28.243Z" }, + { url = "https://files.pythonhosted.org/packages/04/bf/90339ac0f55726dce7d794e6d79a18a91265bdf3aa70b6b9ca52f35e022a/pywin32-311-cp313-cp313-win_arm64.whl", hash = "sha256:7b4075d959648406202d92a2310cb990fea19b535c7f4a78d3f5e10b926eeb8a", size = 8709318, upload-time = "2025-07-14T20:13:30.348Z" }, + { url = "https://files.pythonhosted.org/packages/c9/31/097f2e132c4f16d99a22bfb777e0fd88bd8e1c634304e102f313af69ace5/pywin32-311-cp314-cp314-win32.whl", hash = "sha256:b7a2c10b93f8986666d0c803ee19b5990885872a7de910fc460f9b0c2fbf92ee", size = 8840714, upload-time = "2025-07-14T20:13:32.449Z" }, + { url = "https://files.pythonhosted.org/packages/90/4b/07c77d8ba0e01349358082713400435347df8426208171ce297da32c313d/pywin32-311-cp314-cp314-win_amd64.whl", hash = "sha256:3aca44c046bd2ed8c90de9cb8427f581c479e594e99b5c0bb19b29c10fd6cb87", size = 9656800, upload-time = "2025-07-14T20:13:34.312Z" }, + { url = "https://files.pythonhosted.org/packages/c0/d2/21af5c535501a7233e734b8af901574572da66fcc254cb35d0609c9080dd/pywin32-311-cp314-cp314-win_arm64.whl", hash = "sha256:a508e2d9025764a8270f93111a970e1d0fbfc33f4153b388bb649b7eec4f9b42", size = 8932540, upload-time = "2025-07-14T20:13:36.379Z" }, ] [[package]] name = "pywin32-ctypes" version = "0.2.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/85/9f/01a1a99704853cb63f253eea009390c88e7131c67e66a0a02099a8c917cb/pywin32-ctypes-0.2.3.tar.gz", hash = "sha256:d162dc04946d704503b2edc4d55f3dba5c1d539ead017afa00142c38b9885755", size = 29471 } +sdist = { url = "https://files.pythonhosted.org/packages/85/9f/01a1a99704853cb63f253eea009390c88e7131c67e66a0a02099a8c917cb/pywin32-ctypes-0.2.3.tar.gz", hash = "sha256:d162dc04946d704503b2edc4d55f3dba5c1d539ead017afa00142c38b9885755", size = 29471, upload-time = "2024-08-14T10:15:34.626Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/de/3d/8161f7711c017e01ac9f008dfddd9410dff3674334c233bde66e7ba65bbf/pywin32_ctypes-0.2.3-py3-none-any.whl", hash = "sha256:8a1513379d709975552d202d942d9837758905c8d01eb82b8bcc30918929e7b8", size = 30756 }, + { url = "https://files.pythonhosted.org/packages/de/3d/8161f7711c017e01ac9f008dfddd9410dff3674334c233bde66e7ba65bbf/pywin32_ctypes-0.2.3-py3-none-any.whl", hash = "sha256:8a1513379d709975552d202d942d9837758905c8d01eb82b8bcc30918929e7b8", size = 30756, upload-time = "2024-08-14T10:15:33.187Z" }, ] [[package]] name = "pyyaml" version = "6.0.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669 }, - { url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252 }, - { url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081 }, - { url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159 }, - { url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626 }, - { url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613 }, - { url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115 }, - { url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427 }, - { url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090 }, - { url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246 }, - { url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814 }, - { url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809 }, - { url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454 }, - { url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355 }, - { url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175 }, - { url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228 }, - { url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194 }, - { url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429 }, - { url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912 }, - { url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108 }, - { url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641 }, - { url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901 }, - { url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132 }, - { url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261 }, - { url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272 }, - { url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923 }, - { url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062 }, - { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341 }, +sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" }, + { url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" }, + { url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" }, + { url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" }, + { url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" }, + { url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" }, + { url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" }, + { url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" }, + { url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" }, + { url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" }, + { url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" }, + { url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" }, + { url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" }, + { url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" }, + { url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" }, + { url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" }, + { url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" }, + { url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" }, + { url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" }, + { url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" }, + { url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" }, + { url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" }, + { url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" }, + { url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" }, + { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" }, ] [[package]] name = "redis" version = "7.1.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/43/c8/983d5c6579a411d8a99bc5823cc5712768859b5ce2c8afe1a65b37832c81/redis-7.1.0.tar.gz", hash = "sha256:b1cc3cfa5a2cb9c2ab3ba700864fb0ad75617b41f01352ce5779dabf6d5f9c3c", size = 4796669 } +sdist = { url = "https://files.pythonhosted.org/packages/43/c8/983d5c6579a411d8a99bc5823cc5712768859b5ce2c8afe1a65b37832c81/redis-7.1.0.tar.gz", hash = "sha256:b1cc3cfa5a2cb9c2ab3ba700864fb0ad75617b41f01352ce5779dabf6d5f9c3c", size = 4796669, upload-time = "2025-11-19T15:54:39.961Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/89/f0/8956f8a86b20d7bb9d6ac0187cf4cd54d8065bc9a1a09eb8011d4d326596/redis-7.1.0-py3-none-any.whl", hash = "sha256:23c52b208f92b56103e17c5d06bdc1a6c2c0b3106583985a76a18f83b265de2b", size = 354159 }, + { url = "https://files.pythonhosted.org/packages/89/f0/8956f8a86b20d7bb9d6ac0187cf4cd54d8065bc9a1a09eb8011d4d326596/redis-7.1.0-py3-none-any.whl", hash = "sha256:23c52b208f92b56103e17c5d06bdc1a6c2c0b3106583985a76a18f83b265de2b", size = 354159, upload-time = "2025-11-19T15:54:38.064Z" }, ] [[package]] @@ -2434,73 +2411,73 @@ dependencies = [ { name = "attrs" }, { name = "rpds-py" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/2f/db/98b5c277be99dd18bfd91dd04e1b759cad18d1a338188c936e92f921c7e2/referencing-0.36.2.tar.gz", hash = "sha256:df2e89862cd09deabbdba16944cc3f10feb6b3e6f18e902f7cc25609a34775aa", size = 74744 } +sdist = { url = "https://files.pythonhosted.org/packages/2f/db/98b5c277be99dd18bfd91dd04e1b759cad18d1a338188c936e92f921c7e2/referencing-0.36.2.tar.gz", hash = "sha256:df2e89862cd09deabbdba16944cc3f10feb6b3e6f18e902f7cc25609a34775aa", size = 74744, upload-time = "2025-01-25T08:48:16.138Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c1/b1/3baf80dc6d2b7bc27a95a67752d0208e410351e3feb4eb78de5f77454d8d/referencing-0.36.2-py3-none-any.whl", hash = "sha256:e8699adbbf8b5c7de96d8ffa0eb5c158b3beafce084968e2ea8bb08c6794dcd0", size = 26775 }, + { url = "https://files.pythonhosted.org/packages/c1/b1/3baf80dc6d2b7bc27a95a67752d0208e410351e3feb4eb78de5f77454d8d/referencing-0.36.2-py3-none-any.whl", hash = "sha256:e8699adbbf8b5c7de96d8ffa0eb5c158b3beafce084968e2ea8bb08c6794dcd0", size = 26775, upload-time = "2025-01-25T08:48:14.241Z" }, ] [[package]] name = "regex" version = "2025.11.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/cc/a9/546676f25e573a4cf00fe8e119b78a37b6a8fe2dc95cda877b30889c9c45/regex-2025.11.3.tar.gz", hash = "sha256:1fedc720f9bb2494ce31a58a1631f9c82df6a09b49c19517ea5cc280b4541e01", size = 414669 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e1/a7/dda24ebd49da46a197436ad96378f17df30ceb40e52e859fc42cac45b850/regex-2025.11.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:c1e448051717a334891f2b9a620fe36776ebf3dd8ec46a0b877c8ae69575feb4", size = 489081 }, - { url = "https://files.pythonhosted.org/packages/19/22/af2dc751aacf88089836aa088a1a11c4f21a04707eb1b0478e8e8fb32847/regex-2025.11.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:9b5aca4d5dfd7fbfbfbdaf44850fcc7709a01146a797536a8f84952e940cca76", size = 291123 }, - { url = "https://files.pythonhosted.org/packages/a3/88/1a3ea5672f4b0a84802ee9891b86743438e7c04eb0b8f8c4e16a42375327/regex-2025.11.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:04d2765516395cf7dda331a244a3282c0f5ae96075f728629287dfa6f76ba70a", size = 288814 }, - { url = "https://files.pythonhosted.org/packages/fb/8c/f5987895bf42b8ddeea1b315c9fedcfe07cadee28b9c98cf50d00adcb14d/regex-2025.11.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5d9903ca42bfeec4cebedba8022a7c97ad2aab22e09573ce9976ba01b65e4361", size = 798592 }, - { url = "https://files.pythonhosted.org/packages/99/2a/6591ebeede78203fa77ee46a1c36649e02df9eaa77a033d1ccdf2fcd5d4e/regex-2025.11.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:639431bdc89d6429f6721625e8129413980ccd62e9d3f496be618a41d205f160", size = 864122 }, - { url = "https://files.pythonhosted.org/packages/94/d6/be32a87cf28cf8ed064ff281cfbd49aefd90242a83e4b08b5a86b38e8eb4/regex-2025.11.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f117efad42068f9715677c8523ed2be1518116d1c49b1dd17987716695181efe", size = 912272 }, - { url = "https://files.pythonhosted.org/packages/62/11/9bcef2d1445665b180ac7f230406ad80671f0fc2a6ffb93493b5dd8cd64c/regex-2025.11.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4aecb6f461316adf9f1f0f6a4a1a3d79e045f9b71ec76055a791affa3b285850", size = 803497 }, - { url = "https://files.pythonhosted.org/packages/e5/a7/da0dc273d57f560399aa16d8a68ae7f9b57679476fc7ace46501d455fe84/regex-2025.11.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:3b3a5f320136873cc5561098dfab677eea139521cb9a9e8db98b7e64aef44cbc", size = 787892 }, - { url = "https://files.pythonhosted.org/packages/da/4b/732a0c5a9736a0b8d6d720d4945a2f1e6f38f87f48f3173559f53e8d5d82/regex-2025.11.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:75fa6f0056e7efb1f42a1c34e58be24072cb9e61a601340cc1196ae92326a4f9", size = 858462 }, - { url = "https://files.pythonhosted.org/packages/0c/f5/a2a03df27dc4c2d0c769220f5110ba8c4084b0bfa9ab0f9b4fcfa3d2b0fc/regex-2025.11.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:dbe6095001465294f13f1adcd3311e50dd84e5a71525f20a10bd16689c61ce0b", size = 850528 }, - { url = "https://files.pythonhosted.org/packages/d6/09/e1cd5bee3841c7f6eb37d95ca91cdee7100b8f88b81e41c2ef426910891a/regex-2025.11.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:454d9b4ae7881afbc25015b8627c16d88a597479b9dea82b8c6e7e2e07240dc7", size = 789866 }, - { url = "https://files.pythonhosted.org/packages/eb/51/702f5ea74e2a9c13d855a6a85b7f80c30f9e72a95493260193c07f3f8d74/regex-2025.11.3-cp313-cp313-win32.whl", hash = "sha256:28ba4d69171fc6e9896337d4fc63a43660002b7da53fc15ac992abcf3410917c", size = 266189 }, - { url = "https://files.pythonhosted.org/packages/8b/00/6e29bb314e271a743170e53649db0fdb8e8ff0b64b4f425f5602f4eb9014/regex-2025.11.3-cp313-cp313-win_amd64.whl", hash = "sha256:bac4200befe50c670c405dc33af26dad5a3b6b255dd6c000d92fe4629f9ed6a5", size = 277054 }, - { url = "https://files.pythonhosted.org/packages/25/f1/b156ff9f2ec9ac441710764dda95e4edaf5f36aca48246d1eea3f1fd96ec/regex-2025.11.3-cp313-cp313-win_arm64.whl", hash = "sha256:2292cd5a90dab247f9abe892ac584cb24f0f54680c73fcb4a7493c66c2bf2467", size = 270325 }, - { url = "https://files.pythonhosted.org/packages/20/28/fd0c63357caefe5680b8ea052131acbd7f456893b69cc2a90cc3e0dc90d4/regex-2025.11.3-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:1eb1ebf6822b756c723e09f5186473d93236c06c579d2cc0671a722d2ab14281", size = 491984 }, - { url = "https://files.pythonhosted.org/packages/df/ec/7014c15626ab46b902b3bcc4b28a7bae46d8f281fc7ea9c95e22fcaaa917/regex-2025.11.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:1e00ec2970aab10dc5db34af535f21fcf32b4a31d99e34963419636e2f85ae39", size = 292673 }, - { url = "https://files.pythonhosted.org/packages/23/ab/3b952ff7239f20d05f1f99e9e20188513905f218c81d52fb5e78d2bf7634/regex-2025.11.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a4cb042b615245d5ff9b3794f56be4138b5adc35a4166014d31d1814744148c7", size = 291029 }, - { url = "https://files.pythonhosted.org/packages/21/7e/3dc2749fc684f455f162dcafb8a187b559e2614f3826877d3844a131f37b/regex-2025.11.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:44f264d4bf02f3176467d90b294d59bf1db9fe53c141ff772f27a8b456b2a9ed", size = 807437 }, - { url = "https://files.pythonhosted.org/packages/1b/0b/d529a85ab349c6a25d1ca783235b6e3eedf187247eab536797021f7126c6/regex-2025.11.3-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7be0277469bf3bd7a34a9c57c1b6a724532a0d235cd0dc4e7f4316f982c28b19", size = 873368 }, - { url = "https://files.pythonhosted.org/packages/7d/18/2d868155f8c9e3e9d8f9e10c64e9a9f496bb8f7e037a88a8bed26b435af6/regex-2025.11.3-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0d31e08426ff4b5b650f68839f5af51a92a5b51abd8554a60c2fbc7c71f25d0b", size = 914921 }, - { url = "https://files.pythonhosted.org/packages/2d/71/9d72ff0f354fa783fe2ba913c8734c3b433b86406117a8db4ea2bf1c7a2f/regex-2025.11.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e43586ce5bd28f9f285a6e729466841368c4a0353f6fd08d4ce4630843d3648a", size = 812708 }, - { url = "https://files.pythonhosted.org/packages/e7/19/ce4bf7f5575c97f82b6e804ffb5c4e940c62609ab2a0d9538d47a7fdf7d4/regex-2025.11.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:0f9397d561a4c16829d4e6ff75202c1c08b68a3bdbfe29dbfcdb31c9830907c6", size = 795472 }, - { url = "https://files.pythonhosted.org/packages/03/86/fd1063a176ffb7b2315f9a1b08d17b18118b28d9df163132615b835a26ee/regex-2025.11.3-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:dd16e78eb18ffdb25ee33a0682d17912e8cc8a770e885aeee95020046128f1ce", size = 868341 }, - { url = "https://files.pythonhosted.org/packages/12/43/103fb2e9811205e7386366501bc866a164a0430c79dd59eac886a2822950/regex-2025.11.3-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:ffcca5b9efe948ba0661e9df0fa50d2bc4b097c70b9810212d6b62f05d83b2dd", size = 854666 }, - { url = "https://files.pythonhosted.org/packages/7d/22/e392e53f3869b75804762c7c848bd2dd2abf2b70fb0e526f58724638bd35/regex-2025.11.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:c56b4d162ca2b43318ac671c65bd4d563e841a694ac70e1a976ac38fcf4ca1d2", size = 799473 }, - { url = "https://files.pythonhosted.org/packages/4f/f9/8bd6b656592f925b6845fcbb4d57603a3ac2fb2373344ffa1ed70aa6820a/regex-2025.11.3-cp313-cp313t-win32.whl", hash = "sha256:9ddc42e68114e161e51e272f667d640f97e84a2b9ef14b7477c53aac20c2d59a", size = 268792 }, - { url = "https://files.pythonhosted.org/packages/e5/87/0e7d603467775ff65cd2aeabf1b5b50cc1c3708556a8b849a2fa4dd1542b/regex-2025.11.3-cp313-cp313t-win_amd64.whl", hash = "sha256:7a7c7fdf755032ffdd72c77e3d8096bdcb0eb92e89e17571a196f03d88b11b3c", size = 280214 }, - { url = "https://files.pythonhosted.org/packages/8d/d0/2afc6f8e94e2b64bfb738a7c2b6387ac1699f09f032d363ed9447fd2bb57/regex-2025.11.3-cp313-cp313t-win_arm64.whl", hash = "sha256:df9eb838c44f570283712e7cff14c16329a9f0fb19ca492d21d4b7528ee6821e", size = 271469 }, - { url = "https://files.pythonhosted.org/packages/31/e9/f6e13de7e0983837f7b6d238ad9458800a874bf37c264f7923e63409944c/regex-2025.11.3-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:9697a52e57576c83139d7c6f213d64485d3df5bf84807c35fa409e6c970801c6", size = 489089 }, - { url = "https://files.pythonhosted.org/packages/a3/5c/261f4a262f1fa65141c1b74b255988bd2fa020cc599e53b080667d591cfc/regex-2025.11.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:e18bc3f73bd41243c9b38a6d9f2366cd0e0137a9aebe2d8ff76c5b67d4c0a3f4", size = 291059 }, - { url = "https://files.pythonhosted.org/packages/8e/57/f14eeb7f072b0e9a5a090d1712741fd8f214ec193dba773cf5410108bb7d/regex-2025.11.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:61a08bcb0ec14ff4e0ed2044aad948d0659604f824cbd50b55e30b0ec6f09c73", size = 288900 }, - { url = "https://files.pythonhosted.org/packages/3c/6b/1d650c45e99a9b327586739d926a1cd4e94666b1bd4af90428b36af66dc7/regex-2025.11.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c9c30003b9347c24bcc210958c5d167b9e4f9be786cb380a7d32f14f9b84674f", size = 799010 }, - { url = "https://files.pythonhosted.org/packages/99/ee/d66dcbc6b628ce4e3f7f0cbbb84603aa2fc0ffc878babc857726b8aab2e9/regex-2025.11.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4e1e592789704459900728d88d41a46fe3969b82ab62945560a31732ffc19a6d", size = 864893 }, - { url = "https://files.pythonhosted.org/packages/bf/2d/f238229f1caba7ac87a6c4153d79947fb0261415827ae0f77c304260c7d3/regex-2025.11.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6538241f45eb5a25aa575dbba1069ad786f68a4f2773a29a2bd3dd1f9de787be", size = 911522 }, - { url = "https://files.pythonhosted.org/packages/bd/3d/22a4eaba214a917c80e04f6025d26143690f0419511e0116508e24b11c9b/regex-2025.11.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bce22519c989bb72a7e6b36a199384c53db7722fe669ba891da75907fe3587db", size = 803272 }, - { url = "https://files.pythonhosted.org/packages/84/b1/03188f634a409353a84b5ef49754b97dbcc0c0f6fd6c8ede505a8960a0a4/regex-2025.11.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:66d559b21d3640203ab9075797a55165d79017520685fb407b9234d72ab63c62", size = 787958 }, - { url = "https://files.pythonhosted.org/packages/99/6a/27d072f7fbf6fadd59c64d210305e1ff865cc3b78b526fd147db768c553b/regex-2025.11.3-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:669dcfb2e38f9e8c69507bace46f4889e3abbfd9b0c29719202883c0a603598f", size = 859289 }, - { url = "https://files.pythonhosted.org/packages/9a/70/1b3878f648e0b6abe023172dacb02157e685564853cc363d9961bcccde4e/regex-2025.11.3-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:32f74f35ff0f25a5021373ac61442edcb150731fbaa28286bbc8bb1582c89d02", size = 850026 }, - { url = "https://files.pythonhosted.org/packages/dd/d5/68e25559b526b8baab8e66839304ede68ff6727237a47727d240006bd0ff/regex-2025.11.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:e6c7a21dffba883234baefe91bc3388e629779582038f75d2a5be918e250f0ed", size = 789499 }, - { url = "https://files.pythonhosted.org/packages/fc/df/43971264857140a350910d4e33df725e8c94dd9dee8d2e4729fa0d63d49e/regex-2025.11.3-cp314-cp314-win32.whl", hash = "sha256:795ea137b1d809eb6836b43748b12634291c0ed55ad50a7d72d21edf1cd565c4", size = 271604 }, - { url = "https://files.pythonhosted.org/packages/01/6f/9711b57dc6894a55faf80a4c1b5aa4f8649805cb9c7aef46f7d27e2b9206/regex-2025.11.3-cp314-cp314-win_amd64.whl", hash = "sha256:9f95fbaa0ee1610ec0fc6b26668e9917a582ba80c52cc6d9ada15e30aa9ab9ad", size = 280320 }, - { url = "https://files.pythonhosted.org/packages/f1/7e/f6eaa207d4377481f5e1775cdeb5a443b5a59b392d0065f3417d31d80f87/regex-2025.11.3-cp314-cp314-win_arm64.whl", hash = "sha256:dfec44d532be4c07088c3de2876130ff0fbeeacaa89a137decbbb5f665855a0f", size = 273372 }, - { url = "https://files.pythonhosted.org/packages/c3/06/49b198550ee0f5e4184271cee87ba4dfd9692c91ec55289e6282f0f86ccf/regex-2025.11.3-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:ba0d8a5d7f04f73ee7d01d974d47c5834f8a1b0224390e4fe7c12a3a92a78ecc", size = 491985 }, - { url = "https://files.pythonhosted.org/packages/ce/bf/abdafade008f0b1c9da10d934034cb670432d6cf6cbe38bbb53a1cfd6cf8/regex-2025.11.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:442d86cf1cfe4faabf97db7d901ef58347efd004934da045c745e7b5bd57ac49", size = 292669 }, - { url = "https://files.pythonhosted.org/packages/f9/ef/0c357bb8edbd2ad8e273fcb9e1761bc37b8acbc6e1be050bebd6475f19c1/regex-2025.11.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:fd0a5e563c756de210bb964789b5abe4f114dacae9104a47e1a649b910361536", size = 291030 }, - { url = "https://files.pythonhosted.org/packages/79/06/edbb67257596649b8fb088d6aeacbcb248ac195714b18a65e018bf4c0b50/regex-2025.11.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bf3490bcbb985a1ae97b2ce9ad1c0f06a852d5b19dde9b07bdf25bf224248c95", size = 807674 }, - { url = "https://files.pythonhosted.org/packages/f4/d9/ad4deccfce0ea336296bd087f1a191543bb99ee1c53093dcd4c64d951d00/regex-2025.11.3-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3809988f0a8b8c9dcc0f92478d6501fac7200b9ec56aecf0ec21f4a2ec4b6009", size = 873451 }, - { url = "https://files.pythonhosted.org/packages/13/75/a55a4724c56ef13e3e04acaab29df26582f6978c000ac9cd6810ad1f341f/regex-2025.11.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f4ff94e58e84aedb9c9fce66d4ef9f27a190285b451420f297c9a09f2b9abee9", size = 914980 }, - { url = "https://files.pythonhosted.org/packages/67/1e/a1657ee15bd9116f70d4a530c736983eed997b361e20ecd8f5ca3759d5c5/regex-2025.11.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7eb542fd347ce61e1321b0a6b945d5701528dca0cd9759c2e3bb8bd57e47964d", size = 812852 }, - { url = "https://files.pythonhosted.org/packages/b8/6f/f7516dde5506a588a561d296b2d0044839de06035bb486b326065b4c101e/regex-2025.11.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:d6c2d5919075a1f2e413c00b056ea0c2f065b3f5fe83c3d07d325ab92dce51d6", size = 795566 }, - { url = "https://files.pythonhosted.org/packages/d9/dd/3d10b9e170cc16fb34cb2cef91513cf3df65f440b3366030631b2984a264/regex-2025.11.3-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:3f8bf11a4827cc7ce5a53d4ef6cddd5ad25595d3c1435ef08f76825851343154", size = 868463 }, - { url = "https://files.pythonhosted.org/packages/f5/8e/935e6beff1695aa9085ff83195daccd72acc82c81793df480f34569330de/regex-2025.11.3-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:22c12d837298651e5550ac1d964e4ff57c3f56965fc1812c90c9fb2028eaf267", size = 854694 }, - { url = "https://files.pythonhosted.org/packages/92/12/10650181a040978b2f5720a6a74d44f841371a3d984c2083fc1752e4acf6/regex-2025.11.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:62ba394a3dda9ad41c7c780f60f6e4a70988741415ae96f6d1bf6c239cf01379", size = 799691 }, - { url = "https://files.pythonhosted.org/packages/67/90/8f37138181c9a7690e7e4cb388debbd389342db3c7381d636d2875940752/regex-2025.11.3-cp314-cp314t-win32.whl", hash = "sha256:4bf146dca15cdd53224a1bf46d628bd7590e4a07fbb69e720d561aea43a32b38", size = 274583 }, - { url = "https://files.pythonhosted.org/packages/8f/cd/867f5ec442d56beb56f5f854f40abcfc75e11d10b11fdb1869dd39c63aaf/regex-2025.11.3-cp314-cp314t-win_amd64.whl", hash = "sha256:adad1a1bcf1c9e76346e091d22d23ac54ef28e1365117d99521631078dfec9de", size = 284286 }, - { url = "https://files.pythonhosted.org/packages/20/31/32c0c4610cbc070362bf1d2e4ea86d1ea29014d400a6d6c2486fcfd57766/regex-2025.11.3-cp314-cp314t-win_arm64.whl", hash = "sha256:c54f768482cef41e219720013cd05933b6f971d9562544d691c68699bf2b6801", size = 274741 }, +sdist = { url = "https://files.pythonhosted.org/packages/cc/a9/546676f25e573a4cf00fe8e119b78a37b6a8fe2dc95cda877b30889c9c45/regex-2025.11.3.tar.gz", hash = "sha256:1fedc720f9bb2494ce31a58a1631f9c82df6a09b49c19517ea5cc280b4541e01", size = 414669, upload-time = "2025-11-03T21:34:22.089Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e1/a7/dda24ebd49da46a197436ad96378f17df30ceb40e52e859fc42cac45b850/regex-2025.11.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:c1e448051717a334891f2b9a620fe36776ebf3dd8ec46a0b877c8ae69575feb4", size = 489081, upload-time = "2025-11-03T21:31:55.9Z" }, + { url = "https://files.pythonhosted.org/packages/19/22/af2dc751aacf88089836aa088a1a11c4f21a04707eb1b0478e8e8fb32847/regex-2025.11.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:9b5aca4d5dfd7fbfbfbdaf44850fcc7709a01146a797536a8f84952e940cca76", size = 291123, upload-time = "2025-11-03T21:31:57.758Z" }, + { url = "https://files.pythonhosted.org/packages/a3/88/1a3ea5672f4b0a84802ee9891b86743438e7c04eb0b8f8c4e16a42375327/regex-2025.11.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:04d2765516395cf7dda331a244a3282c0f5ae96075f728629287dfa6f76ba70a", size = 288814, upload-time = "2025-11-03T21:32:01.12Z" }, + { url = "https://files.pythonhosted.org/packages/fb/8c/f5987895bf42b8ddeea1b315c9fedcfe07cadee28b9c98cf50d00adcb14d/regex-2025.11.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5d9903ca42bfeec4cebedba8022a7c97ad2aab22e09573ce9976ba01b65e4361", size = 798592, upload-time = "2025-11-03T21:32:03.006Z" }, + { url = "https://files.pythonhosted.org/packages/99/2a/6591ebeede78203fa77ee46a1c36649e02df9eaa77a033d1ccdf2fcd5d4e/regex-2025.11.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:639431bdc89d6429f6721625e8129413980ccd62e9d3f496be618a41d205f160", size = 864122, upload-time = "2025-11-03T21:32:04.553Z" }, + { url = "https://files.pythonhosted.org/packages/94/d6/be32a87cf28cf8ed064ff281cfbd49aefd90242a83e4b08b5a86b38e8eb4/regex-2025.11.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f117efad42068f9715677c8523ed2be1518116d1c49b1dd17987716695181efe", size = 912272, upload-time = "2025-11-03T21:32:06.148Z" }, + { url = "https://files.pythonhosted.org/packages/62/11/9bcef2d1445665b180ac7f230406ad80671f0fc2a6ffb93493b5dd8cd64c/regex-2025.11.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4aecb6f461316adf9f1f0f6a4a1a3d79e045f9b71ec76055a791affa3b285850", size = 803497, upload-time = "2025-11-03T21:32:08.162Z" }, + { url = "https://files.pythonhosted.org/packages/e5/a7/da0dc273d57f560399aa16d8a68ae7f9b57679476fc7ace46501d455fe84/regex-2025.11.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:3b3a5f320136873cc5561098dfab677eea139521cb9a9e8db98b7e64aef44cbc", size = 787892, upload-time = "2025-11-03T21:32:09.769Z" }, + { url = "https://files.pythonhosted.org/packages/da/4b/732a0c5a9736a0b8d6d720d4945a2f1e6f38f87f48f3173559f53e8d5d82/regex-2025.11.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:75fa6f0056e7efb1f42a1c34e58be24072cb9e61a601340cc1196ae92326a4f9", size = 858462, upload-time = "2025-11-03T21:32:11.769Z" }, + { url = "https://files.pythonhosted.org/packages/0c/f5/a2a03df27dc4c2d0c769220f5110ba8c4084b0bfa9ab0f9b4fcfa3d2b0fc/regex-2025.11.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:dbe6095001465294f13f1adcd3311e50dd84e5a71525f20a10bd16689c61ce0b", size = 850528, upload-time = "2025-11-03T21:32:13.906Z" }, + { url = "https://files.pythonhosted.org/packages/d6/09/e1cd5bee3841c7f6eb37d95ca91cdee7100b8f88b81e41c2ef426910891a/regex-2025.11.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:454d9b4ae7881afbc25015b8627c16d88a597479b9dea82b8c6e7e2e07240dc7", size = 789866, upload-time = "2025-11-03T21:32:15.748Z" }, + { url = "https://files.pythonhosted.org/packages/eb/51/702f5ea74e2a9c13d855a6a85b7f80c30f9e72a95493260193c07f3f8d74/regex-2025.11.3-cp313-cp313-win32.whl", hash = "sha256:28ba4d69171fc6e9896337d4fc63a43660002b7da53fc15ac992abcf3410917c", size = 266189, upload-time = "2025-11-03T21:32:17.493Z" }, + { url = "https://files.pythonhosted.org/packages/8b/00/6e29bb314e271a743170e53649db0fdb8e8ff0b64b4f425f5602f4eb9014/regex-2025.11.3-cp313-cp313-win_amd64.whl", hash = "sha256:bac4200befe50c670c405dc33af26dad5a3b6b255dd6c000d92fe4629f9ed6a5", size = 277054, upload-time = "2025-11-03T21:32:19.042Z" }, + { url = "https://files.pythonhosted.org/packages/25/f1/b156ff9f2ec9ac441710764dda95e4edaf5f36aca48246d1eea3f1fd96ec/regex-2025.11.3-cp313-cp313-win_arm64.whl", hash = "sha256:2292cd5a90dab247f9abe892ac584cb24f0f54680c73fcb4a7493c66c2bf2467", size = 270325, upload-time = "2025-11-03T21:32:21.338Z" }, + { url = "https://files.pythonhosted.org/packages/20/28/fd0c63357caefe5680b8ea052131acbd7f456893b69cc2a90cc3e0dc90d4/regex-2025.11.3-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:1eb1ebf6822b756c723e09f5186473d93236c06c579d2cc0671a722d2ab14281", size = 491984, upload-time = "2025-11-03T21:32:23.466Z" }, + { url = "https://files.pythonhosted.org/packages/df/ec/7014c15626ab46b902b3bcc4b28a7bae46d8f281fc7ea9c95e22fcaaa917/regex-2025.11.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:1e00ec2970aab10dc5db34af535f21fcf32b4a31d99e34963419636e2f85ae39", size = 292673, upload-time = "2025-11-03T21:32:25.034Z" }, + { url = "https://files.pythonhosted.org/packages/23/ab/3b952ff7239f20d05f1f99e9e20188513905f218c81d52fb5e78d2bf7634/regex-2025.11.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a4cb042b615245d5ff9b3794f56be4138b5adc35a4166014d31d1814744148c7", size = 291029, upload-time = "2025-11-03T21:32:26.528Z" }, + { url = "https://files.pythonhosted.org/packages/21/7e/3dc2749fc684f455f162dcafb8a187b559e2614f3826877d3844a131f37b/regex-2025.11.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:44f264d4bf02f3176467d90b294d59bf1db9fe53c141ff772f27a8b456b2a9ed", size = 807437, upload-time = "2025-11-03T21:32:28.363Z" }, + { url = "https://files.pythonhosted.org/packages/1b/0b/d529a85ab349c6a25d1ca783235b6e3eedf187247eab536797021f7126c6/regex-2025.11.3-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7be0277469bf3bd7a34a9c57c1b6a724532a0d235cd0dc4e7f4316f982c28b19", size = 873368, upload-time = "2025-11-03T21:32:30.4Z" }, + { url = "https://files.pythonhosted.org/packages/7d/18/2d868155f8c9e3e9d8f9e10c64e9a9f496bb8f7e037a88a8bed26b435af6/regex-2025.11.3-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0d31e08426ff4b5b650f68839f5af51a92a5b51abd8554a60c2fbc7c71f25d0b", size = 914921, upload-time = "2025-11-03T21:32:32.123Z" }, + { url = "https://files.pythonhosted.org/packages/2d/71/9d72ff0f354fa783fe2ba913c8734c3b433b86406117a8db4ea2bf1c7a2f/regex-2025.11.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e43586ce5bd28f9f285a6e729466841368c4a0353f6fd08d4ce4630843d3648a", size = 812708, upload-time = "2025-11-03T21:32:34.305Z" }, + { url = "https://files.pythonhosted.org/packages/e7/19/ce4bf7f5575c97f82b6e804ffb5c4e940c62609ab2a0d9538d47a7fdf7d4/regex-2025.11.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:0f9397d561a4c16829d4e6ff75202c1c08b68a3bdbfe29dbfcdb31c9830907c6", size = 795472, upload-time = "2025-11-03T21:32:36.364Z" }, + { url = "https://files.pythonhosted.org/packages/03/86/fd1063a176ffb7b2315f9a1b08d17b18118b28d9df163132615b835a26ee/regex-2025.11.3-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:dd16e78eb18ffdb25ee33a0682d17912e8cc8a770e885aeee95020046128f1ce", size = 868341, upload-time = "2025-11-03T21:32:38.042Z" }, + { url = "https://files.pythonhosted.org/packages/12/43/103fb2e9811205e7386366501bc866a164a0430c79dd59eac886a2822950/regex-2025.11.3-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:ffcca5b9efe948ba0661e9df0fa50d2bc4b097c70b9810212d6b62f05d83b2dd", size = 854666, upload-time = "2025-11-03T21:32:40.079Z" }, + { url = "https://files.pythonhosted.org/packages/7d/22/e392e53f3869b75804762c7c848bd2dd2abf2b70fb0e526f58724638bd35/regex-2025.11.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:c56b4d162ca2b43318ac671c65bd4d563e841a694ac70e1a976ac38fcf4ca1d2", size = 799473, upload-time = "2025-11-03T21:32:42.148Z" }, + { url = "https://files.pythonhosted.org/packages/4f/f9/8bd6b656592f925b6845fcbb4d57603a3ac2fb2373344ffa1ed70aa6820a/regex-2025.11.3-cp313-cp313t-win32.whl", hash = "sha256:9ddc42e68114e161e51e272f667d640f97e84a2b9ef14b7477c53aac20c2d59a", size = 268792, upload-time = "2025-11-03T21:32:44.13Z" }, + { url = "https://files.pythonhosted.org/packages/e5/87/0e7d603467775ff65cd2aeabf1b5b50cc1c3708556a8b849a2fa4dd1542b/regex-2025.11.3-cp313-cp313t-win_amd64.whl", hash = "sha256:7a7c7fdf755032ffdd72c77e3d8096bdcb0eb92e89e17571a196f03d88b11b3c", size = 280214, upload-time = "2025-11-03T21:32:45.853Z" }, + { url = "https://files.pythonhosted.org/packages/8d/d0/2afc6f8e94e2b64bfb738a7c2b6387ac1699f09f032d363ed9447fd2bb57/regex-2025.11.3-cp313-cp313t-win_arm64.whl", hash = "sha256:df9eb838c44f570283712e7cff14c16329a9f0fb19ca492d21d4b7528ee6821e", size = 271469, upload-time = "2025-11-03T21:32:48.026Z" }, + { url = "https://files.pythonhosted.org/packages/31/e9/f6e13de7e0983837f7b6d238ad9458800a874bf37c264f7923e63409944c/regex-2025.11.3-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:9697a52e57576c83139d7c6f213d64485d3df5bf84807c35fa409e6c970801c6", size = 489089, upload-time = "2025-11-03T21:32:50.027Z" }, + { url = "https://files.pythonhosted.org/packages/a3/5c/261f4a262f1fa65141c1b74b255988bd2fa020cc599e53b080667d591cfc/regex-2025.11.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:e18bc3f73bd41243c9b38a6d9f2366cd0e0137a9aebe2d8ff76c5b67d4c0a3f4", size = 291059, upload-time = "2025-11-03T21:32:51.682Z" }, + { url = "https://files.pythonhosted.org/packages/8e/57/f14eeb7f072b0e9a5a090d1712741fd8f214ec193dba773cf5410108bb7d/regex-2025.11.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:61a08bcb0ec14ff4e0ed2044aad948d0659604f824cbd50b55e30b0ec6f09c73", size = 288900, upload-time = "2025-11-03T21:32:53.569Z" }, + { url = "https://files.pythonhosted.org/packages/3c/6b/1d650c45e99a9b327586739d926a1cd4e94666b1bd4af90428b36af66dc7/regex-2025.11.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c9c30003b9347c24bcc210958c5d167b9e4f9be786cb380a7d32f14f9b84674f", size = 799010, upload-time = "2025-11-03T21:32:55.222Z" }, + { url = "https://files.pythonhosted.org/packages/99/ee/d66dcbc6b628ce4e3f7f0cbbb84603aa2fc0ffc878babc857726b8aab2e9/regex-2025.11.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4e1e592789704459900728d88d41a46fe3969b82ab62945560a31732ffc19a6d", size = 864893, upload-time = "2025-11-03T21:32:57.239Z" }, + { url = "https://files.pythonhosted.org/packages/bf/2d/f238229f1caba7ac87a6c4153d79947fb0261415827ae0f77c304260c7d3/regex-2025.11.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6538241f45eb5a25aa575dbba1069ad786f68a4f2773a29a2bd3dd1f9de787be", size = 911522, upload-time = "2025-11-03T21:32:59.274Z" }, + { url = "https://files.pythonhosted.org/packages/bd/3d/22a4eaba214a917c80e04f6025d26143690f0419511e0116508e24b11c9b/regex-2025.11.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bce22519c989bb72a7e6b36a199384c53db7722fe669ba891da75907fe3587db", size = 803272, upload-time = "2025-11-03T21:33:01.393Z" }, + { url = "https://files.pythonhosted.org/packages/84/b1/03188f634a409353a84b5ef49754b97dbcc0c0f6fd6c8ede505a8960a0a4/regex-2025.11.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:66d559b21d3640203ab9075797a55165d79017520685fb407b9234d72ab63c62", size = 787958, upload-time = "2025-11-03T21:33:03.379Z" }, + { url = "https://files.pythonhosted.org/packages/99/6a/27d072f7fbf6fadd59c64d210305e1ff865cc3b78b526fd147db768c553b/regex-2025.11.3-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:669dcfb2e38f9e8c69507bace46f4889e3abbfd9b0c29719202883c0a603598f", size = 859289, upload-time = "2025-11-03T21:33:05.374Z" }, + { url = "https://files.pythonhosted.org/packages/9a/70/1b3878f648e0b6abe023172dacb02157e685564853cc363d9961bcccde4e/regex-2025.11.3-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:32f74f35ff0f25a5021373ac61442edcb150731fbaa28286bbc8bb1582c89d02", size = 850026, upload-time = "2025-11-03T21:33:07.131Z" }, + { url = "https://files.pythonhosted.org/packages/dd/d5/68e25559b526b8baab8e66839304ede68ff6727237a47727d240006bd0ff/regex-2025.11.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:e6c7a21dffba883234baefe91bc3388e629779582038f75d2a5be918e250f0ed", size = 789499, upload-time = "2025-11-03T21:33:09.141Z" }, + { url = "https://files.pythonhosted.org/packages/fc/df/43971264857140a350910d4e33df725e8c94dd9dee8d2e4729fa0d63d49e/regex-2025.11.3-cp314-cp314-win32.whl", hash = "sha256:795ea137b1d809eb6836b43748b12634291c0ed55ad50a7d72d21edf1cd565c4", size = 271604, upload-time = "2025-11-03T21:33:10.9Z" }, + { url = "https://files.pythonhosted.org/packages/01/6f/9711b57dc6894a55faf80a4c1b5aa4f8649805cb9c7aef46f7d27e2b9206/regex-2025.11.3-cp314-cp314-win_amd64.whl", hash = "sha256:9f95fbaa0ee1610ec0fc6b26668e9917a582ba80c52cc6d9ada15e30aa9ab9ad", size = 280320, upload-time = "2025-11-03T21:33:12.572Z" }, + { url = "https://files.pythonhosted.org/packages/f1/7e/f6eaa207d4377481f5e1775cdeb5a443b5a59b392d0065f3417d31d80f87/regex-2025.11.3-cp314-cp314-win_arm64.whl", hash = "sha256:dfec44d532be4c07088c3de2876130ff0fbeeacaa89a137decbbb5f665855a0f", size = 273372, upload-time = "2025-11-03T21:33:14.219Z" }, + { url = "https://files.pythonhosted.org/packages/c3/06/49b198550ee0f5e4184271cee87ba4dfd9692c91ec55289e6282f0f86ccf/regex-2025.11.3-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:ba0d8a5d7f04f73ee7d01d974d47c5834f8a1b0224390e4fe7c12a3a92a78ecc", size = 491985, upload-time = "2025-11-03T21:33:16.555Z" }, + { url = "https://files.pythonhosted.org/packages/ce/bf/abdafade008f0b1c9da10d934034cb670432d6cf6cbe38bbb53a1cfd6cf8/regex-2025.11.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:442d86cf1cfe4faabf97db7d901ef58347efd004934da045c745e7b5bd57ac49", size = 292669, upload-time = "2025-11-03T21:33:18.32Z" }, + { url = "https://files.pythonhosted.org/packages/f9/ef/0c357bb8edbd2ad8e273fcb9e1761bc37b8acbc6e1be050bebd6475f19c1/regex-2025.11.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:fd0a5e563c756de210bb964789b5abe4f114dacae9104a47e1a649b910361536", size = 291030, upload-time = "2025-11-03T21:33:20.048Z" }, + { url = "https://files.pythonhosted.org/packages/79/06/edbb67257596649b8fb088d6aeacbcb248ac195714b18a65e018bf4c0b50/regex-2025.11.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bf3490bcbb985a1ae97b2ce9ad1c0f06a852d5b19dde9b07bdf25bf224248c95", size = 807674, upload-time = "2025-11-03T21:33:21.797Z" }, + { url = "https://files.pythonhosted.org/packages/f4/d9/ad4deccfce0ea336296bd087f1a191543bb99ee1c53093dcd4c64d951d00/regex-2025.11.3-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3809988f0a8b8c9dcc0f92478d6501fac7200b9ec56aecf0ec21f4a2ec4b6009", size = 873451, upload-time = "2025-11-03T21:33:23.741Z" }, + { url = "https://files.pythonhosted.org/packages/13/75/a55a4724c56ef13e3e04acaab29df26582f6978c000ac9cd6810ad1f341f/regex-2025.11.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f4ff94e58e84aedb9c9fce66d4ef9f27a190285b451420f297c9a09f2b9abee9", size = 914980, upload-time = "2025-11-03T21:33:25.999Z" }, + { url = "https://files.pythonhosted.org/packages/67/1e/a1657ee15bd9116f70d4a530c736983eed997b361e20ecd8f5ca3759d5c5/regex-2025.11.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7eb542fd347ce61e1321b0a6b945d5701528dca0cd9759c2e3bb8bd57e47964d", size = 812852, upload-time = "2025-11-03T21:33:27.852Z" }, + { url = "https://files.pythonhosted.org/packages/b8/6f/f7516dde5506a588a561d296b2d0044839de06035bb486b326065b4c101e/regex-2025.11.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:d6c2d5919075a1f2e413c00b056ea0c2f065b3f5fe83c3d07d325ab92dce51d6", size = 795566, upload-time = "2025-11-03T21:33:32.364Z" }, + { url = "https://files.pythonhosted.org/packages/d9/dd/3d10b9e170cc16fb34cb2cef91513cf3df65f440b3366030631b2984a264/regex-2025.11.3-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:3f8bf11a4827cc7ce5a53d4ef6cddd5ad25595d3c1435ef08f76825851343154", size = 868463, upload-time = "2025-11-03T21:33:34.459Z" }, + { url = "https://files.pythonhosted.org/packages/f5/8e/935e6beff1695aa9085ff83195daccd72acc82c81793df480f34569330de/regex-2025.11.3-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:22c12d837298651e5550ac1d964e4ff57c3f56965fc1812c90c9fb2028eaf267", size = 854694, upload-time = "2025-11-03T21:33:36.793Z" }, + { url = "https://files.pythonhosted.org/packages/92/12/10650181a040978b2f5720a6a74d44f841371a3d984c2083fc1752e4acf6/regex-2025.11.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:62ba394a3dda9ad41c7c780f60f6e4a70988741415ae96f6d1bf6c239cf01379", size = 799691, upload-time = "2025-11-03T21:33:39.079Z" }, + { url = "https://files.pythonhosted.org/packages/67/90/8f37138181c9a7690e7e4cb388debbd389342db3c7381d636d2875940752/regex-2025.11.3-cp314-cp314t-win32.whl", hash = "sha256:4bf146dca15cdd53224a1bf46d628bd7590e4a07fbb69e720d561aea43a32b38", size = 274583, upload-time = "2025-11-03T21:33:41.302Z" }, + { url = "https://files.pythonhosted.org/packages/8f/cd/867f5ec442d56beb56f5f854f40abcfc75e11d10b11fdb1869dd39c63aaf/regex-2025.11.3-cp314-cp314t-win_amd64.whl", hash = "sha256:adad1a1bcf1c9e76346e091d22d23ac54ef28e1365117d99521631078dfec9de", size = 284286, upload-time = "2025-11-03T21:33:43.324Z" }, + { url = "https://files.pythonhosted.org/packages/20/31/32c0c4610cbc070362bf1d2e4ea86d1ea29014d400a6d6c2486fcfd57766/regex-2025.11.3-cp314-cp314t-win_arm64.whl", hash = "sha256:c54f768482cef41e219720013cd05933b6f971d9562544d691c68699bf2b6801", size = 274741, upload-time = "2025-11-03T21:33:45.557Z" }, ] [[package]] @@ -2513,9 +2490,9 @@ dependencies = [ { name = "idna" }, { name = "urllib3" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c9/74/b3ff8e6c8446842c3f5c837e9c3dfcfe2018ea6ecef224c710c85ef728f4/requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf", size = 134517 } +sdist = { url = "https://files.pythonhosted.org/packages/c9/74/b3ff8e6c8446842c3f5c837e9c3dfcfe2018ea6ecef224c710c85ef728f4/requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf", size = 134517, upload-time = "2025-08-18T20:46:02.573Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738 }, + { url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" }, ] [[package]] @@ -2526,9 +2503,9 @@ dependencies = [ { name = "oauthlib" }, { name = "requests" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/42/f2/05f29bc3913aea15eb670be136045bf5c5bbf4b99ecb839da9b422bb2c85/requests-oauthlib-2.0.0.tar.gz", hash = "sha256:b3dffaebd884d8cd778494369603a9e7b58d29111bf6b41bdc2dcd87203af4e9", size = 55650 } +sdist = { url = "https://files.pythonhosted.org/packages/42/f2/05f29bc3913aea15eb670be136045bf5c5bbf4b99ecb839da9b422bb2c85/requests-oauthlib-2.0.0.tar.gz", hash = "sha256:b3dffaebd884d8cd778494369603a9e7b58d29111bf6b41bdc2dcd87203af4e9", size = 55650, upload-time = "2024-03-22T20:32:29.939Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/3b/5d/63d4ae3b9daea098d5d6f5da83984853c1bbacd5dc826764b249fe119d24/requests_oauthlib-2.0.0-py2.py3-none-any.whl", hash = "sha256:7dd8a5c40426b779b0868c404bdef9768deccf22749cde15852df527e6269b36", size = 24179 }, + { url = "https://files.pythonhosted.org/packages/3b/5d/63d4ae3b9daea098d5d6f5da83984853c1bbacd5dc826764b249fe119d24/requests_oauthlib-2.0.0-py2.py3-none-any.whl", hash = "sha256:7dd8a5c40426b779b0868c404bdef9768deccf22749cde15852df527e6269b36", size = 24179, upload-time = "2024-03-22T20:32:28.055Z" }, ] [[package]] @@ -2539,9 +2516,9 @@ dependencies = [ { name = "markdown-it-py" }, { name = "pygments" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/fb/d2/8920e102050a0de7bfabeb4c4614a49248cf8d5d7a8d01885fbb24dc767a/rich-14.2.0.tar.gz", hash = "sha256:73ff50c7c0c1c77c8243079283f4edb376f0f6442433aecb8ce7e6d0b92d1fe4", size = 219990 } +sdist = { url = "https://files.pythonhosted.org/packages/fb/d2/8920e102050a0de7bfabeb4c4614a49248cf8d5d7a8d01885fbb24dc767a/rich-14.2.0.tar.gz", hash = "sha256:73ff50c7c0c1c77c8243079283f4edb376f0f6442433aecb8ce7e6d0b92d1fe4", size = 219990, upload-time = "2025-10-09T14:16:53.064Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/25/7a/b0178788f8dc6cafce37a212c99565fa1fe7872c70c6c9c1e1a372d9d88f/rich-14.2.0-py3-none-any.whl", hash = "sha256:76bc51fe2e57d2b1be1f96c524b890b816e334ab4c1e45888799bfaab0021edd", size = 243393 }, + { url = "https://files.pythonhosted.org/packages/25/7a/b0178788f8dc6cafce37a212c99565fa1fe7872c70c6c9c1e1a372d9d88f/rich-14.2.0-py3-none-any.whl", hash = "sha256:76bc51fe2e57d2b1be1f96c524b890b816e334ab4c1e45888799bfaab0021edd", size = 243393, upload-time = "2025-10-09T14:16:51.245Z" }, ] [[package]] @@ -2552,75 +2529,75 @@ dependencies = [ { name = "docutils" }, { name = "rich" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/bc/6d/a506aaa4a9eaa945ed8ab2b7347859f53593864289853c5d6d62b77246e0/rich_rst-1.3.2.tar.gz", hash = "sha256:a1196fdddf1e364b02ec68a05e8ff8f6914fee10fbca2e6b6735f166bb0da8d4", size = 14936 } +sdist = { url = "https://files.pythonhosted.org/packages/bc/6d/a506aaa4a9eaa945ed8ab2b7347859f53593864289853c5d6d62b77246e0/rich_rst-1.3.2.tar.gz", hash = "sha256:a1196fdddf1e364b02ec68a05e8ff8f6914fee10fbca2e6b6735f166bb0da8d4", size = 14936, upload-time = "2025-10-14T16:49:45.332Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/13/2f/b4530fbf948867702d0a3f27de4a6aab1d156f406d72852ab902c4d04de9/rich_rst-1.3.2-py3-none-any.whl", hash = "sha256:a99b4907cbe118cf9d18b0b44de272efa61f15117c61e39ebdc431baf5df722a", size = 12567 }, + { url = "https://files.pythonhosted.org/packages/13/2f/b4530fbf948867702d0a3f27de4a6aab1d156f406d72852ab902c4d04de9/rich_rst-1.3.2-py3-none-any.whl", hash = "sha256:a99b4907cbe118cf9d18b0b44de272efa61f15117c61e39ebdc431baf5df722a", size = 12567, upload-time = "2025-10-14T16:49:42.953Z" }, ] [[package]] name = "rpds-py" version = "0.30.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/20/af/3f2f423103f1113b36230496629986e0ef7e199d2aa8392452b484b38ced/rpds_py-0.30.0.tar.gz", hash = "sha256:dd8ff7cf90014af0c0f787eea34794ebf6415242ee1d6fa91eaba725cc441e84", size = 69469 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ed/dc/d61221eb88ff410de3c49143407f6f3147acf2538c86f2ab7ce65ae7d5f9/rpds_py-0.30.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:f83424d738204d9770830d35290ff3273fbb02b41f919870479fab14b9d303b2", size = 374887 }, - { url = "https://files.pythonhosted.org/packages/fd/32/55fb50ae104061dbc564ef15cc43c013dc4a9f4527a1f4d99baddf56fe5f/rpds_py-0.30.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e7536cd91353c5273434b4e003cbda89034d67e7710eab8761fd918ec6c69cf8", size = 358904 }, - { url = "https://files.pythonhosted.org/packages/58/70/faed8186300e3b9bdd138d0273109784eea2396c68458ed580f885dfe7ad/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2771c6c15973347f50fece41fc447c054b7ac2ae0502388ce3b6738cd366e3d4", size = 389945 }, - { url = "https://files.pythonhosted.org/packages/bd/a8/073cac3ed2c6387df38f71296d002ab43496a96b92c823e76f46b8af0543/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0a59119fc6e3f460315fe9d08149f8102aa322299deaa5cab5b40092345c2136", size = 407783 }, - { url = "https://files.pythonhosted.org/packages/77/57/5999eb8c58671f1c11eba084115e77a8899d6e694d2a18f69f0ba471ec8b/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:76fec018282b4ead0364022e3c54b60bf368b9d926877957a8624b58419169b7", size = 515021 }, - { url = "https://files.pythonhosted.org/packages/e0/af/5ab4833eadc36c0a8ed2bc5c0de0493c04f6c06de223170bd0798ff98ced/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:692bef75a5525db97318e8cd061542b5a79812d711ea03dbc1f6f8dbb0c5f0d2", size = 414589 }, - { url = "https://files.pythonhosted.org/packages/b7/de/f7192e12b21b9e9a68a6d0f249b4af3fdcdff8418be0767a627564afa1f1/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9027da1ce107104c50c81383cae773ef5c24d296dd11c99e2629dbd7967a20c6", size = 394025 }, - { url = "https://files.pythonhosted.org/packages/91/c4/fc70cd0249496493500e7cc2de87504f5aa6509de1e88623431fec76d4b6/rpds_py-0.30.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:9cf69cdda1f5968a30a359aba2f7f9aa648a9ce4b580d6826437f2b291cfc86e", size = 408895 }, - { url = "https://files.pythonhosted.org/packages/58/95/d9275b05ab96556fefff73a385813eb66032e4c99f411d0795372d9abcea/rpds_py-0.30.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a4796a717bf12b9da9d3ad002519a86063dcac8988b030e405704ef7d74d2d9d", size = 422799 }, - { url = "https://files.pythonhosted.org/packages/06/c1/3088fc04b6624eb12a57eb814f0d4997a44b0d208d6cace713033ff1a6ba/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5d4c2aa7c50ad4728a094ebd5eb46c452e9cb7edbfdb18f9e1221f597a73e1e7", size = 572731 }, - { url = "https://files.pythonhosted.org/packages/d8/42/c612a833183b39774e8ac8fecae81263a68b9583ee343db33ab571a7ce55/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ba81a9203d07805435eb06f536d95a266c21e5b2dfbf6517748ca40c98d19e31", size = 599027 }, - { url = "https://files.pythonhosted.org/packages/5f/60/525a50f45b01d70005403ae0e25f43c0384369ad24ffe46e8d9068b50086/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:945dccface01af02675628334f7cf49c2af4c1c904748efc5cf7bbdf0b579f95", size = 563020 }, - { url = "https://files.pythonhosted.org/packages/0b/5d/47c4655e9bcd5ca907148535c10e7d489044243cc9941c16ed7cd53be91d/rpds_py-0.30.0-cp313-cp313-win32.whl", hash = "sha256:b40fb160a2db369a194cb27943582b38f79fc4887291417685f3ad693c5a1d5d", size = 223139 }, - { url = "https://files.pythonhosted.org/packages/f2/e1/485132437d20aa4d3e1d8b3fb5a5e65aa8139f1e097080c2a8443201742c/rpds_py-0.30.0-cp313-cp313-win_amd64.whl", hash = "sha256:806f36b1b605e2d6a72716f321f20036b9489d29c51c91f4dd29a3e3afb73b15", size = 240224 }, - { url = "https://files.pythonhosted.org/packages/24/95/ffd128ed1146a153d928617b0ef673960130be0009c77d8fbf0abe306713/rpds_py-0.30.0-cp313-cp313-win_arm64.whl", hash = "sha256:d96c2086587c7c30d44f31f42eae4eac89b60dabbac18c7669be3700f13c3ce1", size = 230645 }, - { url = "https://files.pythonhosted.org/packages/ff/1b/b10de890a0def2a319a2626334a7f0ae388215eb60914dbac8a3bae54435/rpds_py-0.30.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:eb0b93f2e5c2189ee831ee43f156ed34e2a89a78a66b98cadad955972548be5a", size = 364443 }, - { url = "https://files.pythonhosted.org/packages/0d/bf/27e39f5971dc4f305a4fb9c672ca06f290f7c4e261c568f3dea16a410d47/rpds_py-0.30.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:922e10f31f303c7c920da8981051ff6d8c1a56207dbdf330d9047f6d30b70e5e", size = 353375 }, - { url = "https://files.pythonhosted.org/packages/40/58/442ada3bba6e8e6615fc00483135c14a7538d2ffac30e2d933ccf6852232/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cdc62c8286ba9bf7f47befdcea13ea0e26bf294bda99758fd90535cbaf408000", size = 383850 }, - { url = "https://files.pythonhosted.org/packages/14/14/f59b0127409a33c6ef6f5c1ebd5ad8e32d7861c9c7adfa9a624fc3889f6c/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:47f9a91efc418b54fb8190a6b4aa7813a23fb79c51f4bb84e418f5476c38b8db", size = 392812 }, - { url = "https://files.pythonhosted.org/packages/b3/66/e0be3e162ac299b3a22527e8913767d869e6cc75c46bd844aa43fb81ab62/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1f3587eb9b17f3789ad50824084fa6f81921bbf9a795826570bda82cb3ed91f2", size = 517841 }, - { url = "https://files.pythonhosted.org/packages/3d/55/fa3b9cf31d0c963ecf1ba777f7cf4b2a2c976795ac430d24a1f43d25a6ba/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:39c02563fc592411c2c61d26b6c5fe1e51eaa44a75aa2c8735ca88b0d9599daa", size = 408149 }, - { url = "https://files.pythonhosted.org/packages/60/ca/780cf3b1a32b18c0f05c441958d3758f02544f1d613abf9488cd78876378/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:51a1234d8febafdfd33a42d97da7a43f5dcb120c1060e352a3fbc0c6d36e2083", size = 383843 }, - { url = "https://files.pythonhosted.org/packages/82/86/d5f2e04f2aa6247c613da0c1dd87fcd08fa17107e858193566048a1e2f0a/rpds_py-0.30.0-cp313-cp313t-manylinux_2_31_riscv64.whl", hash = "sha256:eb2c4071ab598733724c08221091e8d80e89064cd472819285a9ab0f24bcedb9", size = 396507 }, - { url = "https://files.pythonhosted.org/packages/4b/9a/453255d2f769fe44e07ea9785c8347edaf867f7026872e76c1ad9f7bed92/rpds_py-0.30.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6bdfdb946967d816e6adf9a3d8201bfad269c67efe6cefd7093ef959683c8de0", size = 414949 }, - { url = "https://files.pythonhosted.org/packages/a3/31/622a86cdc0c45d6df0e9ccb6becdba5074735e7033c20e401a6d9d0e2ca0/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c77afbd5f5250bf27bf516c7c4a016813eb2d3e116139aed0096940c5982da94", size = 565790 }, - { url = "https://files.pythonhosted.org/packages/1c/5d/15bbf0fb4a3f58a3b1c67855ec1efcc4ceaef4e86644665fff03e1b66d8d/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:61046904275472a76c8c90c9ccee9013d70a6d0f73eecefd38c1ae7c39045a08", size = 590217 }, - { url = "https://files.pythonhosted.org/packages/6d/61/21b8c41f68e60c8cc3b2e25644f0e3681926020f11d06ab0b78e3c6bbff1/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4c5f36a861bc4b7da6516dbdf302c55313afa09b81931e8280361a4f6c9a2d27", size = 555806 }, - { url = "https://files.pythonhosted.org/packages/f9/39/7e067bb06c31de48de3eb200f9fc7c58982a4d3db44b07e73963e10d3be9/rpds_py-0.30.0-cp313-cp313t-win32.whl", hash = "sha256:3d4a69de7a3e50ffc214ae16d79d8fbb0922972da0356dcf4d0fdca2878559c6", size = 211341 }, - { url = "https://files.pythonhosted.org/packages/0a/4d/222ef0b46443cf4cf46764d9c630f3fe4abaa7245be9417e56e9f52b8f65/rpds_py-0.30.0-cp313-cp313t-win_amd64.whl", hash = "sha256:f14fc5df50a716f7ece6a80b6c78bb35ea2ca47c499e422aa4463455dd96d56d", size = 225768 }, - { url = "https://files.pythonhosted.org/packages/86/81/dad16382ebbd3d0e0328776d8fd7ca94220e4fa0798d1dc5e7da48cb3201/rpds_py-0.30.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:68f19c879420aa08f61203801423f6cd5ac5f0ac4ac82a2368a9fcd6a9a075e0", size = 362099 }, - { url = "https://files.pythonhosted.org/packages/2b/60/19f7884db5d5603edf3c6bce35408f45ad3e97e10007df0e17dd57af18f8/rpds_py-0.30.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ec7c4490c672c1a0389d319b3a9cfcd098dcdc4783991553c332a15acf7249be", size = 353192 }, - { url = "https://files.pythonhosted.org/packages/bf/c4/76eb0e1e72d1a9c4703c69607cec123c29028bff28ce41588792417098ac/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f251c812357a3fed308d684a5079ddfb9d933860fc6de89f2b7ab00da481e65f", size = 384080 }, - { url = "https://files.pythonhosted.org/packages/72/87/87ea665e92f3298d1b26d78814721dc39ed8d2c74b86e83348d6b48a6f31/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ac98b175585ecf4c0348fd7b29c3864bda53b805c773cbf7bfdaffc8070c976f", size = 394841 }, - { url = "https://files.pythonhosted.org/packages/77/ad/7783a89ca0587c15dcbf139b4a8364a872a25f861bdb88ed99f9b0dec985/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3e62880792319dbeb7eb866547f2e35973289e7d5696c6e295476448f5b63c87", size = 516670 }, - { url = "https://files.pythonhosted.org/packages/5b/3c/2882bdac942bd2172f3da574eab16f309ae10a3925644e969536553cb4ee/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4e7fc54e0900ab35d041b0601431b0a0eb495f0851a0639b6ef90f7741b39a18", size = 408005 }, - { url = "https://files.pythonhosted.org/packages/ce/81/9a91c0111ce1758c92516a3e44776920b579d9a7c09b2b06b642d4de3f0f/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47e77dc9822d3ad616c3d5759ea5631a75e5809d5a28707744ef79d7a1bcfcad", size = 382112 }, - { url = "https://files.pythonhosted.org/packages/cf/8e/1da49d4a107027e5fbc64daeab96a0706361a2918da10cb41769244b805d/rpds_py-0.30.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:b4dc1a6ff022ff85ecafef7979a2c6eb423430e05f1165d6688234e62ba99a07", size = 399049 }, - { url = "https://files.pythonhosted.org/packages/df/5a/7ee239b1aa48a127570ec03becbb29c9d5a9eb092febbd1699d567cae859/rpds_py-0.30.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4559c972db3a360808309e06a74628b95eaccbf961c335c8fe0d590cf587456f", size = 415661 }, - { url = "https://files.pythonhosted.org/packages/70/ea/caa143cf6b772f823bc7929a45da1fa83569ee49b11d18d0ada7f5ee6fd6/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:0ed177ed9bded28f8deb6ab40c183cd1192aa0de40c12f38be4d59cd33cb5c65", size = 565606 }, - { url = "https://files.pythonhosted.org/packages/64/91/ac20ba2d69303f961ad8cf55bf7dbdb4763f627291ba3d0d7d67333cced9/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ad1fa8db769b76ea911cb4e10f049d80bf518c104f15b3edb2371cc65375c46f", size = 591126 }, - { url = "https://files.pythonhosted.org/packages/21/20/7ff5f3c8b00c8a95f75985128c26ba44503fb35b8e0259d812766ea966c7/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:46e83c697b1f1c72b50e5ee5adb4353eef7406fb3f2043d64c33f20ad1c2fc53", size = 553371 }, - { url = "https://files.pythonhosted.org/packages/72/c7/81dadd7b27c8ee391c132a6b192111ca58d866577ce2d9b0ca157552cce0/rpds_py-0.30.0-cp314-cp314-win32.whl", hash = "sha256:ee454b2a007d57363c2dfd5b6ca4a5d7e2c518938f8ed3b706e37e5d470801ed", size = 215298 }, - { url = "https://files.pythonhosted.org/packages/3e/d2/1aaac33287e8cfb07aab2e6b8ac1deca62f6f65411344f1433c55e6f3eb8/rpds_py-0.30.0-cp314-cp314-win_amd64.whl", hash = "sha256:95f0802447ac2d10bcc69f6dc28fe95fdf17940367b21d34e34c737870758950", size = 228604 }, - { url = "https://files.pythonhosted.org/packages/e8/95/ab005315818cc519ad074cb7784dae60d939163108bd2b394e60dc7b5461/rpds_py-0.30.0-cp314-cp314-win_arm64.whl", hash = "sha256:613aa4771c99f03346e54c3f038e4cc574ac09a3ddfb0e8878487335e96dead6", size = 222391 }, - { url = "https://files.pythonhosted.org/packages/9e/68/154fe0194d83b973cdedcdcc88947a2752411165930182ae41d983dcefa6/rpds_py-0.30.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:7e6ecfcb62edfd632e56983964e6884851786443739dbfe3582947e87274f7cb", size = 364868 }, - { url = "https://files.pythonhosted.org/packages/83/69/8bbc8b07ec854d92a8b75668c24d2abcb1719ebf890f5604c61c9369a16f/rpds_py-0.30.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a1d0bc22a7cdc173fedebb73ef81e07faef93692b8c1ad3733b67e31e1b6e1b8", size = 353747 }, - { url = "https://files.pythonhosted.org/packages/ab/00/ba2e50183dbd9abcce9497fa5149c62b4ff3e22d338a30d690f9af970561/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0d08f00679177226c4cb8c5265012eea897c8ca3b93f429e546600c971bcbae7", size = 383795 }, - { url = "https://files.pythonhosted.org/packages/05/6f/86f0272b84926bcb0e4c972262f54223e8ecc556b3224d281e6598fc9268/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5965af57d5848192c13534f90f9dd16464f3c37aaf166cc1da1cae1fd5a34898", size = 393330 }, - { url = "https://files.pythonhosted.org/packages/cb/e9/0e02bb2e6dc63d212641da45df2b0bf29699d01715913e0d0f017ee29438/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9a4e86e34e9ab6b667c27f3211ca48f73dba7cd3d90f8d5b11be56e5dbc3fb4e", size = 518194 }, - { url = "https://files.pythonhosted.org/packages/ee/ca/be7bca14cf21513bdf9c0606aba17d1f389ea2b6987035eb4f62bd923f25/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e5d3e6b26f2c785d65cc25ef1e5267ccbe1b069c5c21b8cc724efee290554419", size = 408340 }, - { url = "https://files.pythonhosted.org/packages/c2/c7/736e00ebf39ed81d75544c0da6ef7b0998f8201b369acf842f9a90dc8fce/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:626a7433c34566535b6e56a1b39a7b17ba961e97ce3b80ec62e6f1312c025551", size = 383765 }, - { url = "https://files.pythonhosted.org/packages/4a/3f/da50dfde9956aaf365c4adc9533b100008ed31aea635f2b8d7b627e25b49/rpds_py-0.30.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:acd7eb3f4471577b9b5a41baf02a978e8bdeb08b4b355273994f8b87032000a8", size = 396834 }, - { url = "https://files.pythonhosted.org/packages/4e/00/34bcc2565b6020eab2623349efbdec810676ad571995911f1abdae62a3a0/rpds_py-0.30.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fe5fa731a1fa8a0a56b0977413f8cacac1768dad38d16b3a296712709476fbd5", size = 415470 }, - { url = "https://files.pythonhosted.org/packages/8c/28/882e72b5b3e6f718d5453bd4d0d9cf8df36fddeb4ddbbab17869d5868616/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:74a3243a411126362712ee1524dfc90c650a503502f135d54d1b352bd01f2404", size = 565630 }, - { url = "https://files.pythonhosted.org/packages/3b/97/04a65539c17692de5b85c6e293520fd01317fd878ea1995f0367d4532fb1/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:3e8eeb0544f2eb0d2581774be4c3410356eba189529a6b3e36bbbf9696175856", size = 591148 }, - { url = "https://files.pythonhosted.org/packages/85/70/92482ccffb96f5441aab93e26c4d66489eb599efdcf96fad90c14bbfb976/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:dbd936cde57abfee19ab3213cf9c26be06d60750e60a8e4dd85d1ab12c8b1f40", size = 556030 }, - { url = "https://files.pythonhosted.org/packages/20/53/7c7e784abfa500a2b6b583b147ee4bb5a2b3747a9166bab52fec4b5b5e7d/rpds_py-0.30.0-cp314-cp314t-win32.whl", hash = "sha256:dc824125c72246d924f7f796b4f63c1e9dc810c7d9e2355864b3c3a73d59ade0", size = 211570 }, - { url = "https://files.pythonhosted.org/packages/d0/02/fa464cdfbe6b26e0600b62c528b72d8608f5cc49f96b8d6e38c95d60c676/rpds_py-0.30.0-cp314-cp314t-win_amd64.whl", hash = "sha256:27f4b0e92de5bfbc6f86e43959e6edd1425c33b5e69aab0984a72047f2bcf1e3", size = 226532 }, +sdist = { url = "https://files.pythonhosted.org/packages/20/af/3f2f423103f1113b36230496629986e0ef7e199d2aa8392452b484b38ced/rpds_py-0.30.0.tar.gz", hash = "sha256:dd8ff7cf90014af0c0f787eea34794ebf6415242ee1d6fa91eaba725cc441e84", size = 69469, upload-time = "2025-11-30T20:24:38.837Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ed/dc/d61221eb88ff410de3c49143407f6f3147acf2538c86f2ab7ce65ae7d5f9/rpds_py-0.30.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:f83424d738204d9770830d35290ff3273fbb02b41f919870479fab14b9d303b2", size = 374887, upload-time = "2025-11-30T20:22:41.812Z" }, + { url = "https://files.pythonhosted.org/packages/fd/32/55fb50ae104061dbc564ef15cc43c013dc4a9f4527a1f4d99baddf56fe5f/rpds_py-0.30.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e7536cd91353c5273434b4e003cbda89034d67e7710eab8761fd918ec6c69cf8", size = 358904, upload-time = "2025-11-30T20:22:43.479Z" }, + { url = "https://files.pythonhosted.org/packages/58/70/faed8186300e3b9bdd138d0273109784eea2396c68458ed580f885dfe7ad/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2771c6c15973347f50fece41fc447c054b7ac2ae0502388ce3b6738cd366e3d4", size = 389945, upload-time = "2025-11-30T20:22:44.819Z" }, + { url = "https://files.pythonhosted.org/packages/bd/a8/073cac3ed2c6387df38f71296d002ab43496a96b92c823e76f46b8af0543/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0a59119fc6e3f460315fe9d08149f8102aa322299deaa5cab5b40092345c2136", size = 407783, upload-time = "2025-11-30T20:22:46.103Z" }, + { url = "https://files.pythonhosted.org/packages/77/57/5999eb8c58671f1c11eba084115e77a8899d6e694d2a18f69f0ba471ec8b/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:76fec018282b4ead0364022e3c54b60bf368b9d926877957a8624b58419169b7", size = 515021, upload-time = "2025-11-30T20:22:47.458Z" }, + { url = "https://files.pythonhosted.org/packages/e0/af/5ab4833eadc36c0a8ed2bc5c0de0493c04f6c06de223170bd0798ff98ced/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:692bef75a5525db97318e8cd061542b5a79812d711ea03dbc1f6f8dbb0c5f0d2", size = 414589, upload-time = "2025-11-30T20:22:48.872Z" }, + { url = "https://files.pythonhosted.org/packages/b7/de/f7192e12b21b9e9a68a6d0f249b4af3fdcdff8418be0767a627564afa1f1/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9027da1ce107104c50c81383cae773ef5c24d296dd11c99e2629dbd7967a20c6", size = 394025, upload-time = "2025-11-30T20:22:50.196Z" }, + { url = "https://files.pythonhosted.org/packages/91/c4/fc70cd0249496493500e7cc2de87504f5aa6509de1e88623431fec76d4b6/rpds_py-0.30.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:9cf69cdda1f5968a30a359aba2f7f9aa648a9ce4b580d6826437f2b291cfc86e", size = 408895, upload-time = "2025-11-30T20:22:51.87Z" }, + { url = "https://files.pythonhosted.org/packages/58/95/d9275b05ab96556fefff73a385813eb66032e4c99f411d0795372d9abcea/rpds_py-0.30.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a4796a717bf12b9da9d3ad002519a86063dcac8988b030e405704ef7d74d2d9d", size = 422799, upload-time = "2025-11-30T20:22:53.341Z" }, + { url = "https://files.pythonhosted.org/packages/06/c1/3088fc04b6624eb12a57eb814f0d4997a44b0d208d6cace713033ff1a6ba/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5d4c2aa7c50ad4728a094ebd5eb46c452e9cb7edbfdb18f9e1221f597a73e1e7", size = 572731, upload-time = "2025-11-30T20:22:54.778Z" }, + { url = "https://files.pythonhosted.org/packages/d8/42/c612a833183b39774e8ac8fecae81263a68b9583ee343db33ab571a7ce55/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ba81a9203d07805435eb06f536d95a266c21e5b2dfbf6517748ca40c98d19e31", size = 599027, upload-time = "2025-11-30T20:22:56.212Z" }, + { url = "https://files.pythonhosted.org/packages/5f/60/525a50f45b01d70005403ae0e25f43c0384369ad24ffe46e8d9068b50086/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:945dccface01af02675628334f7cf49c2af4c1c904748efc5cf7bbdf0b579f95", size = 563020, upload-time = "2025-11-30T20:22:58.2Z" }, + { url = "https://files.pythonhosted.org/packages/0b/5d/47c4655e9bcd5ca907148535c10e7d489044243cc9941c16ed7cd53be91d/rpds_py-0.30.0-cp313-cp313-win32.whl", hash = "sha256:b40fb160a2db369a194cb27943582b38f79fc4887291417685f3ad693c5a1d5d", size = 223139, upload-time = "2025-11-30T20:23:00.209Z" }, + { url = "https://files.pythonhosted.org/packages/f2/e1/485132437d20aa4d3e1d8b3fb5a5e65aa8139f1e097080c2a8443201742c/rpds_py-0.30.0-cp313-cp313-win_amd64.whl", hash = "sha256:806f36b1b605e2d6a72716f321f20036b9489d29c51c91f4dd29a3e3afb73b15", size = 240224, upload-time = "2025-11-30T20:23:02.008Z" }, + { url = "https://files.pythonhosted.org/packages/24/95/ffd128ed1146a153d928617b0ef673960130be0009c77d8fbf0abe306713/rpds_py-0.30.0-cp313-cp313-win_arm64.whl", hash = "sha256:d96c2086587c7c30d44f31f42eae4eac89b60dabbac18c7669be3700f13c3ce1", size = 230645, upload-time = "2025-11-30T20:23:03.43Z" }, + { url = "https://files.pythonhosted.org/packages/ff/1b/b10de890a0def2a319a2626334a7f0ae388215eb60914dbac8a3bae54435/rpds_py-0.30.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:eb0b93f2e5c2189ee831ee43f156ed34e2a89a78a66b98cadad955972548be5a", size = 364443, upload-time = "2025-11-30T20:23:04.878Z" }, + { url = "https://files.pythonhosted.org/packages/0d/bf/27e39f5971dc4f305a4fb9c672ca06f290f7c4e261c568f3dea16a410d47/rpds_py-0.30.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:922e10f31f303c7c920da8981051ff6d8c1a56207dbdf330d9047f6d30b70e5e", size = 353375, upload-time = "2025-11-30T20:23:06.342Z" }, + { url = "https://files.pythonhosted.org/packages/40/58/442ada3bba6e8e6615fc00483135c14a7538d2ffac30e2d933ccf6852232/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cdc62c8286ba9bf7f47befdcea13ea0e26bf294bda99758fd90535cbaf408000", size = 383850, upload-time = "2025-11-30T20:23:07.825Z" }, + { url = "https://files.pythonhosted.org/packages/14/14/f59b0127409a33c6ef6f5c1ebd5ad8e32d7861c9c7adfa9a624fc3889f6c/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:47f9a91efc418b54fb8190a6b4aa7813a23fb79c51f4bb84e418f5476c38b8db", size = 392812, upload-time = "2025-11-30T20:23:09.228Z" }, + { url = "https://files.pythonhosted.org/packages/b3/66/e0be3e162ac299b3a22527e8913767d869e6cc75c46bd844aa43fb81ab62/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1f3587eb9b17f3789ad50824084fa6f81921bbf9a795826570bda82cb3ed91f2", size = 517841, upload-time = "2025-11-30T20:23:11.186Z" }, + { url = "https://files.pythonhosted.org/packages/3d/55/fa3b9cf31d0c963ecf1ba777f7cf4b2a2c976795ac430d24a1f43d25a6ba/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:39c02563fc592411c2c61d26b6c5fe1e51eaa44a75aa2c8735ca88b0d9599daa", size = 408149, upload-time = "2025-11-30T20:23:12.864Z" }, + { url = "https://files.pythonhosted.org/packages/60/ca/780cf3b1a32b18c0f05c441958d3758f02544f1d613abf9488cd78876378/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:51a1234d8febafdfd33a42d97da7a43f5dcb120c1060e352a3fbc0c6d36e2083", size = 383843, upload-time = "2025-11-30T20:23:14.638Z" }, + { url = "https://files.pythonhosted.org/packages/82/86/d5f2e04f2aa6247c613da0c1dd87fcd08fa17107e858193566048a1e2f0a/rpds_py-0.30.0-cp313-cp313t-manylinux_2_31_riscv64.whl", hash = "sha256:eb2c4071ab598733724c08221091e8d80e89064cd472819285a9ab0f24bcedb9", size = 396507, upload-time = "2025-11-30T20:23:16.105Z" }, + { url = "https://files.pythonhosted.org/packages/4b/9a/453255d2f769fe44e07ea9785c8347edaf867f7026872e76c1ad9f7bed92/rpds_py-0.30.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6bdfdb946967d816e6adf9a3d8201bfad269c67efe6cefd7093ef959683c8de0", size = 414949, upload-time = "2025-11-30T20:23:17.539Z" }, + { url = "https://files.pythonhosted.org/packages/a3/31/622a86cdc0c45d6df0e9ccb6becdba5074735e7033c20e401a6d9d0e2ca0/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c77afbd5f5250bf27bf516c7c4a016813eb2d3e116139aed0096940c5982da94", size = 565790, upload-time = "2025-11-30T20:23:19.029Z" }, + { url = "https://files.pythonhosted.org/packages/1c/5d/15bbf0fb4a3f58a3b1c67855ec1efcc4ceaef4e86644665fff03e1b66d8d/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:61046904275472a76c8c90c9ccee9013d70a6d0f73eecefd38c1ae7c39045a08", size = 590217, upload-time = "2025-11-30T20:23:20.885Z" }, + { url = "https://files.pythonhosted.org/packages/6d/61/21b8c41f68e60c8cc3b2e25644f0e3681926020f11d06ab0b78e3c6bbff1/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4c5f36a861bc4b7da6516dbdf302c55313afa09b81931e8280361a4f6c9a2d27", size = 555806, upload-time = "2025-11-30T20:23:22.488Z" }, + { url = "https://files.pythonhosted.org/packages/f9/39/7e067bb06c31de48de3eb200f9fc7c58982a4d3db44b07e73963e10d3be9/rpds_py-0.30.0-cp313-cp313t-win32.whl", hash = "sha256:3d4a69de7a3e50ffc214ae16d79d8fbb0922972da0356dcf4d0fdca2878559c6", size = 211341, upload-time = "2025-11-30T20:23:24.449Z" }, + { url = "https://files.pythonhosted.org/packages/0a/4d/222ef0b46443cf4cf46764d9c630f3fe4abaa7245be9417e56e9f52b8f65/rpds_py-0.30.0-cp313-cp313t-win_amd64.whl", hash = "sha256:f14fc5df50a716f7ece6a80b6c78bb35ea2ca47c499e422aa4463455dd96d56d", size = 225768, upload-time = "2025-11-30T20:23:25.908Z" }, + { url = "https://files.pythonhosted.org/packages/86/81/dad16382ebbd3d0e0328776d8fd7ca94220e4fa0798d1dc5e7da48cb3201/rpds_py-0.30.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:68f19c879420aa08f61203801423f6cd5ac5f0ac4ac82a2368a9fcd6a9a075e0", size = 362099, upload-time = "2025-11-30T20:23:27.316Z" }, + { url = "https://files.pythonhosted.org/packages/2b/60/19f7884db5d5603edf3c6bce35408f45ad3e97e10007df0e17dd57af18f8/rpds_py-0.30.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ec7c4490c672c1a0389d319b3a9cfcd098dcdc4783991553c332a15acf7249be", size = 353192, upload-time = "2025-11-30T20:23:29.151Z" }, + { url = "https://files.pythonhosted.org/packages/bf/c4/76eb0e1e72d1a9c4703c69607cec123c29028bff28ce41588792417098ac/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f251c812357a3fed308d684a5079ddfb9d933860fc6de89f2b7ab00da481e65f", size = 384080, upload-time = "2025-11-30T20:23:30.785Z" }, + { url = "https://files.pythonhosted.org/packages/72/87/87ea665e92f3298d1b26d78814721dc39ed8d2c74b86e83348d6b48a6f31/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ac98b175585ecf4c0348fd7b29c3864bda53b805c773cbf7bfdaffc8070c976f", size = 394841, upload-time = "2025-11-30T20:23:32.209Z" }, + { url = "https://files.pythonhosted.org/packages/77/ad/7783a89ca0587c15dcbf139b4a8364a872a25f861bdb88ed99f9b0dec985/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3e62880792319dbeb7eb866547f2e35973289e7d5696c6e295476448f5b63c87", size = 516670, upload-time = "2025-11-30T20:23:33.742Z" }, + { url = "https://files.pythonhosted.org/packages/5b/3c/2882bdac942bd2172f3da574eab16f309ae10a3925644e969536553cb4ee/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4e7fc54e0900ab35d041b0601431b0a0eb495f0851a0639b6ef90f7741b39a18", size = 408005, upload-time = "2025-11-30T20:23:35.253Z" }, + { url = "https://files.pythonhosted.org/packages/ce/81/9a91c0111ce1758c92516a3e44776920b579d9a7c09b2b06b642d4de3f0f/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47e77dc9822d3ad616c3d5759ea5631a75e5809d5a28707744ef79d7a1bcfcad", size = 382112, upload-time = "2025-11-30T20:23:36.842Z" }, + { url = "https://files.pythonhosted.org/packages/cf/8e/1da49d4a107027e5fbc64daeab96a0706361a2918da10cb41769244b805d/rpds_py-0.30.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:b4dc1a6ff022ff85ecafef7979a2c6eb423430e05f1165d6688234e62ba99a07", size = 399049, upload-time = "2025-11-30T20:23:38.343Z" }, + { url = "https://files.pythonhosted.org/packages/df/5a/7ee239b1aa48a127570ec03becbb29c9d5a9eb092febbd1699d567cae859/rpds_py-0.30.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4559c972db3a360808309e06a74628b95eaccbf961c335c8fe0d590cf587456f", size = 415661, upload-time = "2025-11-30T20:23:40.263Z" }, + { url = "https://files.pythonhosted.org/packages/70/ea/caa143cf6b772f823bc7929a45da1fa83569ee49b11d18d0ada7f5ee6fd6/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:0ed177ed9bded28f8deb6ab40c183cd1192aa0de40c12f38be4d59cd33cb5c65", size = 565606, upload-time = "2025-11-30T20:23:42.186Z" }, + { url = "https://files.pythonhosted.org/packages/64/91/ac20ba2d69303f961ad8cf55bf7dbdb4763f627291ba3d0d7d67333cced9/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ad1fa8db769b76ea911cb4e10f049d80bf518c104f15b3edb2371cc65375c46f", size = 591126, upload-time = "2025-11-30T20:23:44.086Z" }, + { url = "https://files.pythonhosted.org/packages/21/20/7ff5f3c8b00c8a95f75985128c26ba44503fb35b8e0259d812766ea966c7/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:46e83c697b1f1c72b50e5ee5adb4353eef7406fb3f2043d64c33f20ad1c2fc53", size = 553371, upload-time = "2025-11-30T20:23:46.004Z" }, + { url = "https://files.pythonhosted.org/packages/72/c7/81dadd7b27c8ee391c132a6b192111ca58d866577ce2d9b0ca157552cce0/rpds_py-0.30.0-cp314-cp314-win32.whl", hash = "sha256:ee454b2a007d57363c2dfd5b6ca4a5d7e2c518938f8ed3b706e37e5d470801ed", size = 215298, upload-time = "2025-11-30T20:23:47.696Z" }, + { url = "https://files.pythonhosted.org/packages/3e/d2/1aaac33287e8cfb07aab2e6b8ac1deca62f6f65411344f1433c55e6f3eb8/rpds_py-0.30.0-cp314-cp314-win_amd64.whl", hash = "sha256:95f0802447ac2d10bcc69f6dc28fe95fdf17940367b21d34e34c737870758950", size = 228604, upload-time = "2025-11-30T20:23:49.501Z" }, + { url = "https://files.pythonhosted.org/packages/e8/95/ab005315818cc519ad074cb7784dae60d939163108bd2b394e60dc7b5461/rpds_py-0.30.0-cp314-cp314-win_arm64.whl", hash = "sha256:613aa4771c99f03346e54c3f038e4cc574ac09a3ddfb0e8878487335e96dead6", size = 222391, upload-time = "2025-11-30T20:23:50.96Z" }, + { url = "https://files.pythonhosted.org/packages/9e/68/154fe0194d83b973cdedcdcc88947a2752411165930182ae41d983dcefa6/rpds_py-0.30.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:7e6ecfcb62edfd632e56983964e6884851786443739dbfe3582947e87274f7cb", size = 364868, upload-time = "2025-11-30T20:23:52.494Z" }, + { url = "https://files.pythonhosted.org/packages/83/69/8bbc8b07ec854d92a8b75668c24d2abcb1719ebf890f5604c61c9369a16f/rpds_py-0.30.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a1d0bc22a7cdc173fedebb73ef81e07faef93692b8c1ad3733b67e31e1b6e1b8", size = 353747, upload-time = "2025-11-30T20:23:54.036Z" }, + { url = "https://files.pythonhosted.org/packages/ab/00/ba2e50183dbd9abcce9497fa5149c62b4ff3e22d338a30d690f9af970561/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0d08f00679177226c4cb8c5265012eea897c8ca3b93f429e546600c971bcbae7", size = 383795, upload-time = "2025-11-30T20:23:55.556Z" }, + { url = "https://files.pythonhosted.org/packages/05/6f/86f0272b84926bcb0e4c972262f54223e8ecc556b3224d281e6598fc9268/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5965af57d5848192c13534f90f9dd16464f3c37aaf166cc1da1cae1fd5a34898", size = 393330, upload-time = "2025-11-30T20:23:57.033Z" }, + { url = "https://files.pythonhosted.org/packages/cb/e9/0e02bb2e6dc63d212641da45df2b0bf29699d01715913e0d0f017ee29438/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9a4e86e34e9ab6b667c27f3211ca48f73dba7cd3d90f8d5b11be56e5dbc3fb4e", size = 518194, upload-time = "2025-11-30T20:23:58.637Z" }, + { url = "https://files.pythonhosted.org/packages/ee/ca/be7bca14cf21513bdf9c0606aba17d1f389ea2b6987035eb4f62bd923f25/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e5d3e6b26f2c785d65cc25ef1e5267ccbe1b069c5c21b8cc724efee290554419", size = 408340, upload-time = "2025-11-30T20:24:00.2Z" }, + { url = "https://files.pythonhosted.org/packages/c2/c7/736e00ebf39ed81d75544c0da6ef7b0998f8201b369acf842f9a90dc8fce/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:626a7433c34566535b6e56a1b39a7b17ba961e97ce3b80ec62e6f1312c025551", size = 383765, upload-time = "2025-11-30T20:24:01.759Z" }, + { url = "https://files.pythonhosted.org/packages/4a/3f/da50dfde9956aaf365c4adc9533b100008ed31aea635f2b8d7b627e25b49/rpds_py-0.30.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:acd7eb3f4471577b9b5a41baf02a978e8bdeb08b4b355273994f8b87032000a8", size = 396834, upload-time = "2025-11-30T20:24:03.687Z" }, + { url = "https://files.pythonhosted.org/packages/4e/00/34bcc2565b6020eab2623349efbdec810676ad571995911f1abdae62a3a0/rpds_py-0.30.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fe5fa731a1fa8a0a56b0977413f8cacac1768dad38d16b3a296712709476fbd5", size = 415470, upload-time = "2025-11-30T20:24:05.232Z" }, + { url = "https://files.pythonhosted.org/packages/8c/28/882e72b5b3e6f718d5453bd4d0d9cf8df36fddeb4ddbbab17869d5868616/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:74a3243a411126362712ee1524dfc90c650a503502f135d54d1b352bd01f2404", size = 565630, upload-time = "2025-11-30T20:24:06.878Z" }, + { url = "https://files.pythonhosted.org/packages/3b/97/04a65539c17692de5b85c6e293520fd01317fd878ea1995f0367d4532fb1/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:3e8eeb0544f2eb0d2581774be4c3410356eba189529a6b3e36bbbf9696175856", size = 591148, upload-time = "2025-11-30T20:24:08.445Z" }, + { url = "https://files.pythonhosted.org/packages/85/70/92482ccffb96f5441aab93e26c4d66489eb599efdcf96fad90c14bbfb976/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:dbd936cde57abfee19ab3213cf9c26be06d60750e60a8e4dd85d1ab12c8b1f40", size = 556030, upload-time = "2025-11-30T20:24:10.956Z" }, + { url = "https://files.pythonhosted.org/packages/20/53/7c7e784abfa500a2b6b583b147ee4bb5a2b3747a9166bab52fec4b5b5e7d/rpds_py-0.30.0-cp314-cp314t-win32.whl", hash = "sha256:dc824125c72246d924f7f796b4f63c1e9dc810c7d9e2355864b3c3a73d59ade0", size = 211570, upload-time = "2025-11-30T20:24:12.735Z" }, + { url = "https://files.pythonhosted.org/packages/d0/02/fa464cdfbe6b26e0600b62c528b72d8608f5cc49f96b8d6e38c95d60c676/rpds_py-0.30.0-cp314-cp314t-win_amd64.whl", hash = "sha256:27f4b0e92de5bfbc6f86e43959e6edd1425c33b5e69aab0984a72047f2bcf1e3", size = 226532, upload-time = "2025-11-30T20:24:14.634Z" }, ] [[package]] @@ -2630,9 +2607,9 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pyasn1" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/da/8a/22b7beea3ee0d44b1916c0c1cb0ee3af23b700b6da9f04991899d0c555d4/rsa-4.9.1.tar.gz", hash = "sha256:e7bdbfdb5497da4c07dfd35530e1a902659db6ff241e39d9953cad06ebd0ae75", size = 29034 } +sdist = { url = "https://files.pythonhosted.org/packages/da/8a/22b7beea3ee0d44b1916c0c1cb0ee3af23b700b6da9f04991899d0c555d4/rsa-4.9.1.tar.gz", hash = "sha256:e7bdbfdb5497da4c07dfd35530e1a902659db6ff241e39d9953cad06ebd0ae75", size = 29034, upload-time = "2025-04-16T09:51:18.218Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/64/8d/0133e4eb4beed9e425d9a98ed6e081a55d195481b7632472be1af08d2f6b/rsa-4.9.1-py3-none-any.whl", hash = "sha256:68635866661c6836b8d39430f97a996acbd61bfa49406748ea243539fe239762", size = 34696 }, + { url = "https://files.pythonhosted.org/packages/64/8d/0133e4eb4beed9e425d9a98ed6e081a55d195481b7632472be1af08d2f6b/rsa-4.9.1-py3-none-any.whl", hash = "sha256:68635866661c6836b8d39430f97a996acbd61bfa49406748ea243539fe239762", size = 34696, upload-time = "2025-04-16T09:51:17.142Z" }, ] [[package]] @@ -2642,9 +2619,9 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "botocore" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/05/04/74127fc843314818edfa81b5540e26dd537353b123a4edc563109d8f17dd/s3transfer-0.16.0.tar.gz", hash = "sha256:8e990f13268025792229cd52fa10cb7163744bf56e719e0b9cb925ab79abf920", size = 153827 } +sdist = { url = "https://files.pythonhosted.org/packages/05/04/74127fc843314818edfa81b5540e26dd537353b123a4edc563109d8f17dd/s3transfer-0.16.0.tar.gz", hash = "sha256:8e990f13268025792229cd52fa10cb7163744bf56e719e0b9cb925ab79abf920", size = 153827, upload-time = "2025-12-01T02:30:59.114Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/fc/51/727abb13f44c1fcf6d145979e1535a35794db0f6e450a0cb46aa24732fe2/s3transfer-0.16.0-py3-none-any.whl", hash = "sha256:18e25d66fed509e3868dc1572b3f427ff947dd2c56f844a5bf09481ad3f3b2fe", size = 86830 }, + { url = "https://files.pythonhosted.org/packages/fc/51/727abb13f44c1fcf6d145979e1535a35794db0f6e450a0cb46aa24732fe2/s3transfer-0.16.0-py3-none-any.whl", hash = "sha256:18e25d66fed509e3868dc1572b3f427ff947dd2c56f844a5bf09481ad3f3b2fe", size = 86830, upload-time = "2025-12-01T02:30:57.729Z" }, ] [[package]] @@ -2655,45 +2632,45 @@ dependencies = [ { name = "cryptography" }, { name = "jeepney" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/1c/03/e834bcd866f2f8a49a85eaff47340affa3bfa391ee9912a952a1faa68c7b/secretstorage-3.5.0.tar.gz", hash = "sha256:f04b8e4689cbce351744d5537bf6b1329c6fc68f91fa666f60a380edddcd11be", size = 19884 } +sdist = { url = "https://files.pythonhosted.org/packages/1c/03/e834bcd866f2f8a49a85eaff47340affa3bfa391ee9912a952a1faa68c7b/secretstorage-3.5.0.tar.gz", hash = "sha256:f04b8e4689cbce351744d5537bf6b1329c6fc68f91fa666f60a380edddcd11be", size = 19884, upload-time = "2025-11-23T19:02:53.191Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b7/46/f5af3402b579fd5e11573ce652019a67074317e18c1935cc0b4ba9b35552/secretstorage-3.5.0-py3-none-any.whl", hash = "sha256:0ce65888c0725fcb2c5bc0fdb8e5438eece02c523557ea40ce0703c266248137", size = 15554 }, + { url = "https://files.pythonhosted.org/packages/b7/46/f5af3402b579fd5e11573ce652019a67074317e18c1935cc0b4ba9b35552/secretstorage-3.5.0-py3-none-any.whl", hash = "sha256:0ce65888c0725fcb2c5bc0fdb8e5438eece02c523557ea40ce0703c266248137", size = 15554, upload-time = "2025-11-23T19:02:51.545Z" }, ] [[package]] name = "shellingham" version = "1.5.4" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310 } +sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310, upload-time = "2023-10-24T04:13:40.426Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755 }, + { url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755, upload-time = "2023-10-24T04:13:38.866Z" }, ] [[package]] name = "six" version = "1.17.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031 } +sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050 }, + { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, ] [[package]] name = "sniffio" version = "1.3.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372 } +sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372, upload-time = "2024-02-25T23:20:04.057Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235 }, + { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" }, ] [[package]] name = "sortedcontainers" version = "2.4.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e8/c4/ba2f8066cceb6f23394729afe52f3bf7adec04bf9ed2c820b39e19299111/sortedcontainers-2.4.0.tar.gz", hash = "sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88", size = 30594 } +sdist = { url = "https://files.pythonhosted.org/packages/e8/c4/ba2f8066cceb6f23394729afe52f3bf7adec04bf9ed2c820b39e19299111/sortedcontainers-2.4.0.tar.gz", hash = "sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88", size = 30594, upload-time = "2021-05-16T22:03:42.897Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/32/46/9cb0e58b2deb7f82b84065f37f3bffeb12413f947f9388e4cac22c4621ce/sortedcontainers-2.4.0-py2.py3-none-any.whl", hash = "sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0", size = 29575 }, + { url = "https://files.pythonhosted.org/packages/32/46/9cb0e58b2deb7f82b84065f37f3bffeb12413f947f9388e4cac22c4621ce/sortedcontainers-2.4.0-py2.py3-none-any.whl", hash = "sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0", size = 29575, upload-time = "2021-05-16T22:03:41.177Z" }, ] [[package]] @@ -2704,25 +2681,25 @@ dependencies = [ { name = "greenlet", marker = "platform_machine == 'AMD64' or platform_machine == 'WIN32' or platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'ppc64le' or platform_machine == 'win32' or platform_machine == 'x86_64'" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/be/f9/5e4491e5ccf42f5d9cfc663741d261b3e6e1683ae7812114e7636409fcc6/sqlalchemy-2.0.45.tar.gz", hash = "sha256:1632a4bda8d2d25703fdad6363058d882541bdaaee0e5e3ddfa0cd3229efce88", size = 9869912 } +sdist = { url = "https://files.pythonhosted.org/packages/be/f9/5e4491e5ccf42f5d9cfc663741d261b3e6e1683ae7812114e7636409fcc6/sqlalchemy-2.0.45.tar.gz", hash = "sha256:1632a4bda8d2d25703fdad6363058d882541bdaaee0e5e3ddfa0cd3229efce88", size = 9869912, upload-time = "2025-12-09T21:05:16.737Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/6a/c8/7cc5221b47a54edc72a0140a1efa56e0a2730eefa4058d7ed0b4c4357ff8/sqlalchemy-2.0.45-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fe187fc31a54d7fd90352f34e8c008cf3ad5d064d08fedd3de2e8df83eb4a1cf", size = 3277082 }, - { url = "https://files.pythonhosted.org/packages/0e/50/80a8d080ac7d3d321e5e5d420c9a522b0aa770ec7013ea91f9a8b7d36e4a/sqlalchemy-2.0.45-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:672c45cae53ba88e0dad74b9027dddd09ef6f441e927786b05bec75d949fbb2e", size = 3293131 }, - { url = "https://files.pythonhosted.org/packages/da/4c/13dab31266fc9904f7609a5dc308a2432a066141d65b857760c3bef97e69/sqlalchemy-2.0.45-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:470daea2c1ce73910f08caf10575676a37159a6d16c4da33d0033546bddebc9b", size = 3225389 }, - { url = "https://files.pythonhosted.org/packages/74/04/891b5c2e9f83589de202e7abaf24cd4e4fa59e1837d64d528829ad6cc107/sqlalchemy-2.0.45-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9c6378449e0940476577047150fd09e242529b761dc887c9808a9a937fe990c8", size = 3266054 }, - { url = "https://files.pythonhosted.org/packages/f1/24/fc59e7f71b0948cdd4cff7a286210e86b0443ef1d18a23b0d83b87e4b1f7/sqlalchemy-2.0.45-cp313-cp313-win32.whl", hash = "sha256:4b6bec67ca45bc166c8729910bd2a87f1c0407ee955df110d78948f5b5827e8a", size = 2110299 }, - { url = "https://files.pythonhosted.org/packages/c0/c5/d17113020b2d43073412aeca09b60d2009442420372123b8d49cc253f8b8/sqlalchemy-2.0.45-cp313-cp313-win_amd64.whl", hash = "sha256:afbf47dc4de31fa38fd491f3705cac5307d21d4bb828a4f020ee59af412744ee", size = 2136264 }, - { url = "https://files.pythonhosted.org/packages/3d/8d/bb40a5d10e7a5f2195f235c0b2f2c79b0bf6e8f00c0c223130a4fbd2db09/sqlalchemy-2.0.45-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:83d7009f40ce619d483d26ac1b757dfe3167b39921379a8bd1b596cf02dab4a6", size = 3521998 }, - { url = "https://files.pythonhosted.org/packages/75/a5/346128b0464886f036c039ea287b7332a410aa2d3fb0bb5d404cb8861635/sqlalchemy-2.0.45-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:d8a2ca754e5415cde2b656c27900b19d50ba076aa05ce66e2207623d3fe41f5a", size = 3473434 }, - { url = "https://files.pythonhosted.org/packages/cc/64/4e1913772646b060b025d3fc52ce91a58967fe58957df32b455de5a12b4f/sqlalchemy-2.0.45-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7f46ec744e7f51275582e6a24326e10c49fbdd3fc99103e01376841213028774", size = 3272404 }, - { url = "https://files.pythonhosted.org/packages/b3/27/caf606ee924282fe4747ee4fd454b335a72a6e018f97eab5ff7f28199e16/sqlalchemy-2.0.45-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:883c600c345123c033c2f6caca18def08f1f7f4c3ebeb591a63b6fceffc95cce", size = 3277057 }, - { url = "https://files.pythonhosted.org/packages/85/d0/3d64218c9724e91f3d1574d12eb7ff8f19f937643815d8daf792046d88ab/sqlalchemy-2.0.45-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:2c0b74aa79e2deade948fe8593654c8ef4228c44ba862bb7c9585c8e0db90f33", size = 3222279 }, - { url = "https://files.pythonhosted.org/packages/24/10/dd7688a81c5bc7690c2a3764d55a238c524cd1a5a19487928844cb247695/sqlalchemy-2.0.45-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:8a420169cef179d4c9064365f42d779f1e5895ad26ca0c8b4c0233920973db74", size = 3244508 }, - { url = "https://files.pythonhosted.org/packages/aa/41/db75756ca49f777e029968d9c9fee338c7907c563267740c6d310a8e3f60/sqlalchemy-2.0.45-cp314-cp314-win32.whl", hash = "sha256:e50dcb81a5dfe4b7b4a4aa8f338116d127cb209559124f3694c70d6cd072b68f", size = 2113204 }, - { url = "https://files.pythonhosted.org/packages/89/a2/0e1590e9adb292b1d576dbcf67ff7df8cf55e56e78d2c927686d01080f4b/sqlalchemy-2.0.45-cp314-cp314-win_amd64.whl", hash = "sha256:4748601c8ea959e37e03d13dcda4a44837afcd1b21338e637f7c935b8da06177", size = 2138785 }, - { url = "https://files.pythonhosted.org/packages/42/39/f05f0ed54d451156bbed0e23eb0516bcad7cbb9f18b3bf219c786371b3f0/sqlalchemy-2.0.45-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cd337d3526ec5298f67d6a30bbbe4ed7e5e68862f0bf6dd21d289f8d37b7d60b", size = 3522029 }, - { url = "https://files.pythonhosted.org/packages/54/0f/d15398b98b65c2bce288d5ee3f7d0a81f77ab89d9456994d5c7cc8b2a9db/sqlalchemy-2.0.45-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:9a62b446b7d86a3909abbcd1cd3cc550a832f99c2bc37c5b22e1925438b9367b", size = 3475142 }, - { url = "https://files.pythonhosted.org/packages/bf/e1/3ccb13c643399d22289c6a9786c1a91e3dcbb68bce4beb44926ac2c557bf/sqlalchemy-2.0.45-py3-none-any.whl", hash = "sha256:5225a288e4c8cc2308dbdd874edad6e7d0fd38eac1e9e5f23503425c8eee20d0", size = 1936672 }, + { url = "https://files.pythonhosted.org/packages/6a/c8/7cc5221b47a54edc72a0140a1efa56e0a2730eefa4058d7ed0b4c4357ff8/sqlalchemy-2.0.45-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fe187fc31a54d7fd90352f34e8c008cf3ad5d064d08fedd3de2e8df83eb4a1cf", size = 3277082, upload-time = "2025-12-09T22:11:06.167Z" }, + { url = "https://files.pythonhosted.org/packages/0e/50/80a8d080ac7d3d321e5e5d420c9a522b0aa770ec7013ea91f9a8b7d36e4a/sqlalchemy-2.0.45-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:672c45cae53ba88e0dad74b9027dddd09ef6f441e927786b05bec75d949fbb2e", size = 3293131, upload-time = "2025-12-09T22:13:52.626Z" }, + { url = "https://files.pythonhosted.org/packages/da/4c/13dab31266fc9904f7609a5dc308a2432a066141d65b857760c3bef97e69/sqlalchemy-2.0.45-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:470daea2c1ce73910f08caf10575676a37159a6d16c4da33d0033546bddebc9b", size = 3225389, upload-time = "2025-12-09T22:11:08.093Z" }, + { url = "https://files.pythonhosted.org/packages/74/04/891b5c2e9f83589de202e7abaf24cd4e4fa59e1837d64d528829ad6cc107/sqlalchemy-2.0.45-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9c6378449e0940476577047150fd09e242529b761dc887c9808a9a937fe990c8", size = 3266054, upload-time = "2025-12-09T22:13:54.262Z" }, + { url = "https://files.pythonhosted.org/packages/f1/24/fc59e7f71b0948cdd4cff7a286210e86b0443ef1d18a23b0d83b87e4b1f7/sqlalchemy-2.0.45-cp313-cp313-win32.whl", hash = "sha256:4b6bec67ca45bc166c8729910bd2a87f1c0407ee955df110d78948f5b5827e8a", size = 2110299, upload-time = "2025-12-09T21:39:33.486Z" }, + { url = "https://files.pythonhosted.org/packages/c0/c5/d17113020b2d43073412aeca09b60d2009442420372123b8d49cc253f8b8/sqlalchemy-2.0.45-cp313-cp313-win_amd64.whl", hash = "sha256:afbf47dc4de31fa38fd491f3705cac5307d21d4bb828a4f020ee59af412744ee", size = 2136264, upload-time = "2025-12-09T21:39:36.801Z" }, + { url = "https://files.pythonhosted.org/packages/3d/8d/bb40a5d10e7a5f2195f235c0b2f2c79b0bf6e8f00c0c223130a4fbd2db09/sqlalchemy-2.0.45-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:83d7009f40ce619d483d26ac1b757dfe3167b39921379a8bd1b596cf02dab4a6", size = 3521998, upload-time = "2025-12-09T22:13:28.622Z" }, + { url = "https://files.pythonhosted.org/packages/75/a5/346128b0464886f036c039ea287b7332a410aa2d3fb0bb5d404cb8861635/sqlalchemy-2.0.45-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:d8a2ca754e5415cde2b656c27900b19d50ba076aa05ce66e2207623d3fe41f5a", size = 3473434, upload-time = "2025-12-09T22:13:30.188Z" }, + { url = "https://files.pythonhosted.org/packages/cc/64/4e1913772646b060b025d3fc52ce91a58967fe58957df32b455de5a12b4f/sqlalchemy-2.0.45-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7f46ec744e7f51275582e6a24326e10c49fbdd3fc99103e01376841213028774", size = 3272404, upload-time = "2025-12-09T22:11:09.662Z" }, + { url = "https://files.pythonhosted.org/packages/b3/27/caf606ee924282fe4747ee4fd454b335a72a6e018f97eab5ff7f28199e16/sqlalchemy-2.0.45-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:883c600c345123c033c2f6caca18def08f1f7f4c3ebeb591a63b6fceffc95cce", size = 3277057, upload-time = "2025-12-09T22:13:56.213Z" }, + { url = "https://files.pythonhosted.org/packages/85/d0/3d64218c9724e91f3d1574d12eb7ff8f19f937643815d8daf792046d88ab/sqlalchemy-2.0.45-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:2c0b74aa79e2deade948fe8593654c8ef4228c44ba862bb7c9585c8e0db90f33", size = 3222279, upload-time = "2025-12-09T22:11:11.1Z" }, + { url = "https://files.pythonhosted.org/packages/24/10/dd7688a81c5bc7690c2a3764d55a238c524cd1a5a19487928844cb247695/sqlalchemy-2.0.45-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:8a420169cef179d4c9064365f42d779f1e5895ad26ca0c8b4c0233920973db74", size = 3244508, upload-time = "2025-12-09T22:13:57.932Z" }, + { url = "https://files.pythonhosted.org/packages/aa/41/db75756ca49f777e029968d9c9fee338c7907c563267740c6d310a8e3f60/sqlalchemy-2.0.45-cp314-cp314-win32.whl", hash = "sha256:e50dcb81a5dfe4b7b4a4aa8f338116d127cb209559124f3694c70d6cd072b68f", size = 2113204, upload-time = "2025-12-09T21:39:38.365Z" }, + { url = "https://files.pythonhosted.org/packages/89/a2/0e1590e9adb292b1d576dbcf67ff7df8cf55e56e78d2c927686d01080f4b/sqlalchemy-2.0.45-cp314-cp314-win_amd64.whl", hash = "sha256:4748601c8ea959e37e03d13dcda4a44837afcd1b21338e637f7c935b8da06177", size = 2138785, upload-time = "2025-12-09T21:39:39.503Z" }, + { url = "https://files.pythonhosted.org/packages/42/39/f05f0ed54d451156bbed0e23eb0516bcad7cbb9f18b3bf219c786371b3f0/sqlalchemy-2.0.45-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cd337d3526ec5298f67d6a30bbbe4ed7e5e68862f0bf6dd21d289f8d37b7d60b", size = 3522029, upload-time = "2025-12-09T22:13:32.09Z" }, + { url = "https://files.pythonhosted.org/packages/54/0f/d15398b98b65c2bce288d5ee3f7d0a81f77ab89d9456994d5c7cc8b2a9db/sqlalchemy-2.0.45-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:9a62b446b7d86a3909abbcd1cd3cc550a832f99c2bc37c5b22e1925438b9367b", size = 3475142, upload-time = "2025-12-09T22:13:33.739Z" }, + { url = "https://files.pythonhosted.org/packages/bf/e1/3ccb13c643399d22289c6a9786c1a91e3dcbb68bce4beb44926ac2c557bf/sqlalchemy-2.0.45-py3-none-any.whl", hash = "sha256:5225a288e4c8cc2308dbdd874edad6e7d0fd38eac1e9e5f23503425c8eee20d0", size = 1936672, upload-time = "2025-12-09T21:54:52.608Z" }, ] [[package]] @@ -2733,9 +2710,9 @@ dependencies = [ { name = "anyio" }, { name = "starlette" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/62/08/8f554b0e5bad3e4e880521a1686d96c05198471eed860b0eb89b57ea3636/sse_starlette-3.1.1.tar.gz", hash = "sha256:bffa531420c1793ab224f63648c059bcadc412bf9fdb1301ac8de1cf9a67b7fb", size = 24306 } +sdist = { url = "https://files.pythonhosted.org/packages/62/08/8f554b0e5bad3e4e880521a1686d96c05198471eed860b0eb89b57ea3636/sse_starlette-3.1.1.tar.gz", hash = "sha256:bffa531420c1793ab224f63648c059bcadc412bf9fdb1301ac8de1cf9a67b7fb", size = 24306, upload-time = "2025-12-26T15:22:53.836Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e3/31/4c281581a0f8de137b710a07f65518b34bcf333b201cfa06cfda9af05f8a/sse_starlette-3.1.1-py3-none-any.whl", hash = "sha256:bb38f71ae74cfd86b529907a9fda5632195dfa6ae120f214ea4c890c7ee9d436", size = 12442 }, + { url = "https://files.pythonhosted.org/packages/e3/31/4c281581a0f8de137b710a07f65518b34bcf333b201cfa06cfda9af05f8a/sse_starlette-3.1.1-py3-none-any.whl", hash = "sha256:bb38f71ae74cfd86b529907a9fda5632195dfa6ae120f214ea4c890c7ee9d436", size = 12442, upload-time = "2025-12-26T15:22:52.911Z" }, ] [[package]] @@ -2745,9 +2722,9 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ba/b8/73a0e6a6e079a9d9cfa64113d771e421640b6f679a52eeb9b32f72d871a1/starlette-0.50.0.tar.gz", hash = "sha256:a2a17b22203254bcbc2e1f926d2d55f3f9497f769416b3190768befe598fa3ca", size = 2646985 } +sdist = { url = "https://files.pythonhosted.org/packages/ba/b8/73a0e6a6e079a9d9cfa64113d771e421640b6f679a52eeb9b32f72d871a1/starlette-0.50.0.tar.gz", hash = "sha256:a2a17b22203254bcbc2e1f926d2d55f3f9497f769416b3190768befe598fa3ca", size = 2646985, upload-time = "2025-11-01T15:25:27.516Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d9/52/1064f510b141bd54025f9b55105e26d1fa970b9be67ad766380a3c9b74b0/starlette-0.50.0-py3-none-any.whl", hash = "sha256:9e5391843ec9b6e472eed1365a78c8098cfceb7a74bfd4d6b1c0c0095efb3bca", size = 74033 }, + { url = "https://files.pythonhosted.org/packages/d9/52/1064f510b141bd54025f9b55105e26d1fa970b9be67ad766380a3c9b74b0/starlette-0.50.0-py3-none-any.whl", hash = "sha256:9e5391843ec9b6e472eed1365a78c8098cfceb7a74bfd4d6b1c0c0095efb3bca", size = 74033, upload-time = "2025-11-01T15:25:25.461Z" }, ] [[package]] @@ -2760,22 +2737,22 @@ dependencies = [ { name = "types-protobuf" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/21/db/7d5118d28b0918888e1ec98f56f659fdb006351e06d95f30f4274962a76f/temporalio-1.20.0.tar.gz", hash = "sha256:5a6a85b7d298b7359bffa30025f7deac83c74ac095a4c6952fbf06c249a2a67c", size = 1850498 } +sdist = { url = "https://files.pythonhosted.org/packages/21/db/7d5118d28b0918888e1ec98f56f659fdb006351e06d95f30f4274962a76f/temporalio-1.20.0.tar.gz", hash = "sha256:5a6a85b7d298b7359bffa30025f7deac83c74ac095a4c6952fbf06c249a2a67c", size = 1850498, upload-time = "2025-11-25T21:25:20.225Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f4/1b/e69052aa6003eafe595529485d9c62d1382dd5e671108f1bddf544fb6032/temporalio-1.20.0-cp310-abi3-macosx_10_12_x86_64.whl", hash = "sha256:fba70314b4068f8b1994bddfa0e2ad742483f0ae714d2ef52e63013ccfd7042e", size = 12061638 }, - { url = "https://files.pythonhosted.org/packages/ae/3b/3e8c67ed7f23bedfa231c6ac29a7a9c12b89881da7694732270f3ecd6b0c/temporalio-1.20.0-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:ffc5bb6cabc6ae67f0bfba44de6a9c121603134ae18784a2ff3a7f230ad99080", size = 11562603 }, - { url = "https://files.pythonhosted.org/packages/6d/be/ed0cc11702210522a79e09703267ebeca06eb45832b873a58de3ca76b9d0/temporalio-1.20.0-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a1e80c1e4cdf88fa8277177f563edc91466fe4dc13c0322f26e55c76b6a219e6", size = 11824016 }, - { url = "https://files.pythonhosted.org/packages/9d/97/09c5cafabc80139d97338a2bdd8ec22e08817dfd2949ab3e5b73565006eb/temporalio-1.20.0-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba92d909188930860c9d89ca6d7a753bc5a67e4e9eac6cea351477c967355eed", size = 12189521 }, - { url = "https://files.pythonhosted.org/packages/11/23/5689c014a76aff3b744b3ee0d80815f63b1362637814f5fbb105244df09b/temporalio-1.20.0-cp310-abi3-win_amd64.whl", hash = "sha256:eacfd571b653e0a0f4aa6593f4d06fc628797898f0900d400e833a1f40cad03a", size = 12745027 }, + { url = "https://files.pythonhosted.org/packages/f4/1b/e69052aa6003eafe595529485d9c62d1382dd5e671108f1bddf544fb6032/temporalio-1.20.0-cp310-abi3-macosx_10_12_x86_64.whl", hash = "sha256:fba70314b4068f8b1994bddfa0e2ad742483f0ae714d2ef52e63013ccfd7042e", size = 12061638, upload-time = "2025-11-25T21:24:57.918Z" }, + { url = "https://files.pythonhosted.org/packages/ae/3b/3e8c67ed7f23bedfa231c6ac29a7a9c12b89881da7694732270f3ecd6b0c/temporalio-1.20.0-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:ffc5bb6cabc6ae67f0bfba44de6a9c121603134ae18784a2ff3a7f230ad99080", size = 11562603, upload-time = "2025-11-25T21:25:01.721Z" }, + { url = "https://files.pythonhosted.org/packages/6d/be/ed0cc11702210522a79e09703267ebeca06eb45832b873a58de3ca76b9d0/temporalio-1.20.0-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a1e80c1e4cdf88fa8277177f563edc91466fe4dc13c0322f26e55c76b6a219e6", size = 11824016, upload-time = "2025-11-25T21:25:06.771Z" }, + { url = "https://files.pythonhosted.org/packages/9d/97/09c5cafabc80139d97338a2bdd8ec22e08817dfd2949ab3e5b73565006eb/temporalio-1.20.0-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba92d909188930860c9d89ca6d7a753bc5a67e4e9eac6cea351477c967355eed", size = 12189521, upload-time = "2025-11-25T21:25:12.091Z" }, + { url = "https://files.pythonhosted.org/packages/11/23/5689c014a76aff3b744b3ee0d80815f63b1362637814f5fbb105244df09b/temporalio-1.20.0-cp310-abi3-win_amd64.whl", hash = "sha256:eacfd571b653e0a0f4aa6593f4d06fc628797898f0900d400e833a1f40cad03a", size = 12745027, upload-time = "2025-11-25T21:25:16.827Z" }, ] [[package]] name = "tenacity" version = "9.1.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/0a/d4/2b0cd0fe285e14b36db076e78c93766ff1d529d70408bd1d2a5a84f1d929/tenacity-9.1.2.tar.gz", hash = "sha256:1169d376c297e7de388d18b4481760d478b0e99a777cad3a9c86e556f4b697cb", size = 48036 } +sdist = { url = "https://files.pythonhosted.org/packages/0a/d4/2b0cd0fe285e14b36db076e78c93766ff1d529d70408bd1d2a5a84f1d929/tenacity-9.1.2.tar.gz", hash = "sha256:1169d376c297e7de388d18b4481760d478b0e99a777cad3a9c86e556f4b697cb", size = 48036, upload-time = "2025-04-02T08:25:09.966Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e5/30/643397144bfbfec6f6ef821f36f33e57d35946c44a2352d3c9f0ae847619/tenacity-9.1.2-py3-none-any.whl", hash = "sha256:f77bf36710d8b73a50b2dd155c97b870017ad21afe6ab300326b0371b3b05138", size = 28248 }, + { url = "https://files.pythonhosted.org/packages/e5/30/643397144bfbfec6f6ef821f36f33e57d35946c44a2352d3c9f0ae847619/tenacity-9.1.2-py3-none-any.whl", hash = "sha256:f77bf36710d8b73a50b2dd155c97b870017ad21afe6ab300326b0371b3b05138", size = 28248, upload-time = "2025-04-02T08:25:07.678Z" }, ] [[package]] @@ -2786,36 +2763,36 @@ dependencies = [ { name = "regex" }, { name = "requests" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/7d/ab/4d017d0f76ec3171d469d80fc03dfbb4e48a4bcaddaa831b31d526f05edc/tiktoken-0.12.0.tar.gz", hash = "sha256:b18ba7ee2b093863978fcb14f74b3707cdc8d4d4d3836853ce7ec60772139931", size = 37806 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/00/61/441588ee21e6b5cdf59d6870f86beb9789e532ee9718c251b391b70c68d6/tiktoken-0.12.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:775c2c55de2310cc1bc9a3ad8826761cbdc87770e586fd7b6da7d4589e13dab3", size = 1050802 }, - { url = "https://files.pythonhosted.org/packages/1f/05/dcf94486d5c5c8d34496abe271ac76c5b785507c8eae71b3708f1ad9b45a/tiktoken-0.12.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a01b12f69052fbe4b080a2cfb867c4de12c704b56178edf1d1d7b273561db160", size = 993995 }, - { url = "https://files.pythonhosted.org/packages/a0/70/5163fe5359b943f8db9946b62f19be2305de8c3d78a16f629d4165e2f40e/tiktoken-0.12.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:01d99484dc93b129cd0964f9d34eee953f2737301f18b3c7257bf368d7615baa", size = 1128948 }, - { url = "https://files.pythonhosted.org/packages/0c/da/c028aa0babf77315e1cef357d4d768800c5f8a6de04d0eac0f377cb619fa/tiktoken-0.12.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:4a1a4fcd021f022bfc81904a911d3df0f6543b9e7627b51411da75ff2fe7a1be", size = 1151986 }, - { url = "https://files.pythonhosted.org/packages/a0/5a/886b108b766aa53e295f7216b509be95eb7d60b166049ce2c58416b25f2a/tiktoken-0.12.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:981a81e39812d57031efdc9ec59fa32b2a5a5524d20d4776574c4b4bd2e9014a", size = 1194222 }, - { url = "https://files.pythonhosted.org/packages/f4/f8/4db272048397636ac7a078d22773dd2795b1becee7bc4922fe6207288d57/tiktoken-0.12.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9baf52f84a3f42eef3ff4e754a0db79a13a27921b457ca9832cf944c6be4f8f3", size = 1255097 }, - { url = "https://files.pythonhosted.org/packages/8e/32/45d02e2e0ea2be3a9ed22afc47d93741247e75018aac967b713b2941f8ea/tiktoken-0.12.0-cp313-cp313-win_amd64.whl", hash = "sha256:b8a0cd0c789a61f31bf44851defbd609e8dd1e2c8589c614cc1060940ef1f697", size = 879117 }, - { url = "https://files.pythonhosted.org/packages/ce/76/994fc868f88e016e6d05b0da5ac24582a14c47893f4474c3e9744283f1d5/tiktoken-0.12.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:d5f89ea5680066b68bcb797ae85219c72916c922ef0fcdd3480c7d2315ffff16", size = 1050309 }, - { url = "https://files.pythonhosted.org/packages/f6/b8/57ef1456504c43a849821920d582a738a461b76a047f352f18c0b26c6516/tiktoken-0.12.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:b4e7ed1c6a7a8a60a3230965bdedba8cc58f68926b835e519341413370e0399a", size = 993712 }, - { url = "https://files.pythonhosted.org/packages/72/90/13da56f664286ffbae9dbcfadcc625439142675845baa62715e49b87b68b/tiktoken-0.12.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:fc530a28591a2d74bce821d10b418b26a094bf33839e69042a6e86ddb7a7fb27", size = 1128725 }, - { url = "https://files.pythonhosted.org/packages/05/df/4f80030d44682235bdaecd7346c90f67ae87ec8f3df4a3442cb53834f7e4/tiktoken-0.12.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:06a9f4f49884139013b138920a4c393aa6556b2f8f536345f11819389c703ebb", size = 1151875 }, - { url = "https://files.pythonhosted.org/packages/22/1f/ae535223a8c4ef4c0c1192e3f9b82da660be9eb66b9279e95c99288e9dab/tiktoken-0.12.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:04f0e6a985d95913cabc96a741c5ffec525a2c72e9df086ff17ebe35985c800e", size = 1194451 }, - { url = "https://files.pythonhosted.org/packages/78/a7/f8ead382fce0243cb625c4f266e66c27f65ae65ee9e77f59ea1653b6d730/tiktoken-0.12.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:0ee8f9ae00c41770b5f9b0bb1235474768884ae157de3beb5439ca0fd70f3e25", size = 1253794 }, - { url = "https://files.pythonhosted.org/packages/93/e0/6cc82a562bc6365785a3ff0af27a2a092d57c47d7a81d9e2295d8c36f011/tiktoken-0.12.0-cp313-cp313t-win_amd64.whl", hash = "sha256:dc2dd125a62cb2b3d858484d6c614d136b5b848976794edfb63688d539b8b93f", size = 878777 }, - { url = "https://files.pythonhosted.org/packages/72/05/3abc1db5d2c9aadc4d2c76fa5640134e475e58d9fbb82b5c535dc0de9b01/tiktoken-0.12.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:a90388128df3b3abeb2bfd1895b0681412a8d7dc644142519e6f0a97c2111646", size = 1050188 }, - { url = "https://files.pythonhosted.org/packages/e3/7b/50c2f060412202d6c95f32b20755c7a6273543b125c0985d6fa9465105af/tiktoken-0.12.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:da900aa0ad52247d8794e307d6446bd3cdea8e192769b56276695d34d2c9aa88", size = 993978 }, - { url = "https://files.pythonhosted.org/packages/14/27/bf795595a2b897e271771cd31cb847d479073497344c637966bdf2853da1/tiktoken-0.12.0-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:285ba9d73ea0d6171e7f9407039a290ca77efcdb026be7769dccc01d2c8d7fff", size = 1129271 }, - { url = "https://files.pythonhosted.org/packages/f5/de/9341a6d7a8f1b448573bbf3425fa57669ac58258a667eb48a25dfe916d70/tiktoken-0.12.0-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:d186a5c60c6a0213f04a7a802264083dea1bbde92a2d4c7069e1a56630aef830", size = 1151216 }, - { url = "https://files.pythonhosted.org/packages/75/0d/881866647b8d1be4d67cb24e50d0c26f9f807f994aa1510cb9ba2fe5f612/tiktoken-0.12.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:604831189bd05480f2b885ecd2d1986dc7686f609de48208ebbbddeea071fc0b", size = 1194860 }, - { url = "https://files.pythonhosted.org/packages/b3/1e/b651ec3059474dab649b8d5b69f5c65cd8fcd8918568c1935bd4136c9392/tiktoken-0.12.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:8f317e8530bb3a222547b85a58583238c8f74fd7a7408305f9f63246d1a0958b", size = 1254567 }, - { url = "https://files.pythonhosted.org/packages/80/57/ce64fd16ac390fafde001268c364d559447ba09b509181b2808622420eec/tiktoken-0.12.0-cp314-cp314-win_amd64.whl", hash = "sha256:399c3dd672a6406719d84442299a490420b458c44d3ae65516302a99675888f3", size = 921067 }, - { url = "https://files.pythonhosted.org/packages/ac/a4/72eed53e8976a099539cdd5eb36f241987212c29629d0a52c305173e0a68/tiktoken-0.12.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:c2c714c72bc00a38ca969dae79e8266ddec999c7ceccd603cc4f0d04ccd76365", size = 1050473 }, - { url = "https://files.pythonhosted.org/packages/e6/d7/0110b8f54c008466b19672c615f2168896b83706a6611ba6e47313dbc6e9/tiktoken-0.12.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:cbb9a3ba275165a2cb0f9a83f5d7025afe6b9d0ab01a22b50f0e74fee2ad253e", size = 993855 }, - { url = "https://files.pythonhosted.org/packages/5f/77/4f268c41a3957c418b084dd576ea2fad2e95da0d8e1ab705372892c2ca22/tiktoken-0.12.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:dfdfaa5ffff8993a3af94d1125870b1d27aed7cb97aa7eb8c1cefdbc87dbee63", size = 1129022 }, - { url = "https://files.pythonhosted.org/packages/4e/2b/fc46c90fe5028bd094cd6ee25a7db321cb91d45dc87531e2bdbb26b4867a/tiktoken-0.12.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:584c3ad3d0c74f5269906eb8a659c8bfc6144a52895d9261cdaf90a0ae5f4de0", size = 1150736 }, - { url = "https://files.pythonhosted.org/packages/28/c0/3c7a39ff68022ddfd7d93f3337ad90389a342f761c4d71de99a3ccc57857/tiktoken-0.12.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:54c891b416a0e36b8e2045b12b33dd66fb34a4fe7965565f1b482da50da3e86a", size = 1194908 }, - { url = "https://files.pythonhosted.org/packages/ab/0d/c1ad6f4016a3968c048545f5d9b8ffebf577774b2ede3e2e352553b685fe/tiktoken-0.12.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5edb8743b88d5be814b1a8a8854494719080c28faaa1ccbef02e87354fe71ef0", size = 1253706 }, - { url = "https://files.pythonhosted.org/packages/af/df/c7891ef9d2712ad774777271d39fdef63941ffba0a9d59b7ad1fd2765e57/tiktoken-0.12.0-cp314-cp314t-win_amd64.whl", hash = "sha256:f61c0aea5565ac82e2ec50a05e02a6c44734e91b51c10510b084ea1b8e633a71", size = 920667 }, +sdist = { url = "https://files.pythonhosted.org/packages/7d/ab/4d017d0f76ec3171d469d80fc03dfbb4e48a4bcaddaa831b31d526f05edc/tiktoken-0.12.0.tar.gz", hash = "sha256:b18ba7ee2b093863978fcb14f74b3707cdc8d4d4d3836853ce7ec60772139931", size = 37806, upload-time = "2025-10-06T20:22:45.419Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/00/61/441588ee21e6b5cdf59d6870f86beb9789e532ee9718c251b391b70c68d6/tiktoken-0.12.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:775c2c55de2310cc1bc9a3ad8826761cbdc87770e586fd7b6da7d4589e13dab3", size = 1050802, upload-time = "2025-10-06T20:22:00.96Z" }, + { url = "https://files.pythonhosted.org/packages/1f/05/dcf94486d5c5c8d34496abe271ac76c5b785507c8eae71b3708f1ad9b45a/tiktoken-0.12.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a01b12f69052fbe4b080a2cfb867c4de12c704b56178edf1d1d7b273561db160", size = 993995, upload-time = "2025-10-06T20:22:02.788Z" }, + { url = "https://files.pythonhosted.org/packages/a0/70/5163fe5359b943f8db9946b62f19be2305de8c3d78a16f629d4165e2f40e/tiktoken-0.12.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:01d99484dc93b129cd0964f9d34eee953f2737301f18b3c7257bf368d7615baa", size = 1128948, upload-time = "2025-10-06T20:22:03.814Z" }, + { url = "https://files.pythonhosted.org/packages/0c/da/c028aa0babf77315e1cef357d4d768800c5f8a6de04d0eac0f377cb619fa/tiktoken-0.12.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:4a1a4fcd021f022bfc81904a911d3df0f6543b9e7627b51411da75ff2fe7a1be", size = 1151986, upload-time = "2025-10-06T20:22:05.173Z" }, + { url = "https://files.pythonhosted.org/packages/a0/5a/886b108b766aa53e295f7216b509be95eb7d60b166049ce2c58416b25f2a/tiktoken-0.12.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:981a81e39812d57031efdc9ec59fa32b2a5a5524d20d4776574c4b4bd2e9014a", size = 1194222, upload-time = "2025-10-06T20:22:06.265Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f8/4db272048397636ac7a078d22773dd2795b1becee7bc4922fe6207288d57/tiktoken-0.12.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9baf52f84a3f42eef3ff4e754a0db79a13a27921b457ca9832cf944c6be4f8f3", size = 1255097, upload-time = "2025-10-06T20:22:07.403Z" }, + { url = "https://files.pythonhosted.org/packages/8e/32/45d02e2e0ea2be3a9ed22afc47d93741247e75018aac967b713b2941f8ea/tiktoken-0.12.0-cp313-cp313-win_amd64.whl", hash = "sha256:b8a0cd0c789a61f31bf44851defbd609e8dd1e2c8589c614cc1060940ef1f697", size = 879117, upload-time = "2025-10-06T20:22:08.418Z" }, + { url = "https://files.pythonhosted.org/packages/ce/76/994fc868f88e016e6d05b0da5ac24582a14c47893f4474c3e9744283f1d5/tiktoken-0.12.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:d5f89ea5680066b68bcb797ae85219c72916c922ef0fcdd3480c7d2315ffff16", size = 1050309, upload-time = "2025-10-06T20:22:10.939Z" }, + { url = "https://files.pythonhosted.org/packages/f6/b8/57ef1456504c43a849821920d582a738a461b76a047f352f18c0b26c6516/tiktoken-0.12.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:b4e7ed1c6a7a8a60a3230965bdedba8cc58f68926b835e519341413370e0399a", size = 993712, upload-time = "2025-10-06T20:22:12.115Z" }, + { url = "https://files.pythonhosted.org/packages/72/90/13da56f664286ffbae9dbcfadcc625439142675845baa62715e49b87b68b/tiktoken-0.12.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:fc530a28591a2d74bce821d10b418b26a094bf33839e69042a6e86ddb7a7fb27", size = 1128725, upload-time = "2025-10-06T20:22:13.541Z" }, + { url = "https://files.pythonhosted.org/packages/05/df/4f80030d44682235bdaecd7346c90f67ae87ec8f3df4a3442cb53834f7e4/tiktoken-0.12.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:06a9f4f49884139013b138920a4c393aa6556b2f8f536345f11819389c703ebb", size = 1151875, upload-time = "2025-10-06T20:22:14.559Z" }, + { url = "https://files.pythonhosted.org/packages/22/1f/ae535223a8c4ef4c0c1192e3f9b82da660be9eb66b9279e95c99288e9dab/tiktoken-0.12.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:04f0e6a985d95913cabc96a741c5ffec525a2c72e9df086ff17ebe35985c800e", size = 1194451, upload-time = "2025-10-06T20:22:15.545Z" }, + { url = "https://files.pythonhosted.org/packages/78/a7/f8ead382fce0243cb625c4f266e66c27f65ae65ee9e77f59ea1653b6d730/tiktoken-0.12.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:0ee8f9ae00c41770b5f9b0bb1235474768884ae157de3beb5439ca0fd70f3e25", size = 1253794, upload-time = "2025-10-06T20:22:16.624Z" }, + { url = "https://files.pythonhosted.org/packages/93/e0/6cc82a562bc6365785a3ff0af27a2a092d57c47d7a81d9e2295d8c36f011/tiktoken-0.12.0-cp313-cp313t-win_amd64.whl", hash = "sha256:dc2dd125a62cb2b3d858484d6c614d136b5b848976794edfb63688d539b8b93f", size = 878777, upload-time = "2025-10-06T20:22:18.036Z" }, + { url = "https://files.pythonhosted.org/packages/72/05/3abc1db5d2c9aadc4d2c76fa5640134e475e58d9fbb82b5c535dc0de9b01/tiktoken-0.12.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:a90388128df3b3abeb2bfd1895b0681412a8d7dc644142519e6f0a97c2111646", size = 1050188, upload-time = "2025-10-06T20:22:19.563Z" }, + { url = "https://files.pythonhosted.org/packages/e3/7b/50c2f060412202d6c95f32b20755c7a6273543b125c0985d6fa9465105af/tiktoken-0.12.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:da900aa0ad52247d8794e307d6446bd3cdea8e192769b56276695d34d2c9aa88", size = 993978, upload-time = "2025-10-06T20:22:20.702Z" }, + { url = "https://files.pythonhosted.org/packages/14/27/bf795595a2b897e271771cd31cb847d479073497344c637966bdf2853da1/tiktoken-0.12.0-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:285ba9d73ea0d6171e7f9407039a290ca77efcdb026be7769dccc01d2c8d7fff", size = 1129271, upload-time = "2025-10-06T20:22:22.06Z" }, + { url = "https://files.pythonhosted.org/packages/f5/de/9341a6d7a8f1b448573bbf3425fa57669ac58258a667eb48a25dfe916d70/tiktoken-0.12.0-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:d186a5c60c6a0213f04a7a802264083dea1bbde92a2d4c7069e1a56630aef830", size = 1151216, upload-time = "2025-10-06T20:22:23.085Z" }, + { url = "https://files.pythonhosted.org/packages/75/0d/881866647b8d1be4d67cb24e50d0c26f9f807f994aa1510cb9ba2fe5f612/tiktoken-0.12.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:604831189bd05480f2b885ecd2d1986dc7686f609de48208ebbbddeea071fc0b", size = 1194860, upload-time = "2025-10-06T20:22:24.602Z" }, + { url = "https://files.pythonhosted.org/packages/b3/1e/b651ec3059474dab649b8d5b69f5c65cd8fcd8918568c1935bd4136c9392/tiktoken-0.12.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:8f317e8530bb3a222547b85a58583238c8f74fd7a7408305f9f63246d1a0958b", size = 1254567, upload-time = "2025-10-06T20:22:25.671Z" }, + { url = "https://files.pythonhosted.org/packages/80/57/ce64fd16ac390fafde001268c364d559447ba09b509181b2808622420eec/tiktoken-0.12.0-cp314-cp314-win_amd64.whl", hash = "sha256:399c3dd672a6406719d84442299a490420b458c44d3ae65516302a99675888f3", size = 921067, upload-time = "2025-10-06T20:22:26.753Z" }, + { url = "https://files.pythonhosted.org/packages/ac/a4/72eed53e8976a099539cdd5eb36f241987212c29629d0a52c305173e0a68/tiktoken-0.12.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:c2c714c72bc00a38ca969dae79e8266ddec999c7ceccd603cc4f0d04ccd76365", size = 1050473, upload-time = "2025-10-06T20:22:27.775Z" }, + { url = "https://files.pythonhosted.org/packages/e6/d7/0110b8f54c008466b19672c615f2168896b83706a6611ba6e47313dbc6e9/tiktoken-0.12.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:cbb9a3ba275165a2cb0f9a83f5d7025afe6b9d0ab01a22b50f0e74fee2ad253e", size = 993855, upload-time = "2025-10-06T20:22:28.799Z" }, + { url = "https://files.pythonhosted.org/packages/5f/77/4f268c41a3957c418b084dd576ea2fad2e95da0d8e1ab705372892c2ca22/tiktoken-0.12.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:dfdfaa5ffff8993a3af94d1125870b1d27aed7cb97aa7eb8c1cefdbc87dbee63", size = 1129022, upload-time = "2025-10-06T20:22:29.981Z" }, + { url = "https://files.pythonhosted.org/packages/4e/2b/fc46c90fe5028bd094cd6ee25a7db321cb91d45dc87531e2bdbb26b4867a/tiktoken-0.12.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:584c3ad3d0c74f5269906eb8a659c8bfc6144a52895d9261cdaf90a0ae5f4de0", size = 1150736, upload-time = "2025-10-06T20:22:30.996Z" }, + { url = "https://files.pythonhosted.org/packages/28/c0/3c7a39ff68022ddfd7d93f3337ad90389a342f761c4d71de99a3ccc57857/tiktoken-0.12.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:54c891b416a0e36b8e2045b12b33dd66fb34a4fe7965565f1b482da50da3e86a", size = 1194908, upload-time = "2025-10-06T20:22:32.073Z" }, + { url = "https://files.pythonhosted.org/packages/ab/0d/c1ad6f4016a3968c048545f5d9b8ffebf577774b2ede3e2e352553b685fe/tiktoken-0.12.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5edb8743b88d5be814b1a8a8854494719080c28faaa1ccbef02e87354fe71ef0", size = 1253706, upload-time = "2025-10-06T20:22:33.385Z" }, + { url = "https://files.pythonhosted.org/packages/af/df/c7891ef9d2712ad774777271d39fdef63941ffba0a9d59b7ad1fd2765e57/tiktoken-0.12.0-cp314-cp314t-win_amd64.whl", hash = "sha256:f61c0aea5565ac82e2ec50a05e02a6c44734e91b51c10510b084ea1b8e633a71", size = 920667, upload-time = "2025-10-06T20:22:34.444Z" }, ] [[package]] @@ -2825,22 +2802,22 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "huggingface-hub" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/1c/46/fb6854cec3278fbfa4a75b50232c77622bc517ac886156e6afbfa4d8fc6e/tokenizers-0.22.1.tar.gz", hash = "sha256:61de6522785310a309b3407bac22d99c4db5dba349935e99e4d15ea2226af2d9", size = 363123 } +sdist = { url = "https://files.pythonhosted.org/packages/1c/46/fb6854cec3278fbfa4a75b50232c77622bc517ac886156e6afbfa4d8fc6e/tokenizers-0.22.1.tar.gz", hash = "sha256:61de6522785310a309b3407bac22d99c4db5dba349935e99e4d15ea2226af2d9", size = 363123, upload-time = "2025-09-19T09:49:23.424Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/bf/33/f4b2d94ada7ab297328fc671fed209368ddb82f965ec2224eb1892674c3a/tokenizers-0.22.1-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:59fdb013df17455e5f950b4b834a7b3ee2e0271e6378ccb33aa74d178b513c73", size = 3069318 }, - { url = "https://files.pythonhosted.org/packages/1c/58/2aa8c874d02b974990e89ff95826a4852a8b2a273c7d1b4411cdd45a4565/tokenizers-0.22.1-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:8d4e484f7b0827021ac5f9f71d4794aaef62b979ab7608593da22b1d2e3c4edc", size = 2926478 }, - { url = "https://files.pythonhosted.org/packages/1e/3b/55e64befa1e7bfea963cf4b787b2cea1011362c4193f5477047532ce127e/tokenizers-0.22.1-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:19d2962dd28bc67c1f205ab180578a78eef89ac60ca7ef7cbe9635a46a56422a", size = 3256994 }, - { url = "https://files.pythonhosted.org/packages/71/0b/fbfecf42f67d9b7b80fde4aabb2b3110a97fac6585c9470b5bff103a80cb/tokenizers-0.22.1-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:38201f15cdb1f8a6843e6563e6e79f4abd053394992b9bbdf5213ea3469b4ae7", size = 3153141 }, - { url = "https://files.pythonhosted.org/packages/17/a9/b38f4e74e0817af8f8ef925507c63c6ae8171e3c4cb2d5d4624bf58fca69/tokenizers-0.22.1-cp39-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d1cbe5454c9a15df1b3443c726063d930c16f047a3cc724b9e6e1a91140e5a21", size = 3508049 }, - { url = "https://files.pythonhosted.org/packages/d2/48/dd2b3dac46bb9134a88e35d72e1aa4869579eacc1a27238f1577270773ff/tokenizers-0.22.1-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e7d094ae6312d69cc2a872b54b91b309f4f6fbce871ef28eb27b52a98e4d0214", size = 3710730 }, - { url = "https://files.pythonhosted.org/packages/93/0e/ccabc8d16ae4ba84a55d41345207c1e2ea88784651a5a487547d80851398/tokenizers-0.22.1-cp39-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:afd7594a56656ace95cdd6df4cca2e4059d294c5cfb1679c57824b605556cb2f", size = 3412560 }, - { url = "https://files.pythonhosted.org/packages/d0/c6/dc3a0db5a6766416c32c034286d7c2d406da1f498e4de04ab1b8959edd00/tokenizers-0.22.1-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e2ef6063d7a84994129732b47e7915e8710f27f99f3a3260b8a38fc7ccd083f4", size = 3250221 }, - { url = "https://files.pythonhosted.org/packages/d7/a6/2c8486eef79671601ff57b093889a345dd3d576713ef047776015dc66de7/tokenizers-0.22.1-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:ba0a64f450b9ef412c98f6bcd2a50c6df6e2443b560024a09fa6a03189726879", size = 9345569 }, - { url = "https://files.pythonhosted.org/packages/6b/16/32ce667f14c35537f5f605fe9bea3e415ea1b0a646389d2295ec348d5657/tokenizers-0.22.1-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:331d6d149fa9c7d632cde4490fb8bbb12337fa3a0232e77892be656464f4b446", size = 9271599 }, - { url = "https://files.pythonhosted.org/packages/51/7c/a5f7898a3f6baa3fc2685c705e04c98c1094c523051c805cdd9306b8f87e/tokenizers-0.22.1-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:607989f2ea68a46cb1dfbaf3e3aabdf3f21d8748312dbeb6263d1b3b66c5010a", size = 9533862 }, - { url = "https://files.pythonhosted.org/packages/36/65/7e75caea90bc73c1dd8d40438adf1a7bc26af3b8d0a6705ea190462506e1/tokenizers-0.22.1-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a0f307d490295717726598ef6fa4f24af9d484809223bbc253b201c740a06390", size = 9681250 }, - { url = "https://files.pythonhosted.org/packages/30/2c/959dddef581b46e6209da82df3b78471e96260e2bc463f89d23b1bf0e52a/tokenizers-0.22.1-cp39-abi3-win32.whl", hash = "sha256:b5120eed1442765cd90b903bb6cfef781fd8fe64e34ccaecbae4c619b7b12a82", size = 2472003 }, - { url = "https://files.pythonhosted.org/packages/b3/46/e33a8c93907b631a99377ef4c5f817ab453d0b34f93529421f42ff559671/tokenizers-0.22.1-cp39-abi3-win_amd64.whl", hash = "sha256:65fd6e3fb11ca1e78a6a93602490f134d1fdeb13bcef99389d5102ea318ed138", size = 2674684 }, + { url = "https://files.pythonhosted.org/packages/bf/33/f4b2d94ada7ab297328fc671fed209368ddb82f965ec2224eb1892674c3a/tokenizers-0.22.1-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:59fdb013df17455e5f950b4b834a7b3ee2e0271e6378ccb33aa74d178b513c73", size = 3069318, upload-time = "2025-09-19T09:49:11.848Z" }, + { url = "https://files.pythonhosted.org/packages/1c/58/2aa8c874d02b974990e89ff95826a4852a8b2a273c7d1b4411cdd45a4565/tokenizers-0.22.1-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:8d4e484f7b0827021ac5f9f71d4794aaef62b979ab7608593da22b1d2e3c4edc", size = 2926478, upload-time = "2025-09-19T09:49:09.759Z" }, + { url = "https://files.pythonhosted.org/packages/1e/3b/55e64befa1e7bfea963cf4b787b2cea1011362c4193f5477047532ce127e/tokenizers-0.22.1-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:19d2962dd28bc67c1f205ab180578a78eef89ac60ca7ef7cbe9635a46a56422a", size = 3256994, upload-time = "2025-09-19T09:48:56.701Z" }, + { url = "https://files.pythonhosted.org/packages/71/0b/fbfecf42f67d9b7b80fde4aabb2b3110a97fac6585c9470b5bff103a80cb/tokenizers-0.22.1-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:38201f15cdb1f8a6843e6563e6e79f4abd053394992b9bbdf5213ea3469b4ae7", size = 3153141, upload-time = "2025-09-19T09:48:59.749Z" }, + { url = "https://files.pythonhosted.org/packages/17/a9/b38f4e74e0817af8f8ef925507c63c6ae8171e3c4cb2d5d4624bf58fca69/tokenizers-0.22.1-cp39-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d1cbe5454c9a15df1b3443c726063d930c16f047a3cc724b9e6e1a91140e5a21", size = 3508049, upload-time = "2025-09-19T09:49:05.868Z" }, + { url = "https://files.pythonhosted.org/packages/d2/48/dd2b3dac46bb9134a88e35d72e1aa4869579eacc1a27238f1577270773ff/tokenizers-0.22.1-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e7d094ae6312d69cc2a872b54b91b309f4f6fbce871ef28eb27b52a98e4d0214", size = 3710730, upload-time = "2025-09-19T09:49:01.832Z" }, + { url = "https://files.pythonhosted.org/packages/93/0e/ccabc8d16ae4ba84a55d41345207c1e2ea88784651a5a487547d80851398/tokenizers-0.22.1-cp39-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:afd7594a56656ace95cdd6df4cca2e4059d294c5cfb1679c57824b605556cb2f", size = 3412560, upload-time = "2025-09-19T09:49:03.867Z" }, + { url = "https://files.pythonhosted.org/packages/d0/c6/dc3a0db5a6766416c32c034286d7c2d406da1f498e4de04ab1b8959edd00/tokenizers-0.22.1-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e2ef6063d7a84994129732b47e7915e8710f27f99f3a3260b8a38fc7ccd083f4", size = 3250221, upload-time = "2025-09-19T09:49:07.664Z" }, + { url = "https://files.pythonhosted.org/packages/d7/a6/2c8486eef79671601ff57b093889a345dd3d576713ef047776015dc66de7/tokenizers-0.22.1-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:ba0a64f450b9ef412c98f6bcd2a50c6df6e2443b560024a09fa6a03189726879", size = 9345569, upload-time = "2025-09-19T09:49:14.214Z" }, + { url = "https://files.pythonhosted.org/packages/6b/16/32ce667f14c35537f5f605fe9bea3e415ea1b0a646389d2295ec348d5657/tokenizers-0.22.1-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:331d6d149fa9c7d632cde4490fb8bbb12337fa3a0232e77892be656464f4b446", size = 9271599, upload-time = "2025-09-19T09:49:16.639Z" }, + { url = "https://files.pythonhosted.org/packages/51/7c/a5f7898a3f6baa3fc2685c705e04c98c1094c523051c805cdd9306b8f87e/tokenizers-0.22.1-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:607989f2ea68a46cb1dfbaf3e3aabdf3f21d8748312dbeb6263d1b3b66c5010a", size = 9533862, upload-time = "2025-09-19T09:49:19.146Z" }, + { url = "https://files.pythonhosted.org/packages/36/65/7e75caea90bc73c1dd8d40438adf1a7bc26af3b8d0a6705ea190462506e1/tokenizers-0.22.1-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a0f307d490295717726598ef6fa4f24af9d484809223bbc253b201c740a06390", size = 9681250, upload-time = "2025-09-19T09:49:21.501Z" }, + { url = "https://files.pythonhosted.org/packages/30/2c/959dddef581b46e6209da82df3b78471e96260e2bc463f89d23b1bf0e52a/tokenizers-0.22.1-cp39-abi3-win32.whl", hash = "sha256:b5120eed1442765cd90b903bb6cfef781fd8fe64e34ccaecbae4c619b7b12a82", size = 2472003, upload-time = "2025-09-19T09:49:27.089Z" }, + { url = "https://files.pythonhosted.org/packages/b3/46/e33a8c93907b631a99377ef4c5f817ab453d0b34f93529421f42ff559671/tokenizers-0.22.1-cp39-abi3-win_amd64.whl", hash = "sha256:65fd6e3fb11ca1e78a6a93602490f134d1fdeb13bcef99389d5102ea318ed138", size = 2674684, upload-time = "2025-09-19T09:49:24.953Z" }, ] [[package]] @@ -2850,9 +2827,9 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a8/4b/29b4ef32e036bb34e4ab51796dd745cdba7ed47ad142a9f4a1eb8e0c744d/tqdm-4.67.1.tar.gz", hash = "sha256:f8aef9c52c08c13a65f30ea34f4e5aac3fd1a34959879d7e59e63027286627f2", size = 169737 } +sdist = { url = "https://files.pythonhosted.org/packages/a8/4b/29b4ef32e036bb34e4ab51796dd745cdba7ed47ad142a9f4a1eb8e0c744d/tqdm-4.67.1.tar.gz", hash = "sha256:f8aef9c52c08c13a65f30ea34f4e5aac3fd1a34959879d7e59e63027286627f2", size = 169737, upload-time = "2024-11-24T20:12:22.481Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d0/30/dc54f88dd4a2b5dc8a0279bdd7270e735851848b762aeb1c1184ed1f6b14/tqdm-4.67.1-py3-none-any.whl", hash = "sha256:26445eca388f82e72884e0d580d5464cd801a3ea01e63e5601bdff9ba6a48de2", size = 78540 }, + { url = "https://files.pythonhosted.org/packages/d0/30/dc54f88dd4a2b5dc8a0279bdd7270e735851848b762aeb1c1184ed1f6b14/tqdm-4.67.1-py3-none-any.whl", hash = "sha256:26445eca388f82e72884e0d580d5464cd801a3ea01e63e5601bdff9ba6a48de2", size = 78540, upload-time = "2024-11-24T20:12:19.698Z" }, ] [[package]] @@ -2865,9 +2842,9 @@ dependencies = [ { name = "shellingham" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/85/30/ff9ede605e3bd086b4dd842499814e128500621f7951ca1e5ce84bbf61b1/typer-0.21.0.tar.gz", hash = "sha256:c87c0d2b6eee3b49c5c64649ec92425492c14488096dfbc8a0c2799b2f6f9c53", size = 106781 } +sdist = { url = "https://files.pythonhosted.org/packages/85/30/ff9ede605e3bd086b4dd842499814e128500621f7951ca1e5ce84bbf61b1/typer-0.21.0.tar.gz", hash = "sha256:c87c0d2b6eee3b49c5c64649ec92425492c14488096dfbc8a0c2799b2f6f9c53", size = 106781, upload-time = "2025-12-25T09:54:53.651Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e1/e4/5ebc1899d31d2b1601b32d21cfb4bba022ae6fce323d365f0448031b1660/typer-0.21.0-py3-none-any.whl", hash = "sha256:c79c01ca6b30af9fd48284058a7056ba0d3bf5cf10d0ff3d0c5b11b68c258ac6", size = 47109 }, + { url = "https://files.pythonhosted.org/packages/e1/e4/5ebc1899d31d2b1601b32d21cfb4bba022ae6fce323d365f0448031b1660/typer-0.21.0-py3-none-any.whl", hash = "sha256:c79c01ca6b30af9fd48284058a7056ba0d3bf5cf10d0ff3d0c5b11b68c258ac6", size = 47109, upload-time = "2025-12-25T09:54:51.918Z" }, ] [[package]] @@ -2878,18 +2855,18 @@ dependencies = [ { name = "click" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/f9/3b/2f60ce16f578b1db5b8816d37d6a4d9786b33b76407fc8c13b0b86312c31/typer_slim-0.21.0.tar.gz", hash = "sha256:f2dbd150cfa0fead2242e21fa9f654dfc64773763ddf07c6be9a49ad34f79557", size = 106841 } +sdist = { url = "https://files.pythonhosted.org/packages/f9/3b/2f60ce16f578b1db5b8816d37d6a4d9786b33b76407fc8c13b0b86312c31/typer_slim-0.21.0.tar.gz", hash = "sha256:f2dbd150cfa0fead2242e21fa9f654dfc64773763ddf07c6be9a49ad34f79557", size = 106841, upload-time = "2025-12-25T09:54:55.998Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b4/84/e97abf10e4a699194ff07fd586ec7f4cf867d9d04bead559a65f9e7aff84/typer_slim-0.21.0-py3-none-any.whl", hash = "sha256:92aee2188ac6fc2b2924bd75bb61a340b78bd8cd51fd9735533ce5a856812c8e", size = 47174 }, + { url = "https://files.pythonhosted.org/packages/b4/84/e97abf10e4a699194ff07fd586ec7f4cf867d9d04bead559a65f9e7aff84/typer_slim-0.21.0-py3-none-any.whl", hash = "sha256:92aee2188ac6fc2b2924bd75bb61a340b78bd8cd51fd9735533ce5a856812c8e", size = 47174, upload-time = "2025-12-25T09:54:54.609Z" }, ] [[package]] name = "types-protobuf" version = "6.32.1.20251210" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/c2/59/c743a842911887cd96d56aa8936522b0cd5f7a7f228c96e81b59fced45be/types_protobuf-6.32.1.20251210.tar.gz", hash = "sha256:c698bb3f020274b1a2798ae09dc773728ce3f75209a35187bd11916ebfde6763", size = 63900 } +sdist = { url = "https://files.pythonhosted.org/packages/c2/59/c743a842911887cd96d56aa8936522b0cd5f7a7f228c96e81b59fced45be/types_protobuf-6.32.1.20251210.tar.gz", hash = "sha256:c698bb3f020274b1a2798ae09dc773728ce3f75209a35187bd11916ebfde6763", size = 63900, upload-time = "2025-12-10T03:14:25.451Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/aa/43/58e75bac4219cbafee83179505ff44cae3153ec279be0e30583a73b8f108/types_protobuf-6.32.1.20251210-py3-none-any.whl", hash = "sha256:2641f78f3696822a048cfb8d0ff42ccd85c25f12f871fbebe86da63793692140", size = 77921 }, + { url = "https://files.pythonhosted.org/packages/aa/43/58e75bac4219cbafee83179505ff44cae3153ec279be0e30583a73b8f108/types_protobuf-6.32.1.20251210-py3-none-any.whl", hash = "sha256:2641f78f3696822a048cfb8d0ff42ccd85c25f12f871fbebe86da63793692140", size = 77921, upload-time = "2025-12-10T03:14:24.477Z" }, ] [[package]] @@ -2899,18 +2876,18 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "urllib3" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/36/27/489922f4505975b11de2b5ad07b4fe1dca0bca9be81a703f26c5f3acfce5/types_requests-2.32.4.20250913.tar.gz", hash = "sha256:abd6d4f9ce3a9383f269775a9835a4c24e5cd6b9f647d64f88aa4613c33def5d", size = 23113 } +sdist = { url = "https://files.pythonhosted.org/packages/36/27/489922f4505975b11de2b5ad07b4fe1dca0bca9be81a703f26c5f3acfce5/types_requests-2.32.4.20250913.tar.gz", hash = "sha256:abd6d4f9ce3a9383f269775a9835a4c24e5cd6b9f647d64f88aa4613c33def5d", size = 23113, upload-time = "2025-09-13T02:40:02.309Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/2a/20/9a227ea57c1285986c4cf78400d0a91615d25b24e257fd9e2969606bdfae/types_requests-2.32.4.20250913-py3-none-any.whl", hash = "sha256:78c9c1fffebbe0fa487a418e0fa5252017e9c60d1a2da394077f1780f655d7e1", size = 20658 }, + { url = "https://files.pythonhosted.org/packages/2a/20/9a227ea57c1285986c4cf78400d0a91615d25b24e257fd9e2969606bdfae/types_requests-2.32.4.20250913-py3-none-any.whl", hash = "sha256:78c9c1fffebbe0fa487a418e0fa5252017e9c60d1a2da394077f1780f655d7e1", size = 20658, upload-time = "2025-09-13T02:40:01.115Z" }, ] [[package]] name = "typing-extensions" version = "4.15.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391 } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614 }, + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, ] [[package]] @@ -2920,27 +2897,27 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949 } +sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611 }, + { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" }, ] [[package]] name = "tzdata" version = "2025.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/5e/a7/c202b344c5ca7daf398f3b8a477eeb205cf3b6f32e7ec3a6bac0629ca975/tzdata-2025.3.tar.gz", hash = "sha256:de39c2ca5dc7b0344f2eba86f49d614019d29f060fc4ebc8a417896a620b56a7", size = 196772 } +sdist = { url = "https://files.pythonhosted.org/packages/5e/a7/c202b344c5ca7daf398f3b8a477eeb205cf3b6f32e7ec3a6bac0629ca975/tzdata-2025.3.tar.gz", hash = "sha256:de39c2ca5dc7b0344f2eba86f49d614019d29f060fc4ebc8a417896a620b56a7", size = 196772, upload-time = "2025-12-13T17:45:35.667Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c7/b0/003792df09decd6849a5e39c28b513c06e84436a54440380862b5aeff25d/tzdata-2025.3-py2.py3-none-any.whl", hash = "sha256:06a47e5700f3081aab02b2e513160914ff0694bce9947d6b76ebd6bf57cfc5d1", size = 348521 }, + { url = "https://files.pythonhosted.org/packages/c7/b0/003792df09decd6849a5e39c28b513c06e84436a54440380862b5aeff25d/tzdata-2025.3-py2.py3-none-any.whl", hash = "sha256:06a47e5700f3081aab02b2e513160914ff0694bce9947d6b76ebd6bf57cfc5d1", size = 348521, upload-time = "2025-12-13T17:45:33.889Z" }, ] [[package]] name = "urllib3" version = "2.3.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/aa/63/e53da845320b757bf29ef6a9062f5c669fe997973f966045cb019c3f4b66/urllib3-2.3.0.tar.gz", hash = "sha256:f8c5449b3cf0861679ce7e0503c7b44b5ec981bec0d1d3795a07f1ba96f0204d", size = 307268 } +sdist = { url = "https://files.pythonhosted.org/packages/aa/63/e53da845320b757bf29ef6a9062f5c669fe997973f966045cb019c3f4b66/urllib3-2.3.0.tar.gz", hash = "sha256:f8c5449b3cf0861679ce7e0503c7b44b5ec981bec0d1d3795a07f1ba96f0204d", size = 307268, upload-time = "2024-12-22T07:47:30.032Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c8/19/4ec628951a74043532ca2cf5d97b7b14863931476d117c471e8e2b1eb39f/urllib3-2.3.0-py3-none-any.whl", hash = "sha256:1cee9ad369867bfdbbb48b7dd50374c0967a0bb7710050facf0dd6911440e3df", size = 128369 }, + { url = "https://files.pythonhosted.org/packages/c8/19/4ec628951a74043532ca2cf5d97b7b14863931476d117c471e8e2b1eb39f/urllib3-2.3.0-py3-none-any.whl", hash = "sha256:1cee9ad369867bfdbbb48b7dd50374c0967a0bb7710050facf0dd6911440e3df", size = 128369, upload-time = "2024-12-22T07:47:28.074Z" }, ] [[package]] @@ -2951,9 +2928,9 @@ dependencies = [ { name = "click" }, { name = "h11" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/cb/ce/f06b84e2697fef4688ca63bdb2fdf113ca0a3be33f94488f2cadb690b0cf/uvicorn-0.38.0.tar.gz", hash = "sha256:fd97093bdd120a2609fc0d3afe931d4d4ad688b6e75f0f929fde1bc36fe0e91d", size = 80605 } +sdist = { url = "https://files.pythonhosted.org/packages/cb/ce/f06b84e2697fef4688ca63bdb2fdf113ca0a3be33f94488f2cadb690b0cf/uvicorn-0.38.0.tar.gz", hash = "sha256:fd97093bdd120a2609fc0d3afe931d4d4ad688b6e75f0f929fde1bc36fe0e91d", size = 80605, upload-time = "2025-10-18T13:46:44.63Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ee/d9/d88e73ca598f4f6ff671fb5fde8a32925c2e08a637303a1d12883c7305fa/uvicorn-0.38.0-py3-none-any.whl", hash = "sha256:48c0afd214ceb59340075b4a052ea1ee91c16fbc2a9b1469cca0e54566977b02", size = 68109 }, + { url = "https://files.pythonhosted.org/packages/ee/d9/d88e73ca598f4f6ff671fb5fde8a32925c2e08a637303a1d12883c7305fa/uvicorn-0.38.0-py3-none-any.whl", hash = "sha256:48c0afd214ceb59340075b4a052ea1ee91c16fbc2a9b1469cca0e54566977b02", size = 68109, upload-time = "2025-10-18T13:46:42.958Z" }, ] [package.optional-dependencies] @@ -2971,26 +2948,26 @@ standard = [ name = "uvloop" version = "0.22.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/06/f0/18d39dbd1971d6d62c4629cc7fa67f74821b0dc1f5a77af43719de7936a7/uvloop-0.22.1.tar.gz", hash = "sha256:6c84bae345b9147082b17371e3dd5d42775bddce91f885499017f4607fdaf39f", size = 2443250 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/89/8c/182a2a593195bfd39842ea68ebc084e20c850806117213f5a299dfc513d9/uvloop-0.22.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:561577354eb94200d75aca23fbde86ee11be36b00e52a4eaf8f50fb0c86b7705", size = 1358611 }, - { url = "https://files.pythonhosted.org/packages/d2/14/e301ee96a6dc95224b6f1162cd3312f6d1217be3907b79173b06785f2fe7/uvloop-0.22.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1cdf5192ab3e674ca26da2eada35b288d2fa49fdd0f357a19f0e7c4e7d5077c8", size = 751811 }, - { url = "https://files.pythonhosted.org/packages/b7/02/654426ce265ac19e2980bfd9ea6590ca96a56f10c76e63801a2df01c0486/uvloop-0.22.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6e2ea3d6190a2968f4a14a23019d3b16870dd2190cd69c8180f7c632d21de68d", size = 4288562 }, - { url = "https://files.pythonhosted.org/packages/15/c0/0be24758891ef825f2065cd5db8741aaddabe3e248ee6acc5e8a80f04005/uvloop-0.22.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0530a5fbad9c9e4ee3f2b33b148c6a64d47bbad8000ea63704fa8260f4cf728e", size = 4366890 }, - { url = "https://files.pythonhosted.org/packages/d2/53/8369e5219a5855869bcee5f4d317f6da0e2c669aecf0ef7d371e3d084449/uvloop-0.22.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bc5ef13bbc10b5335792360623cc378d52d7e62c2de64660616478c32cd0598e", size = 4119472 }, - { url = "https://files.pythonhosted.org/packages/f8/ba/d69adbe699b768f6b29a5eec7b47dd610bd17a69de51b251126a801369ea/uvloop-0.22.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1f38ec5e3f18c8a10ded09742f7fb8de0108796eb673f30ce7762ce1b8550cad", size = 4239051 }, - { url = "https://files.pythonhosted.org/packages/90/cd/b62bdeaa429758aee8de8b00ac0dd26593a9de93d302bff3d21439e9791d/uvloop-0.22.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3879b88423ec7e97cd4eba2a443aa26ed4e59b45e6b76aabf13fe2f27023a142", size = 1362067 }, - { url = "https://files.pythonhosted.org/packages/0d/f8/a132124dfda0777e489ca86732e85e69afcd1ff7686647000050ba670689/uvloop-0.22.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:4baa86acedf1d62115c1dc6ad1e17134476688f08c6efd8a2ab076e815665c74", size = 752423 }, - { url = "https://files.pythonhosted.org/packages/a3/94/94af78c156f88da4b3a733773ad5ba0b164393e357cc4bd0ab2e2677a7d6/uvloop-0.22.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:297c27d8003520596236bdb2335e6b3f649480bd09e00d1e3a99144b691d2a35", size = 4272437 }, - { url = "https://files.pythonhosted.org/packages/b5/35/60249e9fd07b32c665192cec7af29e06c7cd96fa1d08b84f012a56a0b38e/uvloop-0.22.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c1955d5a1dd43198244d47664a5858082a3239766a839b2102a269aaff7a4e25", size = 4292101 }, - { url = "https://files.pythonhosted.org/packages/02/62/67d382dfcb25d0a98ce73c11ed1a6fba5037a1a1d533dcbb7cab033a2636/uvloop-0.22.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b31dc2fccbd42adc73bc4e7cdbae4fc5086cf378979e53ca5d0301838c5682c6", size = 4114158 }, - { url = "https://files.pythonhosted.org/packages/f0/7a/f1171b4a882a5d13c8b7576f348acfe6074d72eaf52cccef752f748d4a9f/uvloop-0.22.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:93f617675b2d03af4e72a5333ef89450dfaa5321303ede6e67ba9c9d26878079", size = 4177360 }, - { url = "https://files.pythonhosted.org/packages/79/7b/b01414f31546caf0919da80ad57cbfe24c56b151d12af68cee1b04922ca8/uvloop-0.22.1-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:37554f70528f60cad66945b885eb01f1bb514f132d92b6eeed1c90fd54ed6289", size = 1454790 }, - { url = "https://files.pythonhosted.org/packages/d4/31/0bb232318dd838cad3fa8fb0c68c8b40e1145b32025581975e18b11fab40/uvloop-0.22.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:b76324e2dc033a0b2f435f33eb88ff9913c156ef78e153fb210e03c13da746b3", size = 796783 }, - { url = "https://files.pythonhosted.org/packages/42/38/c9b09f3271a7a723a5de69f8e237ab8e7803183131bc57c890db0b6bb872/uvloop-0.22.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:badb4d8e58ee08dad957002027830d5c3b06aea446a6a3744483c2b3b745345c", size = 4647548 }, - { url = "https://files.pythonhosted.org/packages/c1/37/945b4ca0ac27e3dc4952642d4c900edd030b3da6c9634875af6e13ae80e5/uvloop-0.22.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b91328c72635f6f9e0282e4a57da7470c7350ab1c9f48546c0f2866205349d21", size = 4467065 }, - { url = "https://files.pythonhosted.org/packages/97/cc/48d232f33d60e2e2e0b42f4e73455b146b76ebe216487e862700457fbf3c/uvloop-0.22.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:daf620c2995d193449393d6c62131b3fbd40a63bf7b307a1527856ace637fe88", size = 4328384 }, - { url = "https://files.pythonhosted.org/packages/e4/16/c1fd27e9549f3c4baf1dc9c20c456cd2f822dbf8de9f463824b0c0357e06/uvloop-0.22.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6cde23eeda1a25c75b2e07d39970f3374105d5eafbaab2a4482be82f272d5a5e", size = 4296730 }, +sdist = { url = "https://files.pythonhosted.org/packages/06/f0/18d39dbd1971d6d62c4629cc7fa67f74821b0dc1f5a77af43719de7936a7/uvloop-0.22.1.tar.gz", hash = "sha256:6c84bae345b9147082b17371e3dd5d42775bddce91f885499017f4607fdaf39f", size = 2443250, upload-time = "2025-10-16T22:17:19.342Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/89/8c/182a2a593195bfd39842ea68ebc084e20c850806117213f5a299dfc513d9/uvloop-0.22.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:561577354eb94200d75aca23fbde86ee11be36b00e52a4eaf8f50fb0c86b7705", size = 1358611, upload-time = "2025-10-16T22:16:36.833Z" }, + { url = "https://files.pythonhosted.org/packages/d2/14/e301ee96a6dc95224b6f1162cd3312f6d1217be3907b79173b06785f2fe7/uvloop-0.22.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1cdf5192ab3e674ca26da2eada35b288d2fa49fdd0f357a19f0e7c4e7d5077c8", size = 751811, upload-time = "2025-10-16T22:16:38.275Z" }, + { url = "https://files.pythonhosted.org/packages/b7/02/654426ce265ac19e2980bfd9ea6590ca96a56f10c76e63801a2df01c0486/uvloop-0.22.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6e2ea3d6190a2968f4a14a23019d3b16870dd2190cd69c8180f7c632d21de68d", size = 4288562, upload-time = "2025-10-16T22:16:39.375Z" }, + { url = "https://files.pythonhosted.org/packages/15/c0/0be24758891ef825f2065cd5db8741aaddabe3e248ee6acc5e8a80f04005/uvloop-0.22.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0530a5fbad9c9e4ee3f2b33b148c6a64d47bbad8000ea63704fa8260f4cf728e", size = 4366890, upload-time = "2025-10-16T22:16:40.547Z" }, + { url = "https://files.pythonhosted.org/packages/d2/53/8369e5219a5855869bcee5f4d317f6da0e2c669aecf0ef7d371e3d084449/uvloop-0.22.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bc5ef13bbc10b5335792360623cc378d52d7e62c2de64660616478c32cd0598e", size = 4119472, upload-time = "2025-10-16T22:16:41.694Z" }, + { url = "https://files.pythonhosted.org/packages/f8/ba/d69adbe699b768f6b29a5eec7b47dd610bd17a69de51b251126a801369ea/uvloop-0.22.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1f38ec5e3f18c8a10ded09742f7fb8de0108796eb673f30ce7762ce1b8550cad", size = 4239051, upload-time = "2025-10-16T22:16:43.224Z" }, + { url = "https://files.pythonhosted.org/packages/90/cd/b62bdeaa429758aee8de8b00ac0dd26593a9de93d302bff3d21439e9791d/uvloop-0.22.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3879b88423ec7e97cd4eba2a443aa26ed4e59b45e6b76aabf13fe2f27023a142", size = 1362067, upload-time = "2025-10-16T22:16:44.503Z" }, + { url = "https://files.pythonhosted.org/packages/0d/f8/a132124dfda0777e489ca86732e85e69afcd1ff7686647000050ba670689/uvloop-0.22.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:4baa86acedf1d62115c1dc6ad1e17134476688f08c6efd8a2ab076e815665c74", size = 752423, upload-time = "2025-10-16T22:16:45.968Z" }, + { url = "https://files.pythonhosted.org/packages/a3/94/94af78c156f88da4b3a733773ad5ba0b164393e357cc4bd0ab2e2677a7d6/uvloop-0.22.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:297c27d8003520596236bdb2335e6b3f649480bd09e00d1e3a99144b691d2a35", size = 4272437, upload-time = "2025-10-16T22:16:47.451Z" }, + { url = "https://files.pythonhosted.org/packages/b5/35/60249e9fd07b32c665192cec7af29e06c7cd96fa1d08b84f012a56a0b38e/uvloop-0.22.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c1955d5a1dd43198244d47664a5858082a3239766a839b2102a269aaff7a4e25", size = 4292101, upload-time = "2025-10-16T22:16:49.318Z" }, + { url = "https://files.pythonhosted.org/packages/02/62/67d382dfcb25d0a98ce73c11ed1a6fba5037a1a1d533dcbb7cab033a2636/uvloop-0.22.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b31dc2fccbd42adc73bc4e7cdbae4fc5086cf378979e53ca5d0301838c5682c6", size = 4114158, upload-time = "2025-10-16T22:16:50.517Z" }, + { url = "https://files.pythonhosted.org/packages/f0/7a/f1171b4a882a5d13c8b7576f348acfe6074d72eaf52cccef752f748d4a9f/uvloop-0.22.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:93f617675b2d03af4e72a5333ef89450dfaa5321303ede6e67ba9c9d26878079", size = 4177360, upload-time = "2025-10-16T22:16:52.646Z" }, + { url = "https://files.pythonhosted.org/packages/79/7b/b01414f31546caf0919da80ad57cbfe24c56b151d12af68cee1b04922ca8/uvloop-0.22.1-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:37554f70528f60cad66945b885eb01f1bb514f132d92b6eeed1c90fd54ed6289", size = 1454790, upload-time = "2025-10-16T22:16:54.355Z" }, + { url = "https://files.pythonhosted.org/packages/d4/31/0bb232318dd838cad3fa8fb0c68c8b40e1145b32025581975e18b11fab40/uvloop-0.22.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:b76324e2dc033a0b2f435f33eb88ff9913c156ef78e153fb210e03c13da746b3", size = 796783, upload-time = "2025-10-16T22:16:55.906Z" }, + { url = "https://files.pythonhosted.org/packages/42/38/c9b09f3271a7a723a5de69f8e237ab8e7803183131bc57c890db0b6bb872/uvloop-0.22.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:badb4d8e58ee08dad957002027830d5c3b06aea446a6a3744483c2b3b745345c", size = 4647548, upload-time = "2025-10-16T22:16:57.008Z" }, + { url = "https://files.pythonhosted.org/packages/c1/37/945b4ca0ac27e3dc4952642d4c900edd030b3da6c9634875af6e13ae80e5/uvloop-0.22.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b91328c72635f6f9e0282e4a57da7470c7350ab1c9f48546c0f2866205349d21", size = 4467065, upload-time = "2025-10-16T22:16:58.206Z" }, + { url = "https://files.pythonhosted.org/packages/97/cc/48d232f33d60e2e2e0b42f4e73455b146b76ebe216487e862700457fbf3c/uvloop-0.22.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:daf620c2995d193449393d6c62131b3fbd40a63bf7b307a1527856ace637fe88", size = 4328384, upload-time = "2025-10-16T22:16:59.36Z" }, + { url = "https://files.pythonhosted.org/packages/e4/16/c1fd27e9549f3c4baf1dc9c20c456cd2f822dbf8de9f463824b0c0357e06/uvloop-0.22.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6cde23eeda1a25c75b2e07d39970f3374105d5eafbaab2a4482be82f272d5a5e", size = 4296730, upload-time = "2025-10-16T22:17:00.744Z" }, ] [[package]] @@ -3000,131 +2977,131 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c2/c9/8869df9b2a2d6c59d79220a4db37679e74f807c559ffe5265e08b227a210/watchfiles-1.1.1.tar.gz", hash = "sha256:a173cb5c16c4f40ab19cecf48a534c409f7ea983ab8fed0741304a1c0a31b3f2", size = 94440 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/bb/f4/f750b29225fe77139f7ae5de89d4949f5a99f934c65a1f1c0b248f26f747/watchfiles-1.1.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:130e4876309e8686a5e37dba7d5e9bc77e6ed908266996ca26572437a5271e18", size = 404321 }, - { url = "https://files.pythonhosted.org/packages/2b/f9/f07a295cde762644aa4c4bb0f88921d2d141af45e735b965fb2e87858328/watchfiles-1.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5f3bde70f157f84ece3765b42b4a52c6ac1a50334903c6eaf765362f6ccca88a", size = 391783 }, - { url = "https://files.pythonhosted.org/packages/bc/11/fc2502457e0bea39a5c958d86d2cb69e407a4d00b85735ca724bfa6e0d1a/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:14e0b1fe858430fc0251737ef3824c54027bedb8c37c38114488b8e131cf8219", size = 449279 }, - { url = "https://files.pythonhosted.org/packages/e3/1f/d66bc15ea0b728df3ed96a539c777acfcad0eb78555ad9efcaa1274688f0/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f27db948078f3823a6bb3b465180db8ebecf26dd5dae6f6180bd87383b6b4428", size = 459405 }, - { url = "https://files.pythonhosted.org/packages/be/90/9f4a65c0aec3ccf032703e6db02d89a157462fbb2cf20dd415128251cac0/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:059098c3a429f62fc98e8ec62b982230ef2c8df68c79e826e37b895bc359a9c0", size = 488976 }, - { url = "https://files.pythonhosted.org/packages/37/57/ee347af605d867f712be7029bb94c8c071732a4b44792e3176fa3c612d39/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bfb5862016acc9b869bb57284e6cb35fdf8e22fe59f7548858e2f971d045f150", size = 595506 }, - { url = "https://files.pythonhosted.org/packages/a8/78/cc5ab0b86c122047f75e8fc471c67a04dee395daf847d3e59381996c8707/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:319b27255aacd9923b8a276bb14d21a5f7ff82564c744235fc5eae58d95422ae", size = 474936 }, - { url = "https://files.pythonhosted.org/packages/62/da/def65b170a3815af7bd40a3e7010bf6ab53089ef1b75d05dd5385b87cf08/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c755367e51db90e75b19454b680903631d41f9e3607fbd941d296a020c2d752d", size = 456147 }, - { url = "https://files.pythonhosted.org/packages/57/99/da6573ba71166e82d288d4df0839128004c67d2778d3b566c138695f5c0b/watchfiles-1.1.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c22c776292a23bfc7237a98f791b9ad3144b02116ff10d820829ce62dff46d0b", size = 630007 }, - { url = "https://files.pythonhosted.org/packages/a8/51/7439c4dd39511368849eb1e53279cd3454b4a4dbace80bab88feeb83c6b5/watchfiles-1.1.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:3a476189be23c3686bc2f4321dd501cb329c0a0469e77b7b534ee10129ae6374", size = 622280 }, - { url = "https://files.pythonhosted.org/packages/95/9c/8ed97d4bba5db6fdcdb2b298d3898f2dd5c20f6b73aee04eabe56c59677e/watchfiles-1.1.1-cp313-cp313-win32.whl", hash = "sha256:bf0a91bfb5574a2f7fc223cf95eeea79abfefa404bf1ea5e339c0c1560ae99a0", size = 272056 }, - { url = "https://files.pythonhosted.org/packages/1f/f3/c14e28429f744a260d8ceae18bf58c1d5fa56b50d006a7a9f80e1882cb0d/watchfiles-1.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:52e06553899e11e8074503c8e716d574adeeb7e68913115c4b3653c53f9bae42", size = 288162 }, - { url = "https://files.pythonhosted.org/packages/dc/61/fe0e56c40d5cd29523e398d31153218718c5786b5e636d9ae8ae79453d27/watchfiles-1.1.1-cp313-cp313-win_arm64.whl", hash = "sha256:ac3cc5759570cd02662b15fbcd9d917f7ecd47efe0d6b40474eafd246f91ea18", size = 277909 }, - { url = "https://files.pythonhosted.org/packages/79/42/e0a7d749626f1e28c7108a99fb9bf524b501bbbeb9b261ceecde644d5a07/watchfiles-1.1.1-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:563b116874a9a7ce6f96f87cd0b94f7faf92d08d0021e837796f0a14318ef8da", size = 403389 }, - { url = "https://files.pythonhosted.org/packages/15/49/08732f90ce0fbbc13913f9f215c689cfc9ced345fb1bcd8829a50007cc8d/watchfiles-1.1.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3ad9fe1dae4ab4212d8c91e80b832425e24f421703b5a42ef2e4a1e215aff051", size = 389964 }, - { url = "https://files.pythonhosted.org/packages/27/0d/7c315d4bd5f2538910491a0393c56bf70d333d51bc5b34bee8e68e8cea19/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce70f96a46b894b36eba678f153f052967a0d06d5b5a19b336ab0dbbd029f73e", size = 448114 }, - { url = "https://files.pythonhosted.org/packages/c3/24/9e096de47a4d11bc4df41e9d1e61776393eac4cb6eb11b3e23315b78b2cc/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:cb467c999c2eff23a6417e58d75e5828716f42ed8289fe6b77a7e5a91036ca70", size = 460264 }, - { url = "https://files.pythonhosted.org/packages/cc/0f/e8dea6375f1d3ba5fcb0b3583e2b493e77379834c74fd5a22d66d85d6540/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:836398932192dae4146c8f6f737d74baeac8b70ce14831a239bdb1ca882fc261", size = 487877 }, - { url = "https://files.pythonhosted.org/packages/ac/5b/df24cfc6424a12deb41503b64d42fbea6b8cb357ec62ca84a5a3476f654a/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:743185e7372b7bc7c389e1badcc606931a827112fbbd37f14c537320fca08620", size = 595176 }, - { url = "https://files.pythonhosted.org/packages/8f/b5/853b6757f7347de4e9b37e8cc3289283fb983cba1ab4d2d7144694871d9c/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:afaeff7696e0ad9f02cbb8f56365ff4686ab205fcf9c4c5b6fdfaaa16549dd04", size = 473577 }, - { url = "https://files.pythonhosted.org/packages/e1/f7/0a4467be0a56e80447c8529c9fce5b38eab4f513cb3d9bf82e7392a5696b/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3f7eb7da0eb23aa2ba036d4f616d46906013a68caf61b7fdbe42fc8b25132e77", size = 455425 }, - { url = "https://files.pythonhosted.org/packages/8e/e0/82583485ea00137ddf69bc84a2db88bd92ab4a6e3c405e5fb878ead8d0e7/watchfiles-1.1.1-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:831a62658609f0e5c64178211c942ace999517f5770fe9436be4c2faeba0c0ef", size = 628826 }, - { url = "https://files.pythonhosted.org/packages/28/9a/a785356fccf9fae84c0cc90570f11702ae9571036fb25932f1242c82191c/watchfiles-1.1.1-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:f9a2ae5c91cecc9edd47e041a930490c31c3afb1f5e6d71de3dc671bfaca02bf", size = 622208 }, - { url = "https://files.pythonhosted.org/packages/c3/f4/0872229324ef69b2c3edec35e84bd57a1289e7d3fe74588048ed8947a323/watchfiles-1.1.1-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:d1715143123baeeaeadec0528bb7441103979a1d5f6fd0e1f915383fea7ea6d5", size = 404315 }, - { url = "https://files.pythonhosted.org/packages/7b/22/16d5331eaed1cb107b873f6ae1b69e9ced582fcf0c59a50cd84f403b1c32/watchfiles-1.1.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:39574d6370c4579d7f5d0ad940ce5b20db0e4117444e39b6d8f99db5676c52fd", size = 390869 }, - { url = "https://files.pythonhosted.org/packages/b2/7e/5643bfff5acb6539b18483128fdc0ef2cccc94a5b8fbda130c823e8ed636/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7365b92c2e69ee952902e8f70f3ba6360d0d596d9299d55d7d386df84b6941fb", size = 449919 }, - { url = "https://files.pythonhosted.org/packages/51/2e/c410993ba5025a9f9357c376f48976ef0e1b1aefb73b97a5ae01a5972755/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bfff9740c69c0e4ed32416f013f3c45e2ae42ccedd1167ef2d805c000b6c71a5", size = 460845 }, - { url = "https://files.pythonhosted.org/packages/8e/a4/2df3b404469122e8680f0fcd06079317e48db58a2da2950fb45020947734/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b27cf2eb1dda37b2089e3907d8ea92922b673c0c427886d4edc6b94d8dfe5db3", size = 489027 }, - { url = "https://files.pythonhosted.org/packages/ea/84/4587ba5b1f267167ee715b7f66e6382cca6938e0a4b870adad93e44747e6/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:526e86aced14a65a5b0ec50827c745597c782ff46b571dbfe46192ab9e0b3c33", size = 595615 }, - { url = "https://files.pythonhosted.org/packages/6a/0f/c6988c91d06e93cd0bb3d4a808bcf32375ca1904609835c3031799e3ecae/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:04e78dd0b6352db95507fd8cb46f39d185cf8c74e4cf1e4fbad1d3df96faf510", size = 474836 }, - { url = "https://files.pythonhosted.org/packages/b4/36/ded8aebea91919485b7bbabbd14f5f359326cb5ec218cd67074d1e426d74/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5c85794a4cfa094714fb9c08d4a218375b2b95b8ed1666e8677c349906246c05", size = 455099 }, - { url = "https://files.pythonhosted.org/packages/98/e0/8c9bdba88af756a2fce230dd365fab2baf927ba42cd47521ee7498fd5211/watchfiles-1.1.1-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:74d5012b7630714b66be7b7b7a78855ef7ad58e8650c73afc4c076a1f480a8d6", size = 630626 }, - { url = "https://files.pythonhosted.org/packages/2a/84/a95db05354bf2d19e438520d92a8ca475e578c647f78f53197f5a2f17aaf/watchfiles-1.1.1-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:8fbe85cb3201c7d380d3d0b90e63d520f15d6afe217165d7f98c9c649654db81", size = 622519 }, - { url = "https://files.pythonhosted.org/packages/1d/ce/d8acdc8de545de995c339be67711e474c77d643555a9bb74a9334252bd55/watchfiles-1.1.1-cp314-cp314-win32.whl", hash = "sha256:3fa0b59c92278b5a7800d3ee7733da9d096d4aabcfabb9a928918bd276ef9b9b", size = 272078 }, - { url = "https://files.pythonhosted.org/packages/c4/c9/a74487f72d0451524be827e8edec251da0cc1fcf111646a511ae752e1a3d/watchfiles-1.1.1-cp314-cp314-win_amd64.whl", hash = "sha256:c2047d0b6cea13b3316bdbafbfa0c4228ae593d995030fda39089d36e64fc03a", size = 287664 }, - { url = "https://files.pythonhosted.org/packages/df/b8/8ac000702cdd496cdce998c6f4ee0ca1f15977bba51bdf07d872ebdfc34c/watchfiles-1.1.1-cp314-cp314-win_arm64.whl", hash = "sha256:842178b126593addc05acf6fce960d28bc5fae7afbaa2c6c1b3a7b9460e5be02", size = 277154 }, - { url = "https://files.pythonhosted.org/packages/47/a8/e3af2184707c29f0f14b1963c0aace6529f9d1b8582d5b99f31bbf42f59e/watchfiles-1.1.1-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:88863fbbc1a7312972f1c511f202eb30866370ebb8493aef2812b9ff28156a21", size = 403820 }, - { url = "https://files.pythonhosted.org/packages/c0/ec/e47e307c2f4bd75f9f9e8afbe3876679b18e1bcec449beca132a1c5ffb2d/watchfiles-1.1.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:55c7475190662e202c08c6c0f4d9e345a29367438cf8e8037f3155e10a88d5a5", size = 390510 }, - { url = "https://files.pythonhosted.org/packages/d5/a0/ad235642118090f66e7b2f18fd5c42082418404a79205cdfca50b6309c13/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3f53fa183d53a1d7a8852277c92b967ae99c2d4dcee2bfacff8868e6e30b15f7", size = 448408 }, - { url = "https://files.pythonhosted.org/packages/df/85/97fa10fd5ff3332ae17e7e40e20784e419e28521549780869f1413742e9d/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6aae418a8b323732fa89721d86f39ec8f092fc2af67f4217a2b07fd3e93c6101", size = 458968 }, - { url = "https://files.pythonhosted.org/packages/47/c2/9059c2e8966ea5ce678166617a7f75ecba6164375f3b288e50a40dc6d489/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f096076119da54a6080e8920cbdaac3dbee667eb91dcc5e5b78840b87415bd44", size = 488096 }, - { url = "https://files.pythonhosted.org/packages/94/44/d90a9ec8ac309bc26db808a13e7bfc0e4e78b6fc051078a554e132e80160/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:00485f441d183717038ed2e887a7c868154f216877653121068107b227a2f64c", size = 596040 }, - { url = "https://files.pythonhosted.org/packages/95/68/4e3479b20ca305cfc561db3ed207a8a1c745ee32bf24f2026a129d0ddb6e/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a55f3e9e493158d7bfdb60a1165035f1cf7d320914e7b7ea83fe22c6023b58fc", size = 473847 }, - { url = "https://files.pythonhosted.org/packages/4f/55/2af26693fd15165c4ff7857e38330e1b61ab8c37d15dc79118cdba115b7a/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8c91ed27800188c2ae96d16e3149f199d62f86c7af5f5f4d2c61a3ed8cd3666c", size = 455072 }, - { url = "https://files.pythonhosted.org/packages/66/1d/d0d200b10c9311ec25d2273f8aad8c3ef7cc7ea11808022501811208a750/watchfiles-1.1.1-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:311ff15a0bae3714ffb603e6ba6dbfba4065ab60865d15a6ec544133bdb21099", size = 629104 }, - { url = "https://files.pythonhosted.org/packages/e3/bd/fa9bb053192491b3867ba07d2343d9f2252e00811567d30ae8d0f78136fe/watchfiles-1.1.1-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:a916a2932da8f8ab582f242c065f5c81bed3462849ca79ee357dd9551b0e9b01", size = 622112 }, +sdist = { url = "https://files.pythonhosted.org/packages/c2/c9/8869df9b2a2d6c59d79220a4db37679e74f807c559ffe5265e08b227a210/watchfiles-1.1.1.tar.gz", hash = "sha256:a173cb5c16c4f40ab19cecf48a534c409f7ea983ab8fed0741304a1c0a31b3f2", size = 94440, upload-time = "2025-10-14T15:06:21.08Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bb/f4/f750b29225fe77139f7ae5de89d4949f5a99f934c65a1f1c0b248f26f747/watchfiles-1.1.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:130e4876309e8686a5e37dba7d5e9bc77e6ed908266996ca26572437a5271e18", size = 404321, upload-time = "2025-10-14T15:05:02.063Z" }, + { url = "https://files.pythonhosted.org/packages/2b/f9/f07a295cde762644aa4c4bb0f88921d2d141af45e735b965fb2e87858328/watchfiles-1.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5f3bde70f157f84ece3765b42b4a52c6ac1a50334903c6eaf765362f6ccca88a", size = 391783, upload-time = "2025-10-14T15:05:03.052Z" }, + { url = "https://files.pythonhosted.org/packages/bc/11/fc2502457e0bea39a5c958d86d2cb69e407a4d00b85735ca724bfa6e0d1a/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:14e0b1fe858430fc0251737ef3824c54027bedb8c37c38114488b8e131cf8219", size = 449279, upload-time = "2025-10-14T15:05:04.004Z" }, + { url = "https://files.pythonhosted.org/packages/e3/1f/d66bc15ea0b728df3ed96a539c777acfcad0eb78555ad9efcaa1274688f0/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f27db948078f3823a6bb3b465180db8ebecf26dd5dae6f6180bd87383b6b4428", size = 459405, upload-time = "2025-10-14T15:05:04.942Z" }, + { url = "https://files.pythonhosted.org/packages/be/90/9f4a65c0aec3ccf032703e6db02d89a157462fbb2cf20dd415128251cac0/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:059098c3a429f62fc98e8ec62b982230ef2c8df68c79e826e37b895bc359a9c0", size = 488976, upload-time = "2025-10-14T15:05:05.905Z" }, + { url = "https://files.pythonhosted.org/packages/37/57/ee347af605d867f712be7029bb94c8c071732a4b44792e3176fa3c612d39/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bfb5862016acc9b869bb57284e6cb35fdf8e22fe59f7548858e2f971d045f150", size = 595506, upload-time = "2025-10-14T15:05:06.906Z" }, + { url = "https://files.pythonhosted.org/packages/a8/78/cc5ab0b86c122047f75e8fc471c67a04dee395daf847d3e59381996c8707/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:319b27255aacd9923b8a276bb14d21a5f7ff82564c744235fc5eae58d95422ae", size = 474936, upload-time = "2025-10-14T15:05:07.906Z" }, + { url = "https://files.pythonhosted.org/packages/62/da/def65b170a3815af7bd40a3e7010bf6ab53089ef1b75d05dd5385b87cf08/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c755367e51db90e75b19454b680903631d41f9e3607fbd941d296a020c2d752d", size = 456147, upload-time = "2025-10-14T15:05:09.138Z" }, + { url = "https://files.pythonhosted.org/packages/57/99/da6573ba71166e82d288d4df0839128004c67d2778d3b566c138695f5c0b/watchfiles-1.1.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c22c776292a23bfc7237a98f791b9ad3144b02116ff10d820829ce62dff46d0b", size = 630007, upload-time = "2025-10-14T15:05:10.117Z" }, + { url = "https://files.pythonhosted.org/packages/a8/51/7439c4dd39511368849eb1e53279cd3454b4a4dbace80bab88feeb83c6b5/watchfiles-1.1.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:3a476189be23c3686bc2f4321dd501cb329c0a0469e77b7b534ee10129ae6374", size = 622280, upload-time = "2025-10-14T15:05:11.146Z" }, + { url = "https://files.pythonhosted.org/packages/95/9c/8ed97d4bba5db6fdcdb2b298d3898f2dd5c20f6b73aee04eabe56c59677e/watchfiles-1.1.1-cp313-cp313-win32.whl", hash = "sha256:bf0a91bfb5574a2f7fc223cf95eeea79abfefa404bf1ea5e339c0c1560ae99a0", size = 272056, upload-time = "2025-10-14T15:05:12.156Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f3/c14e28429f744a260d8ceae18bf58c1d5fa56b50d006a7a9f80e1882cb0d/watchfiles-1.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:52e06553899e11e8074503c8e716d574adeeb7e68913115c4b3653c53f9bae42", size = 288162, upload-time = "2025-10-14T15:05:13.208Z" }, + { url = "https://files.pythonhosted.org/packages/dc/61/fe0e56c40d5cd29523e398d31153218718c5786b5e636d9ae8ae79453d27/watchfiles-1.1.1-cp313-cp313-win_arm64.whl", hash = "sha256:ac3cc5759570cd02662b15fbcd9d917f7ecd47efe0d6b40474eafd246f91ea18", size = 277909, upload-time = "2025-10-14T15:05:14.49Z" }, + { url = "https://files.pythonhosted.org/packages/79/42/e0a7d749626f1e28c7108a99fb9bf524b501bbbeb9b261ceecde644d5a07/watchfiles-1.1.1-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:563b116874a9a7ce6f96f87cd0b94f7faf92d08d0021e837796f0a14318ef8da", size = 403389, upload-time = "2025-10-14T15:05:15.777Z" }, + { url = "https://files.pythonhosted.org/packages/15/49/08732f90ce0fbbc13913f9f215c689cfc9ced345fb1bcd8829a50007cc8d/watchfiles-1.1.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3ad9fe1dae4ab4212d8c91e80b832425e24f421703b5a42ef2e4a1e215aff051", size = 389964, upload-time = "2025-10-14T15:05:16.85Z" }, + { url = "https://files.pythonhosted.org/packages/27/0d/7c315d4bd5f2538910491a0393c56bf70d333d51bc5b34bee8e68e8cea19/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce70f96a46b894b36eba678f153f052967a0d06d5b5a19b336ab0dbbd029f73e", size = 448114, upload-time = "2025-10-14T15:05:17.876Z" }, + { url = "https://files.pythonhosted.org/packages/c3/24/9e096de47a4d11bc4df41e9d1e61776393eac4cb6eb11b3e23315b78b2cc/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:cb467c999c2eff23a6417e58d75e5828716f42ed8289fe6b77a7e5a91036ca70", size = 460264, upload-time = "2025-10-14T15:05:18.962Z" }, + { url = "https://files.pythonhosted.org/packages/cc/0f/e8dea6375f1d3ba5fcb0b3583e2b493e77379834c74fd5a22d66d85d6540/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:836398932192dae4146c8f6f737d74baeac8b70ce14831a239bdb1ca882fc261", size = 487877, upload-time = "2025-10-14T15:05:20.094Z" }, + { url = "https://files.pythonhosted.org/packages/ac/5b/df24cfc6424a12deb41503b64d42fbea6b8cb357ec62ca84a5a3476f654a/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:743185e7372b7bc7c389e1badcc606931a827112fbbd37f14c537320fca08620", size = 595176, upload-time = "2025-10-14T15:05:21.134Z" }, + { url = "https://files.pythonhosted.org/packages/8f/b5/853b6757f7347de4e9b37e8cc3289283fb983cba1ab4d2d7144694871d9c/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:afaeff7696e0ad9f02cbb8f56365ff4686ab205fcf9c4c5b6fdfaaa16549dd04", size = 473577, upload-time = "2025-10-14T15:05:22.306Z" }, + { url = "https://files.pythonhosted.org/packages/e1/f7/0a4467be0a56e80447c8529c9fce5b38eab4f513cb3d9bf82e7392a5696b/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3f7eb7da0eb23aa2ba036d4f616d46906013a68caf61b7fdbe42fc8b25132e77", size = 455425, upload-time = "2025-10-14T15:05:23.348Z" }, + { url = "https://files.pythonhosted.org/packages/8e/e0/82583485ea00137ddf69bc84a2db88bd92ab4a6e3c405e5fb878ead8d0e7/watchfiles-1.1.1-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:831a62658609f0e5c64178211c942ace999517f5770fe9436be4c2faeba0c0ef", size = 628826, upload-time = "2025-10-14T15:05:24.398Z" }, + { url = "https://files.pythonhosted.org/packages/28/9a/a785356fccf9fae84c0cc90570f11702ae9571036fb25932f1242c82191c/watchfiles-1.1.1-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:f9a2ae5c91cecc9edd47e041a930490c31c3afb1f5e6d71de3dc671bfaca02bf", size = 622208, upload-time = "2025-10-14T15:05:25.45Z" }, + { url = "https://files.pythonhosted.org/packages/c3/f4/0872229324ef69b2c3edec35e84bd57a1289e7d3fe74588048ed8947a323/watchfiles-1.1.1-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:d1715143123baeeaeadec0528bb7441103979a1d5f6fd0e1f915383fea7ea6d5", size = 404315, upload-time = "2025-10-14T15:05:26.501Z" }, + { url = "https://files.pythonhosted.org/packages/7b/22/16d5331eaed1cb107b873f6ae1b69e9ced582fcf0c59a50cd84f403b1c32/watchfiles-1.1.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:39574d6370c4579d7f5d0ad940ce5b20db0e4117444e39b6d8f99db5676c52fd", size = 390869, upload-time = "2025-10-14T15:05:27.649Z" }, + { url = "https://files.pythonhosted.org/packages/b2/7e/5643bfff5acb6539b18483128fdc0ef2cccc94a5b8fbda130c823e8ed636/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7365b92c2e69ee952902e8f70f3ba6360d0d596d9299d55d7d386df84b6941fb", size = 449919, upload-time = "2025-10-14T15:05:28.701Z" }, + { url = "https://files.pythonhosted.org/packages/51/2e/c410993ba5025a9f9357c376f48976ef0e1b1aefb73b97a5ae01a5972755/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bfff9740c69c0e4ed32416f013f3c45e2ae42ccedd1167ef2d805c000b6c71a5", size = 460845, upload-time = "2025-10-14T15:05:30.064Z" }, + { url = "https://files.pythonhosted.org/packages/8e/a4/2df3b404469122e8680f0fcd06079317e48db58a2da2950fb45020947734/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b27cf2eb1dda37b2089e3907d8ea92922b673c0c427886d4edc6b94d8dfe5db3", size = 489027, upload-time = "2025-10-14T15:05:31.064Z" }, + { url = "https://files.pythonhosted.org/packages/ea/84/4587ba5b1f267167ee715b7f66e6382cca6938e0a4b870adad93e44747e6/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:526e86aced14a65a5b0ec50827c745597c782ff46b571dbfe46192ab9e0b3c33", size = 595615, upload-time = "2025-10-14T15:05:32.074Z" }, + { url = "https://files.pythonhosted.org/packages/6a/0f/c6988c91d06e93cd0bb3d4a808bcf32375ca1904609835c3031799e3ecae/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:04e78dd0b6352db95507fd8cb46f39d185cf8c74e4cf1e4fbad1d3df96faf510", size = 474836, upload-time = "2025-10-14T15:05:33.209Z" }, + { url = "https://files.pythonhosted.org/packages/b4/36/ded8aebea91919485b7bbabbd14f5f359326cb5ec218cd67074d1e426d74/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5c85794a4cfa094714fb9c08d4a218375b2b95b8ed1666e8677c349906246c05", size = 455099, upload-time = "2025-10-14T15:05:34.189Z" }, + { url = "https://files.pythonhosted.org/packages/98/e0/8c9bdba88af756a2fce230dd365fab2baf927ba42cd47521ee7498fd5211/watchfiles-1.1.1-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:74d5012b7630714b66be7b7b7a78855ef7ad58e8650c73afc4c076a1f480a8d6", size = 630626, upload-time = "2025-10-14T15:05:35.216Z" }, + { url = "https://files.pythonhosted.org/packages/2a/84/a95db05354bf2d19e438520d92a8ca475e578c647f78f53197f5a2f17aaf/watchfiles-1.1.1-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:8fbe85cb3201c7d380d3d0b90e63d520f15d6afe217165d7f98c9c649654db81", size = 622519, upload-time = "2025-10-14T15:05:36.259Z" }, + { url = "https://files.pythonhosted.org/packages/1d/ce/d8acdc8de545de995c339be67711e474c77d643555a9bb74a9334252bd55/watchfiles-1.1.1-cp314-cp314-win32.whl", hash = "sha256:3fa0b59c92278b5a7800d3ee7733da9d096d4aabcfabb9a928918bd276ef9b9b", size = 272078, upload-time = "2025-10-14T15:05:37.63Z" }, + { url = "https://files.pythonhosted.org/packages/c4/c9/a74487f72d0451524be827e8edec251da0cc1fcf111646a511ae752e1a3d/watchfiles-1.1.1-cp314-cp314-win_amd64.whl", hash = "sha256:c2047d0b6cea13b3316bdbafbfa0c4228ae593d995030fda39089d36e64fc03a", size = 287664, upload-time = "2025-10-14T15:05:38.95Z" }, + { url = "https://files.pythonhosted.org/packages/df/b8/8ac000702cdd496cdce998c6f4ee0ca1f15977bba51bdf07d872ebdfc34c/watchfiles-1.1.1-cp314-cp314-win_arm64.whl", hash = "sha256:842178b126593addc05acf6fce960d28bc5fae7afbaa2c6c1b3a7b9460e5be02", size = 277154, upload-time = "2025-10-14T15:05:39.954Z" }, + { url = "https://files.pythonhosted.org/packages/47/a8/e3af2184707c29f0f14b1963c0aace6529f9d1b8582d5b99f31bbf42f59e/watchfiles-1.1.1-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:88863fbbc1a7312972f1c511f202eb30866370ebb8493aef2812b9ff28156a21", size = 403820, upload-time = "2025-10-14T15:05:40.932Z" }, + { url = "https://files.pythonhosted.org/packages/c0/ec/e47e307c2f4bd75f9f9e8afbe3876679b18e1bcec449beca132a1c5ffb2d/watchfiles-1.1.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:55c7475190662e202c08c6c0f4d9e345a29367438cf8e8037f3155e10a88d5a5", size = 390510, upload-time = "2025-10-14T15:05:41.945Z" }, + { url = "https://files.pythonhosted.org/packages/d5/a0/ad235642118090f66e7b2f18fd5c42082418404a79205cdfca50b6309c13/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3f53fa183d53a1d7a8852277c92b967ae99c2d4dcee2bfacff8868e6e30b15f7", size = 448408, upload-time = "2025-10-14T15:05:43.385Z" }, + { url = "https://files.pythonhosted.org/packages/df/85/97fa10fd5ff3332ae17e7e40e20784e419e28521549780869f1413742e9d/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6aae418a8b323732fa89721d86f39ec8f092fc2af67f4217a2b07fd3e93c6101", size = 458968, upload-time = "2025-10-14T15:05:44.404Z" }, + { url = "https://files.pythonhosted.org/packages/47/c2/9059c2e8966ea5ce678166617a7f75ecba6164375f3b288e50a40dc6d489/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f096076119da54a6080e8920cbdaac3dbee667eb91dcc5e5b78840b87415bd44", size = 488096, upload-time = "2025-10-14T15:05:45.398Z" }, + { url = "https://files.pythonhosted.org/packages/94/44/d90a9ec8ac309bc26db808a13e7bfc0e4e78b6fc051078a554e132e80160/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:00485f441d183717038ed2e887a7c868154f216877653121068107b227a2f64c", size = 596040, upload-time = "2025-10-14T15:05:46.502Z" }, + { url = "https://files.pythonhosted.org/packages/95/68/4e3479b20ca305cfc561db3ed207a8a1c745ee32bf24f2026a129d0ddb6e/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a55f3e9e493158d7bfdb60a1165035f1cf7d320914e7b7ea83fe22c6023b58fc", size = 473847, upload-time = "2025-10-14T15:05:47.484Z" }, + { url = "https://files.pythonhosted.org/packages/4f/55/2af26693fd15165c4ff7857e38330e1b61ab8c37d15dc79118cdba115b7a/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8c91ed27800188c2ae96d16e3149f199d62f86c7af5f5f4d2c61a3ed8cd3666c", size = 455072, upload-time = "2025-10-14T15:05:48.928Z" }, + { url = "https://files.pythonhosted.org/packages/66/1d/d0d200b10c9311ec25d2273f8aad8c3ef7cc7ea11808022501811208a750/watchfiles-1.1.1-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:311ff15a0bae3714ffb603e6ba6dbfba4065ab60865d15a6ec544133bdb21099", size = 629104, upload-time = "2025-10-14T15:05:49.908Z" }, + { url = "https://files.pythonhosted.org/packages/e3/bd/fa9bb053192491b3867ba07d2343d9f2252e00811567d30ae8d0f78136fe/watchfiles-1.1.1-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:a916a2932da8f8ab582f242c065f5c81bed3462849ca79ee357dd9551b0e9b01", size = 622112, upload-time = "2025-10-14T15:05:50.941Z" }, ] [[package]] name = "wcwidth" version = "0.2.14" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/24/30/6b0809f4510673dc723187aeaf24c7f5459922d01e2f794277a3dfb90345/wcwidth-0.2.14.tar.gz", hash = "sha256:4d478375d31bc5395a3c55c40ccdf3354688364cd61c4f6adacaa9215d0b3605", size = 102293 } +sdist = { url = "https://files.pythonhosted.org/packages/24/30/6b0809f4510673dc723187aeaf24c7f5459922d01e2f794277a3dfb90345/wcwidth-0.2.14.tar.gz", hash = "sha256:4d478375d31bc5395a3c55c40ccdf3354688364cd61c4f6adacaa9215d0b3605", size = 102293, upload-time = "2025-09-22T16:29:53.023Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/af/b5/123f13c975e9f27ab9c0770f514345bd406d0e8d3b7a0723af9d43f710af/wcwidth-0.2.14-py2.py3-none-any.whl", hash = "sha256:a7bb560c8aee30f9957e5f9895805edd20602f2d7f720186dfd906e82b4982e1", size = 37286 }, + { url = "https://files.pythonhosted.org/packages/af/b5/123f13c975e9f27ab9c0770f514345bd406d0e8d3b7a0723af9d43f710af/wcwidth-0.2.14-py2.py3-none-any.whl", hash = "sha256:a7bb560c8aee30f9957e5f9895805edd20602f2d7f720186dfd906e82b4982e1", size = 37286, upload-time = "2025-09-22T16:29:51.641Z" }, ] [[package]] name = "websocket-client" version = "1.9.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/2c/41/aa4bf9664e4cda14c3b39865b12251e8e7d239f4cd0e3cc1b6c2ccde25c1/websocket_client-1.9.0.tar.gz", hash = "sha256:9e813624b6eb619999a97dc7958469217c3176312b3a16a4bd1bc7e08a46ec98", size = 70576 } +sdist = { url = "https://files.pythonhosted.org/packages/2c/41/aa4bf9664e4cda14c3b39865b12251e8e7d239f4cd0e3cc1b6c2ccde25c1/websocket_client-1.9.0.tar.gz", hash = "sha256:9e813624b6eb619999a97dc7958469217c3176312b3a16a4bd1bc7e08a46ec98", size = 70576, upload-time = "2025-10-07T21:16:36.495Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/34/db/b10e48aa8fff7407e67470363eac595018441cf32d5e1001567a7aeba5d2/websocket_client-1.9.0-py3-none-any.whl", hash = "sha256:af248a825037ef591efbf6ed20cc5faa03d3b47b9e5a2230a529eeee1c1fc3ef", size = 82616 }, + { url = "https://files.pythonhosted.org/packages/34/db/b10e48aa8fff7407e67470363eac595018441cf32d5e1001567a7aeba5d2/websocket_client-1.9.0-py3-none-any.whl", hash = "sha256:af248a825037ef591efbf6ed20cc5faa03d3b47b9e5a2230a529eeee1c1fc3ef", size = 82616, upload-time = "2025-10-07T21:16:34.951Z" }, ] [[package]] name = "websockets" version = "15.0.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/21/e6/26d09fab466b7ca9c7737474c52be4f76a40301b08362eb2dbc19dcc16c1/websockets-15.0.1.tar.gz", hash = "sha256:82544de02076bafba038ce055ee6412d68da13ab47f0c60cab827346de828dee", size = 177016 } +sdist = { url = "https://files.pythonhosted.org/packages/21/e6/26d09fab466b7ca9c7737474c52be4f76a40301b08362eb2dbc19dcc16c1/websockets-15.0.1.tar.gz", hash = "sha256:82544de02076bafba038ce055ee6412d68da13ab47f0c60cab827346de828dee", size = 177016, upload-time = "2025-03-05T20:03:41.606Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/cb/9f/51f0cf64471a9d2b4d0fc6c534f323b664e7095640c34562f5182e5a7195/websockets-15.0.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ee443ef070bb3b6ed74514f5efaa37a252af57c90eb33b956d35c8e9c10a1931", size = 175440 }, - { url = "https://files.pythonhosted.org/packages/8a/05/aa116ec9943c718905997412c5989f7ed671bc0188ee2ba89520e8765d7b/websockets-15.0.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5a939de6b7b4e18ca683218320fc67ea886038265fd1ed30173f5ce3f8e85675", size = 173098 }, - { url = "https://files.pythonhosted.org/packages/ff/0b/33cef55ff24f2d92924923c99926dcce78e7bd922d649467f0eda8368923/websockets-15.0.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:746ee8dba912cd6fc889a8147168991d50ed70447bf18bcda7039f7d2e3d9151", size = 173329 }, - { url = "https://files.pythonhosted.org/packages/31/1d/063b25dcc01faa8fada1469bdf769de3768b7044eac9d41f734fd7b6ad6d/websockets-15.0.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:595b6c3969023ecf9041b2936ac3827e4623bfa3ccf007575f04c5a6aa318c22", size = 183111 }, - { url = "https://files.pythonhosted.org/packages/93/53/9a87ee494a51bf63e4ec9241c1ccc4f7c2f45fff85d5bde2ff74fcb68b9e/websockets-15.0.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3c714d2fc58b5ca3e285461a4cc0c9a66bd0e24c5da9911e30158286c9b5be7f", size = 182054 }, - { url = "https://files.pythonhosted.org/packages/ff/b2/83a6ddf56cdcbad4e3d841fcc55d6ba7d19aeb89c50f24dd7e859ec0805f/websockets-15.0.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f3c1e2ab208db911594ae5b4f79addeb3501604a165019dd221c0bdcabe4db8", size = 182496 }, - { url = "https://files.pythonhosted.org/packages/98/41/e7038944ed0abf34c45aa4635ba28136f06052e08fc2168520bb8b25149f/websockets-15.0.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:229cf1d3ca6c1804400b0a9790dc66528e08a6a1feec0d5040e8b9eb14422375", size = 182829 }, - { url = "https://files.pythonhosted.org/packages/e0/17/de15b6158680c7623c6ef0db361da965ab25d813ae54fcfeae2e5b9ef910/websockets-15.0.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:756c56e867a90fb00177d530dca4b097dd753cde348448a1012ed6c5131f8b7d", size = 182217 }, - { url = "https://files.pythonhosted.org/packages/33/2b/1f168cb6041853eef0362fb9554c3824367c5560cbdaad89ac40f8c2edfc/websockets-15.0.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:558d023b3df0bffe50a04e710bc87742de35060580a293c2a984299ed83bc4e4", size = 182195 }, - { url = "https://files.pythonhosted.org/packages/86/eb/20b6cdf273913d0ad05a6a14aed4b9a85591c18a987a3d47f20fa13dcc47/websockets-15.0.1-cp313-cp313-win32.whl", hash = "sha256:ba9e56e8ceeeedb2e080147ba85ffcd5cd0711b89576b83784d8605a7df455fa", size = 176393 }, - { url = "https://files.pythonhosted.org/packages/1b/6c/c65773d6cab416a64d191d6ee8a8b1c68a09970ea6909d16965d26bfed1e/websockets-15.0.1-cp313-cp313-win_amd64.whl", hash = "sha256:e09473f095a819042ecb2ab9465aee615bd9c2028e4ef7d933600a8401c79561", size = 176837 }, - { url = "https://files.pythonhosted.org/packages/fa/a8/5b41e0da817d64113292ab1f8247140aac61cbf6cfd085d6a0fa77f4984f/websockets-15.0.1-py3-none-any.whl", hash = "sha256:f7a866fbc1e97b5c617ee4116daaa09b722101d4a3c170c787450ba409f9736f", size = 169743 }, + { url = "https://files.pythonhosted.org/packages/cb/9f/51f0cf64471a9d2b4d0fc6c534f323b664e7095640c34562f5182e5a7195/websockets-15.0.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ee443ef070bb3b6ed74514f5efaa37a252af57c90eb33b956d35c8e9c10a1931", size = 175440, upload-time = "2025-03-05T20:02:36.695Z" }, + { url = "https://files.pythonhosted.org/packages/8a/05/aa116ec9943c718905997412c5989f7ed671bc0188ee2ba89520e8765d7b/websockets-15.0.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5a939de6b7b4e18ca683218320fc67ea886038265fd1ed30173f5ce3f8e85675", size = 173098, upload-time = "2025-03-05T20:02:37.985Z" }, + { url = "https://files.pythonhosted.org/packages/ff/0b/33cef55ff24f2d92924923c99926dcce78e7bd922d649467f0eda8368923/websockets-15.0.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:746ee8dba912cd6fc889a8147168991d50ed70447bf18bcda7039f7d2e3d9151", size = 173329, upload-time = "2025-03-05T20:02:39.298Z" }, + { url = "https://files.pythonhosted.org/packages/31/1d/063b25dcc01faa8fada1469bdf769de3768b7044eac9d41f734fd7b6ad6d/websockets-15.0.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:595b6c3969023ecf9041b2936ac3827e4623bfa3ccf007575f04c5a6aa318c22", size = 183111, upload-time = "2025-03-05T20:02:40.595Z" }, + { url = "https://files.pythonhosted.org/packages/93/53/9a87ee494a51bf63e4ec9241c1ccc4f7c2f45fff85d5bde2ff74fcb68b9e/websockets-15.0.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3c714d2fc58b5ca3e285461a4cc0c9a66bd0e24c5da9911e30158286c9b5be7f", size = 182054, upload-time = "2025-03-05T20:02:41.926Z" }, + { url = "https://files.pythonhosted.org/packages/ff/b2/83a6ddf56cdcbad4e3d841fcc55d6ba7d19aeb89c50f24dd7e859ec0805f/websockets-15.0.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f3c1e2ab208db911594ae5b4f79addeb3501604a165019dd221c0bdcabe4db8", size = 182496, upload-time = "2025-03-05T20:02:43.304Z" }, + { url = "https://files.pythonhosted.org/packages/98/41/e7038944ed0abf34c45aa4635ba28136f06052e08fc2168520bb8b25149f/websockets-15.0.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:229cf1d3ca6c1804400b0a9790dc66528e08a6a1feec0d5040e8b9eb14422375", size = 182829, upload-time = "2025-03-05T20:02:48.812Z" }, + { url = "https://files.pythonhosted.org/packages/e0/17/de15b6158680c7623c6ef0db361da965ab25d813ae54fcfeae2e5b9ef910/websockets-15.0.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:756c56e867a90fb00177d530dca4b097dd753cde348448a1012ed6c5131f8b7d", size = 182217, upload-time = "2025-03-05T20:02:50.14Z" }, + { url = "https://files.pythonhosted.org/packages/33/2b/1f168cb6041853eef0362fb9554c3824367c5560cbdaad89ac40f8c2edfc/websockets-15.0.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:558d023b3df0bffe50a04e710bc87742de35060580a293c2a984299ed83bc4e4", size = 182195, upload-time = "2025-03-05T20:02:51.561Z" }, + { url = "https://files.pythonhosted.org/packages/86/eb/20b6cdf273913d0ad05a6a14aed4b9a85591c18a987a3d47f20fa13dcc47/websockets-15.0.1-cp313-cp313-win32.whl", hash = "sha256:ba9e56e8ceeeedb2e080147ba85ffcd5cd0711b89576b83784d8605a7df455fa", size = 176393, upload-time = "2025-03-05T20:02:53.814Z" }, + { url = "https://files.pythonhosted.org/packages/1b/6c/c65773d6cab416a64d191d6ee8a8b1c68a09970ea6909d16965d26bfed1e/websockets-15.0.1-cp313-cp313-win_amd64.whl", hash = "sha256:e09473f095a819042ecb2ab9465aee615bd9c2028e4ef7d933600a8401c79561", size = 176837, upload-time = "2025-03-05T20:02:55.237Z" }, + { url = "https://files.pythonhosted.org/packages/fa/a8/5b41e0da817d64113292ab1f8247140aac61cbf6cfd085d6a0fa77f4984f/websockets-15.0.1-py3-none-any.whl", hash = "sha256:f7a866fbc1e97b5c617ee4116daaa09b722101d4a3c170c787450ba409f9736f", size = 169743, upload-time = "2025-03-05T20:03:39.41Z" }, ] [[package]] name = "wrapt" version = "1.17.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/95/8f/aeb76c5b46e273670962298c23e7ddde79916cb74db802131d49a85e4b7d/wrapt-1.17.3.tar.gz", hash = "sha256:f66eb08feaa410fe4eebd17f2a2c8e2e46d3476e9f8c783daa8e09e0faa666d0", size = 55547 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/fc/f6/759ece88472157acb55fc195e5b116e06730f1b651b5b314c66291729193/wrapt-1.17.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a47681378a0439215912ef542c45a783484d4dd82bac412b71e59cf9c0e1cea0", size = 54003 }, - { url = "https://files.pythonhosted.org/packages/4f/a9/49940b9dc6d47027dc850c116d79b4155f15c08547d04db0f07121499347/wrapt-1.17.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:54a30837587c6ee3cd1a4d1c2ec5d24e77984d44e2f34547e2323ddb4e22eb77", size = 39025 }, - { url = "https://files.pythonhosted.org/packages/45/35/6a08de0f2c96dcdd7fe464d7420ddb9a7655a6561150e5fc4da9356aeaab/wrapt-1.17.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:16ecf15d6af39246fe33e507105d67e4b81d8f8d2c6598ff7e3ca1b8a37213f7", size = 39108 }, - { url = "https://files.pythonhosted.org/packages/0c/37/6faf15cfa41bf1f3dba80cd3f5ccc6622dfccb660ab26ed79f0178c7497f/wrapt-1.17.3-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6fd1ad24dc235e4ab88cda009e19bf347aabb975e44fd5c2fb22a3f6e4141277", size = 88072 }, - { url = "https://files.pythonhosted.org/packages/78/f2/efe19ada4a38e4e15b6dff39c3e3f3f73f5decf901f66e6f72fe79623a06/wrapt-1.17.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ed61b7c2d49cee3c027372df5809a59d60cf1b6c2f81ee980a091f3afed6a2d", size = 88214 }, - { url = "https://files.pythonhosted.org/packages/40/90/ca86701e9de1622b16e09689fc24b76f69b06bb0150990f6f4e8b0eeb576/wrapt-1.17.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:423ed5420ad5f5529db9ce89eac09c8a2f97da18eb1c870237e84c5a5c2d60aa", size = 87105 }, - { url = "https://files.pythonhosted.org/packages/fd/e0/d10bd257c9a3e15cbf5523025252cc14d77468e8ed644aafb2d6f54cb95d/wrapt-1.17.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e01375f275f010fcbf7f643b4279896d04e571889b8a5b3f848423d91bf07050", size = 87766 }, - { url = "https://files.pythonhosted.org/packages/e8/cf/7d848740203c7b4b27eb55dbfede11aca974a51c3d894f6cc4b865f42f58/wrapt-1.17.3-cp313-cp313-win32.whl", hash = "sha256:53e5e39ff71b3fc484df8a522c933ea2b7cdd0d5d15ae82e5b23fde87d44cbd8", size = 36711 }, - { url = "https://files.pythonhosted.org/packages/57/54/35a84d0a4d23ea675994104e667ceff49227ce473ba6a59ba2c84f250b74/wrapt-1.17.3-cp313-cp313-win_amd64.whl", hash = "sha256:1f0b2f40cf341ee8cc1a97d51ff50dddb9fcc73241b9143ec74b30fc4f44f6cb", size = 38885 }, - { url = "https://files.pythonhosted.org/packages/01/77/66e54407c59d7b02a3c4e0af3783168fff8e5d61def52cda8728439d86bc/wrapt-1.17.3-cp313-cp313-win_arm64.whl", hash = "sha256:7425ac3c54430f5fc5e7b6f41d41e704db073309acfc09305816bc6a0b26bb16", size = 36896 }, - { url = "https://files.pythonhosted.org/packages/02/a2/cd864b2a14f20d14f4c496fab97802001560f9f41554eef6df201cd7f76c/wrapt-1.17.3-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:cf30f6e3c077c8e6a9a7809c94551203c8843e74ba0c960f4a98cd80d4665d39", size = 54132 }, - { url = "https://files.pythonhosted.org/packages/d5/46/d011725b0c89e853dc44cceb738a307cde5d240d023d6d40a82d1b4e1182/wrapt-1.17.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:e228514a06843cae89621384cfe3a80418f3c04aadf8a3b14e46a7be704e4235", size = 39091 }, - { url = "https://files.pythonhosted.org/packages/2e/9e/3ad852d77c35aae7ddebdbc3b6d35ec8013af7d7dddad0ad911f3d891dae/wrapt-1.17.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:5ea5eb3c0c071862997d6f3e02af1d055f381b1d25b286b9d6644b79db77657c", size = 39172 }, - { url = "https://files.pythonhosted.org/packages/c3/f7/c983d2762bcce2326c317c26a6a1e7016f7eb039c27cdf5c4e30f4160f31/wrapt-1.17.3-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:281262213373b6d5e4bb4353bc36d1ba4084e6d6b5d242863721ef2bf2c2930b", size = 87163 }, - { url = "https://files.pythonhosted.org/packages/e4/0f/f673f75d489c7f22d17fe0193e84b41540d962f75fce579cf6873167c29b/wrapt-1.17.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dc4a8d2b25efb6681ecacad42fca8859f88092d8732b170de6a5dddd80a1c8fa", size = 87963 }, - { url = "https://files.pythonhosted.org/packages/df/61/515ad6caca68995da2fac7a6af97faab8f78ebe3bf4f761e1b77efbc47b5/wrapt-1.17.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:373342dd05b1d07d752cecbec0c41817231f29f3a89aa8b8843f7b95992ed0c7", size = 86945 }, - { url = "https://files.pythonhosted.org/packages/d3/bd/4e70162ce398462a467bc09e768bee112f1412e563620adc353de9055d33/wrapt-1.17.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d40770d7c0fd5cbed9d84b2c3f2e156431a12c9a37dc6284060fb4bec0b7ffd4", size = 86857 }, - { url = "https://files.pythonhosted.org/packages/2b/b8/da8560695e9284810b8d3df8a19396a6e40e7518059584a1a394a2b35e0a/wrapt-1.17.3-cp314-cp314-win32.whl", hash = "sha256:fbd3c8319de8e1dc79d346929cd71d523622da527cca14e0c1d257e31c2b8b10", size = 37178 }, - { url = "https://files.pythonhosted.org/packages/db/c8/b71eeb192c440d67a5a0449aaee2310a1a1e8eca41676046f99ed2487e9f/wrapt-1.17.3-cp314-cp314-win_amd64.whl", hash = "sha256:e1a4120ae5705f673727d3253de3ed0e016f7cd78dc463db1b31e2463e1f3cf6", size = 39310 }, - { url = "https://files.pythonhosted.org/packages/45/20/2cda20fd4865fa40f86f6c46ed37a2a8356a7a2fde0773269311f2af56c7/wrapt-1.17.3-cp314-cp314-win_arm64.whl", hash = "sha256:507553480670cab08a800b9463bdb881b2edeed77dc677b0a5915e6106e91a58", size = 37266 }, - { url = "https://files.pythonhosted.org/packages/77/ed/dd5cf21aec36c80443c6f900449260b80e2a65cf963668eaef3b9accce36/wrapt-1.17.3-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:ed7c635ae45cfbc1a7371f708727bf74690daedc49b4dba310590ca0bd28aa8a", size = 56544 }, - { url = "https://files.pythonhosted.org/packages/8d/96/450c651cc753877ad100c7949ab4d2e2ecc4d97157e00fa8f45df682456a/wrapt-1.17.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:249f88ed15503f6492a71f01442abddd73856a0032ae860de6d75ca62eed8067", size = 40283 }, - { url = "https://files.pythonhosted.org/packages/d1/86/2fcad95994d9b572db57632acb6f900695a648c3e063f2cd344b3f5c5a37/wrapt-1.17.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5a03a38adec8066d5a37bea22f2ba6bbf39fcdefbe2d91419ab864c3fb515454", size = 40366 }, - { url = "https://files.pythonhosted.org/packages/64/0e/f4472f2fdde2d4617975144311f8800ef73677a159be7fe61fa50997d6c0/wrapt-1.17.3-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:5d4478d72eb61c36e5b446e375bbc49ed002430d17cdec3cecb36993398e1a9e", size = 108571 }, - { url = "https://files.pythonhosted.org/packages/cc/01/9b85a99996b0a97c8a17484684f206cbb6ba73c1ce6890ac668bcf3838fb/wrapt-1.17.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:223db574bb38637e8230eb14b185565023ab624474df94d2af18f1cdb625216f", size = 113094 }, - { url = "https://files.pythonhosted.org/packages/25/02/78926c1efddcc7b3aa0bc3d6b33a822f7d898059f7cd9ace8c8318e559ef/wrapt-1.17.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e405adefb53a435f01efa7ccdec012c016b5a1d3f35459990afc39b6be4d5056", size = 110659 }, - { url = "https://files.pythonhosted.org/packages/dc/ee/c414501ad518ac3e6fe184753632fe5e5ecacdcf0effc23f31c1e4f7bfcf/wrapt-1.17.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:88547535b787a6c9ce4086917b6e1d291aa8ed914fdd3a838b3539dc95c12804", size = 106946 }, - { url = "https://files.pythonhosted.org/packages/be/44/a1bd64b723d13bb151d6cc91b986146a1952385e0392a78567e12149c7b4/wrapt-1.17.3-cp314-cp314t-win32.whl", hash = "sha256:41b1d2bc74c2cac6f9074df52b2efbef2b30bdfe5f40cb78f8ca22963bc62977", size = 38717 }, - { url = "https://files.pythonhosted.org/packages/79/d9/7cfd5a312760ac4dd8bf0184a6ee9e43c33e47f3dadc303032ce012b8fa3/wrapt-1.17.3-cp314-cp314t-win_amd64.whl", hash = "sha256:73d496de46cd2cdbdbcce4ae4bcdb4afb6a11234a1df9c085249d55166b95116", size = 41334 }, - { url = "https://files.pythonhosted.org/packages/46/78/10ad9781128ed2f99dbc474f43283b13fea8ba58723e98844367531c18e9/wrapt-1.17.3-cp314-cp314t-win_arm64.whl", hash = "sha256:f38e60678850c42461d4202739f9bf1e3a737c7ad283638251e79cc49effb6b6", size = 38471 }, - { url = "https://files.pythonhosted.org/packages/1f/f6/a933bd70f98e9cf3e08167fc5cd7aaaca49147e48411c0bd5ae701bb2194/wrapt-1.17.3-py3-none-any.whl", hash = "sha256:7171ae35d2c33d326ac19dd8facb1e82e5fd04ef8c6c0e394d7af55a55051c22", size = 23591 }, +sdist = { url = "https://files.pythonhosted.org/packages/95/8f/aeb76c5b46e273670962298c23e7ddde79916cb74db802131d49a85e4b7d/wrapt-1.17.3.tar.gz", hash = "sha256:f66eb08feaa410fe4eebd17f2a2c8e2e46d3476e9f8c783daa8e09e0faa666d0", size = 55547, upload-time = "2025-08-12T05:53:21.714Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fc/f6/759ece88472157acb55fc195e5b116e06730f1b651b5b314c66291729193/wrapt-1.17.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a47681378a0439215912ef542c45a783484d4dd82bac412b71e59cf9c0e1cea0", size = 54003, upload-time = "2025-08-12T05:51:48.627Z" }, + { url = "https://files.pythonhosted.org/packages/4f/a9/49940b9dc6d47027dc850c116d79b4155f15c08547d04db0f07121499347/wrapt-1.17.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:54a30837587c6ee3cd1a4d1c2ec5d24e77984d44e2f34547e2323ddb4e22eb77", size = 39025, upload-time = "2025-08-12T05:51:37.156Z" }, + { url = "https://files.pythonhosted.org/packages/45/35/6a08de0f2c96dcdd7fe464d7420ddb9a7655a6561150e5fc4da9356aeaab/wrapt-1.17.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:16ecf15d6af39246fe33e507105d67e4b81d8f8d2c6598ff7e3ca1b8a37213f7", size = 39108, upload-time = "2025-08-12T05:51:58.425Z" }, + { url = "https://files.pythonhosted.org/packages/0c/37/6faf15cfa41bf1f3dba80cd3f5ccc6622dfccb660ab26ed79f0178c7497f/wrapt-1.17.3-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6fd1ad24dc235e4ab88cda009e19bf347aabb975e44fd5c2fb22a3f6e4141277", size = 88072, upload-time = "2025-08-12T05:52:37.53Z" }, + { url = "https://files.pythonhosted.org/packages/78/f2/efe19ada4a38e4e15b6dff39c3e3f3f73f5decf901f66e6f72fe79623a06/wrapt-1.17.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ed61b7c2d49cee3c027372df5809a59d60cf1b6c2f81ee980a091f3afed6a2d", size = 88214, upload-time = "2025-08-12T05:52:15.886Z" }, + { url = "https://files.pythonhosted.org/packages/40/90/ca86701e9de1622b16e09689fc24b76f69b06bb0150990f6f4e8b0eeb576/wrapt-1.17.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:423ed5420ad5f5529db9ce89eac09c8a2f97da18eb1c870237e84c5a5c2d60aa", size = 87105, upload-time = "2025-08-12T05:52:17.914Z" }, + { url = "https://files.pythonhosted.org/packages/fd/e0/d10bd257c9a3e15cbf5523025252cc14d77468e8ed644aafb2d6f54cb95d/wrapt-1.17.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e01375f275f010fcbf7f643b4279896d04e571889b8a5b3f848423d91bf07050", size = 87766, upload-time = "2025-08-12T05:52:39.243Z" }, + { url = "https://files.pythonhosted.org/packages/e8/cf/7d848740203c7b4b27eb55dbfede11aca974a51c3d894f6cc4b865f42f58/wrapt-1.17.3-cp313-cp313-win32.whl", hash = "sha256:53e5e39ff71b3fc484df8a522c933ea2b7cdd0d5d15ae82e5b23fde87d44cbd8", size = 36711, upload-time = "2025-08-12T05:53:10.074Z" }, + { url = "https://files.pythonhosted.org/packages/57/54/35a84d0a4d23ea675994104e667ceff49227ce473ba6a59ba2c84f250b74/wrapt-1.17.3-cp313-cp313-win_amd64.whl", hash = "sha256:1f0b2f40cf341ee8cc1a97d51ff50dddb9fcc73241b9143ec74b30fc4f44f6cb", size = 38885, upload-time = "2025-08-12T05:53:08.695Z" }, + { url = "https://files.pythonhosted.org/packages/01/77/66e54407c59d7b02a3c4e0af3783168fff8e5d61def52cda8728439d86bc/wrapt-1.17.3-cp313-cp313-win_arm64.whl", hash = "sha256:7425ac3c54430f5fc5e7b6f41d41e704db073309acfc09305816bc6a0b26bb16", size = 36896, upload-time = "2025-08-12T05:52:55.34Z" }, + { url = "https://files.pythonhosted.org/packages/02/a2/cd864b2a14f20d14f4c496fab97802001560f9f41554eef6df201cd7f76c/wrapt-1.17.3-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:cf30f6e3c077c8e6a9a7809c94551203c8843e74ba0c960f4a98cd80d4665d39", size = 54132, upload-time = "2025-08-12T05:51:49.864Z" }, + { url = "https://files.pythonhosted.org/packages/d5/46/d011725b0c89e853dc44cceb738a307cde5d240d023d6d40a82d1b4e1182/wrapt-1.17.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:e228514a06843cae89621384cfe3a80418f3c04aadf8a3b14e46a7be704e4235", size = 39091, upload-time = "2025-08-12T05:51:38.935Z" }, + { url = "https://files.pythonhosted.org/packages/2e/9e/3ad852d77c35aae7ddebdbc3b6d35ec8013af7d7dddad0ad911f3d891dae/wrapt-1.17.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:5ea5eb3c0c071862997d6f3e02af1d055f381b1d25b286b9d6644b79db77657c", size = 39172, upload-time = "2025-08-12T05:51:59.365Z" }, + { url = "https://files.pythonhosted.org/packages/c3/f7/c983d2762bcce2326c317c26a6a1e7016f7eb039c27cdf5c4e30f4160f31/wrapt-1.17.3-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:281262213373b6d5e4bb4353bc36d1ba4084e6d6b5d242863721ef2bf2c2930b", size = 87163, upload-time = "2025-08-12T05:52:40.965Z" }, + { url = "https://files.pythonhosted.org/packages/e4/0f/f673f75d489c7f22d17fe0193e84b41540d962f75fce579cf6873167c29b/wrapt-1.17.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dc4a8d2b25efb6681ecacad42fca8859f88092d8732b170de6a5dddd80a1c8fa", size = 87963, upload-time = "2025-08-12T05:52:20.326Z" }, + { url = "https://files.pythonhosted.org/packages/df/61/515ad6caca68995da2fac7a6af97faab8f78ebe3bf4f761e1b77efbc47b5/wrapt-1.17.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:373342dd05b1d07d752cecbec0c41817231f29f3a89aa8b8843f7b95992ed0c7", size = 86945, upload-time = "2025-08-12T05:52:21.581Z" }, + { url = "https://files.pythonhosted.org/packages/d3/bd/4e70162ce398462a467bc09e768bee112f1412e563620adc353de9055d33/wrapt-1.17.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d40770d7c0fd5cbed9d84b2c3f2e156431a12c9a37dc6284060fb4bec0b7ffd4", size = 86857, upload-time = "2025-08-12T05:52:43.043Z" }, + { url = "https://files.pythonhosted.org/packages/2b/b8/da8560695e9284810b8d3df8a19396a6e40e7518059584a1a394a2b35e0a/wrapt-1.17.3-cp314-cp314-win32.whl", hash = "sha256:fbd3c8319de8e1dc79d346929cd71d523622da527cca14e0c1d257e31c2b8b10", size = 37178, upload-time = "2025-08-12T05:53:12.605Z" }, + { url = "https://files.pythonhosted.org/packages/db/c8/b71eeb192c440d67a5a0449aaee2310a1a1e8eca41676046f99ed2487e9f/wrapt-1.17.3-cp314-cp314-win_amd64.whl", hash = "sha256:e1a4120ae5705f673727d3253de3ed0e016f7cd78dc463db1b31e2463e1f3cf6", size = 39310, upload-time = "2025-08-12T05:53:11.106Z" }, + { url = "https://files.pythonhosted.org/packages/45/20/2cda20fd4865fa40f86f6c46ed37a2a8356a7a2fde0773269311f2af56c7/wrapt-1.17.3-cp314-cp314-win_arm64.whl", hash = "sha256:507553480670cab08a800b9463bdb881b2edeed77dc677b0a5915e6106e91a58", size = 37266, upload-time = "2025-08-12T05:52:56.531Z" }, + { url = "https://files.pythonhosted.org/packages/77/ed/dd5cf21aec36c80443c6f900449260b80e2a65cf963668eaef3b9accce36/wrapt-1.17.3-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:ed7c635ae45cfbc1a7371f708727bf74690daedc49b4dba310590ca0bd28aa8a", size = 56544, upload-time = "2025-08-12T05:51:51.109Z" }, + { url = "https://files.pythonhosted.org/packages/8d/96/450c651cc753877ad100c7949ab4d2e2ecc4d97157e00fa8f45df682456a/wrapt-1.17.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:249f88ed15503f6492a71f01442abddd73856a0032ae860de6d75ca62eed8067", size = 40283, upload-time = "2025-08-12T05:51:39.912Z" }, + { url = "https://files.pythonhosted.org/packages/d1/86/2fcad95994d9b572db57632acb6f900695a648c3e063f2cd344b3f5c5a37/wrapt-1.17.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5a03a38adec8066d5a37bea22f2ba6bbf39fcdefbe2d91419ab864c3fb515454", size = 40366, upload-time = "2025-08-12T05:52:00.693Z" }, + { url = "https://files.pythonhosted.org/packages/64/0e/f4472f2fdde2d4617975144311f8800ef73677a159be7fe61fa50997d6c0/wrapt-1.17.3-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:5d4478d72eb61c36e5b446e375bbc49ed002430d17cdec3cecb36993398e1a9e", size = 108571, upload-time = "2025-08-12T05:52:44.521Z" }, + { url = "https://files.pythonhosted.org/packages/cc/01/9b85a99996b0a97c8a17484684f206cbb6ba73c1ce6890ac668bcf3838fb/wrapt-1.17.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:223db574bb38637e8230eb14b185565023ab624474df94d2af18f1cdb625216f", size = 113094, upload-time = "2025-08-12T05:52:22.618Z" }, + { url = "https://files.pythonhosted.org/packages/25/02/78926c1efddcc7b3aa0bc3d6b33a822f7d898059f7cd9ace8c8318e559ef/wrapt-1.17.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e405adefb53a435f01efa7ccdec012c016b5a1d3f35459990afc39b6be4d5056", size = 110659, upload-time = "2025-08-12T05:52:24.057Z" }, + { url = "https://files.pythonhosted.org/packages/dc/ee/c414501ad518ac3e6fe184753632fe5e5ecacdcf0effc23f31c1e4f7bfcf/wrapt-1.17.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:88547535b787a6c9ce4086917b6e1d291aa8ed914fdd3a838b3539dc95c12804", size = 106946, upload-time = "2025-08-12T05:52:45.976Z" }, + { url = "https://files.pythonhosted.org/packages/be/44/a1bd64b723d13bb151d6cc91b986146a1952385e0392a78567e12149c7b4/wrapt-1.17.3-cp314-cp314t-win32.whl", hash = "sha256:41b1d2bc74c2cac6f9074df52b2efbef2b30bdfe5f40cb78f8ca22963bc62977", size = 38717, upload-time = "2025-08-12T05:53:15.214Z" }, + { url = "https://files.pythonhosted.org/packages/79/d9/7cfd5a312760ac4dd8bf0184a6ee9e43c33e47f3dadc303032ce012b8fa3/wrapt-1.17.3-cp314-cp314t-win_amd64.whl", hash = "sha256:73d496de46cd2cdbdbcce4ae4bcdb4afb6a11234a1df9c085249d55166b95116", size = 41334, upload-time = "2025-08-12T05:53:14.178Z" }, + { url = "https://files.pythonhosted.org/packages/46/78/10ad9781128ed2f99dbc474f43283b13fea8ba58723e98844367531c18e9/wrapt-1.17.3-cp314-cp314t-win_arm64.whl", hash = "sha256:f38e60678850c42461d4202739f9bf1e3a737c7ad283638251e79cc49effb6b6", size = 38471, upload-time = "2025-08-12T05:52:57.784Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f6/a933bd70f98e9cf3e08167fc5cd7aaaca49147e48411c0bd5ae701bb2194/wrapt-1.17.3-py3-none-any.whl", hash = "sha256:7171ae35d2c33d326ac19dd8facb1e82e5fd04ef8c6c0e394d7af55a55051c22", size = 23591, upload-time = "2025-08-12T05:53:20.674Z" }, ] [[package]] @@ -3136,80 +3113,80 @@ dependencies = [ { name = "multidict" }, { name = "propcache" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/57/63/0c6ebca57330cd313f6102b16dd57ffaf3ec4c83403dcb45dbd15c6f3ea1/yarl-1.22.0.tar.gz", hash = "sha256:bebf8557577d4401ba8bd9ff33906f1376c877aa78d1fe216ad01b4d6745af71", size = 187169 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ea/f3/d67de7260456ee105dc1d162d43a019ecad6b91e2f51809d6cddaa56690e/yarl-1.22.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8dee9c25c74997f6a750cd317b8ca63545169c098faee42c84aa5e506c819b53", size = 139980 }, - { url = "https://files.pythonhosted.org/packages/01/88/04d98af0b47e0ef42597b9b28863b9060bb515524da0a65d5f4db160b2d5/yarl-1.22.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:01e73b85a5434f89fc4fe27dcda2aff08ddf35e4d47bbbea3bdcd25321af538a", size = 93424 }, - { url = "https://files.pythonhosted.org/packages/18/91/3274b215fd8442a03975ce6bee5fe6aa57a8326b29b9d3d56234a1dca244/yarl-1.22.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:22965c2af250d20c873cdbee8ff958fb809940aeb2e74ba5f20aaf6b7ac8c70c", size = 93821 }, - { url = "https://files.pythonhosted.org/packages/61/3a/caf4e25036db0f2da4ca22a353dfeb3c9d3c95d2761ebe9b14df8fc16eb0/yarl-1.22.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b4f15793aa49793ec8d1c708ab7f9eded1aa72edc5174cae703651555ed1b601", size = 373243 }, - { url = "https://files.pythonhosted.org/packages/6e/9e/51a77ac7516e8e7803b06e01f74e78649c24ee1021eca3d6a739cb6ea49c/yarl-1.22.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e5542339dcf2747135c5c85f68680353d5cb9ffd741c0f2e8d832d054d41f35a", size = 342361 }, - { url = "https://files.pythonhosted.org/packages/d4/f8/33b92454789dde8407f156c00303e9a891f1f51a0330b0fad7c909f87692/yarl-1.22.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5c401e05ad47a75869c3ab3e35137f8468b846770587e70d71e11de797d113df", size = 387036 }, - { url = "https://files.pythonhosted.org/packages/d9/9a/c5db84ea024f76838220280f732970aa4ee154015d7f5c1bfb60a267af6f/yarl-1.22.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:243dda95d901c733f5b59214d28b0120893d91777cb8aa043e6ef059d3cddfe2", size = 397671 }, - { url = "https://files.pythonhosted.org/packages/11/c9/cd8538dc2e7727095e0c1d867bad1e40c98f37763e6d995c1939f5fdc7b1/yarl-1.22.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bec03d0d388060058f5d291a813f21c011041938a441c593374da6077fe21b1b", size = 377059 }, - { url = "https://files.pythonhosted.org/packages/a1/b9/ab437b261702ced75122ed78a876a6dec0a1b0f5e17a4ac7a9a2482d8abe/yarl-1.22.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b0748275abb8c1e1e09301ee3cf90c8a99678a4e92e4373705f2a2570d581273", size = 365356 }, - { url = "https://files.pythonhosted.org/packages/b2/9d/8e1ae6d1d008a9567877b08f0ce4077a29974c04c062dabdb923ed98e6fe/yarl-1.22.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:47fdb18187e2a4e18fda2c25c05d8251a9e4a521edaed757fef033e7d8498d9a", size = 361331 }, - { url = "https://files.pythonhosted.org/packages/ca/5a/09b7be3905962f145b73beb468cdd53db8aa171cf18c80400a54c5b82846/yarl-1.22.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:c7044802eec4524fde550afc28edda0dd5784c4c45f0be151a2d3ba017daca7d", size = 382590 }, - { url = "https://files.pythonhosted.org/packages/aa/7f/59ec509abf90eda5048b0bc3e2d7b5099dffdb3e6b127019895ab9d5ef44/yarl-1.22.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:139718f35149ff544caba20fce6e8a2f71f1e39b92c700d8438a0b1d2a631a02", size = 385316 }, - { url = "https://files.pythonhosted.org/packages/e5/84/891158426bc8036bfdfd862fabd0e0fa25df4176ec793e447f4b85cf1be4/yarl-1.22.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e1b51bebd221006d3d2f95fbe124b22b247136647ae5dcc8c7acafba66e5ee67", size = 374431 }, - { url = "https://files.pythonhosted.org/packages/bb/49/03da1580665baa8bef5e8ed34c6df2c2aca0a2f28bf397ed238cc1bbc6f2/yarl-1.22.0-cp313-cp313-win32.whl", hash = "sha256:d3e32536234a95f513bd374e93d717cf6b2231a791758de6c509e3653f234c95", size = 81555 }, - { url = "https://files.pythonhosted.org/packages/9a/ee/450914ae11b419eadd067c6183ae08381cfdfcb9798b90b2b713bbebddda/yarl-1.22.0-cp313-cp313-win_amd64.whl", hash = "sha256:47743b82b76d89a1d20b83e60d5c20314cbd5ba2befc9cda8f28300c4a08ed4d", size = 86965 }, - { url = "https://files.pythonhosted.org/packages/98/4d/264a01eae03b6cf629ad69bae94e3b0e5344741e929073678e84bf7a3e3b/yarl-1.22.0-cp313-cp313-win_arm64.whl", hash = "sha256:5d0fcda9608875f7d052eff120c7a5da474a6796fe4d83e152e0e4d42f6d1a9b", size = 81205 }, - { url = "https://files.pythonhosted.org/packages/88/fc/6908f062a2f77b5f9f6d69cecb1747260831ff206adcbc5b510aff88df91/yarl-1.22.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:719ae08b6972befcba4310e49edb1161a88cdd331e3a694b84466bd938a6ab10", size = 146209 }, - { url = "https://files.pythonhosted.org/packages/65/47/76594ae8eab26210b4867be6f49129861ad33da1f1ebdf7051e98492bf62/yarl-1.22.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:47d8a5c446df1c4db9d21b49619ffdba90e77c89ec6e283f453856c74b50b9e3", size = 95966 }, - { url = "https://files.pythonhosted.org/packages/ab/ce/05e9828a49271ba6b5b038b15b3934e996980dd78abdfeb52a04cfb9467e/yarl-1.22.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:cfebc0ac8333520d2d0423cbbe43ae43c8838862ddb898f5ca68565e395516e9", size = 97312 }, - { url = "https://files.pythonhosted.org/packages/d1/c5/7dffad5e4f2265b29c9d7ec869c369e4223166e4f9206fc2243ee9eea727/yarl-1.22.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4398557cbf484207df000309235979c79c4356518fd5c99158c7d38203c4da4f", size = 361967 }, - { url = "https://files.pythonhosted.org/packages/50/b2/375b933c93a54bff7fc041e1a6ad2c0f6f733ffb0c6e642ce56ee3b39970/yarl-1.22.0-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2ca6fd72a8cd803be290d42f2dec5cdcd5299eeb93c2d929bf060ad9efaf5de0", size = 323949 }, - { url = "https://files.pythonhosted.org/packages/66/50/bfc2a29a1d78644c5a7220ce2f304f38248dc94124a326794e677634b6cf/yarl-1.22.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ca1f59c4e1ab6e72f0a23c13fca5430f889634166be85dbf1013683e49e3278e", size = 361818 }, - { url = "https://files.pythonhosted.org/packages/46/96/f3941a46af7d5d0f0498f86d71275696800ddcdd20426298e572b19b91ff/yarl-1.22.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6c5010a52015e7c70f86eb967db0f37f3c8bd503a695a49f8d45700144667708", size = 372626 }, - { url = "https://files.pythonhosted.org/packages/c1/42/8b27c83bb875cd89448e42cd627e0fb971fa1675c9ec546393d18826cb50/yarl-1.22.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d7672ecf7557476642c88497c2f8d8542f8e36596e928e9bcba0e42e1e7d71f", size = 341129 }, - { url = "https://files.pythonhosted.org/packages/49/36/99ca3122201b382a3cf7cc937b95235b0ac944f7e9f2d5331d50821ed352/yarl-1.22.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:3b7c88eeef021579d600e50363e0b6ee4f7f6f728cd3486b9d0f3ee7b946398d", size = 346776 }, - { url = "https://files.pythonhosted.org/packages/85/b4/47328bf996acd01a4c16ef9dcd2f59c969f495073616586f78cd5f2efb99/yarl-1.22.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:f4afb5c34f2c6fecdcc182dfcfc6af6cccf1aa923eed4d6a12e9d96904e1a0d8", size = 334879 }, - { url = "https://files.pythonhosted.org/packages/c2/ad/b77d7b3f14a4283bffb8e92c6026496f6de49751c2f97d4352242bba3990/yarl-1.22.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:59c189e3e99a59cf8d83cbb31d4db02d66cda5a1a4374e8a012b51255341abf5", size = 350996 }, - { url = "https://files.pythonhosted.org/packages/81/c8/06e1d69295792ba54d556f06686cbd6a7ce39c22307100e3fb4a2c0b0a1d/yarl-1.22.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:5a3bf7f62a289fa90f1990422dc8dff5a458469ea71d1624585ec3a4c8d6960f", size = 356047 }, - { url = "https://files.pythonhosted.org/packages/4b/b8/4c0e9e9f597074b208d18cef227d83aac36184bfbc6eab204ea55783dbc5/yarl-1.22.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:de6b9a04c606978fdfe72666fa216ffcf2d1a9f6a381058d4378f8d7b1e5de62", size = 342947 }, - { url = "https://files.pythonhosted.org/packages/e0/e5/11f140a58bf4c6ad7aca69a892bff0ee638c31bea4206748fc0df4ebcb3a/yarl-1.22.0-cp313-cp313t-win32.whl", hash = "sha256:1834bb90991cc2999f10f97f5f01317f99b143284766d197e43cd5b45eb18d03", size = 86943 }, - { url = "https://files.pythonhosted.org/packages/31/74/8b74bae38ed7fe6793d0c15a0c8207bbb819cf287788459e5ed230996cdd/yarl-1.22.0-cp313-cp313t-win_amd64.whl", hash = "sha256:ff86011bd159a9d2dfc89c34cfd8aff12875980e3bd6a39ff097887520e60249", size = 93715 }, - { url = "https://files.pythonhosted.org/packages/69/66/991858aa4b5892d57aef7ee1ba6b4d01ec3b7eb3060795d34090a3ca3278/yarl-1.22.0-cp313-cp313t-win_arm64.whl", hash = "sha256:7861058d0582b847bc4e3a4a4c46828a410bca738673f35a29ba3ca5db0b473b", size = 83857 }, - { url = "https://files.pythonhosted.org/packages/46/b3/e20ef504049f1a1c54a814b4b9bed96d1ac0e0610c3b4da178f87209db05/yarl-1.22.0-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:34b36c2c57124530884d89d50ed2c1478697ad7473efd59cfd479945c95650e4", size = 140520 }, - { url = "https://files.pythonhosted.org/packages/e4/04/3532d990fdbab02e5ede063676b5c4260e7f3abea2151099c2aa745acc4c/yarl-1.22.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:0dd9a702591ca2e543631c2a017e4a547e38a5c0f29eece37d9097e04a7ac683", size = 93504 }, - { url = "https://files.pythonhosted.org/packages/11/63/ff458113c5c2dac9a9719ac68ee7c947cb621432bcf28c9972b1c0e83938/yarl-1.22.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:594fcab1032e2d2cc3321bb2e51271e7cd2b516c7d9aee780ece81b07ff8244b", size = 94282 }, - { url = "https://files.pythonhosted.org/packages/a7/bc/315a56aca762d44a6aaaf7ad253f04d996cb6b27bad34410f82d76ea8038/yarl-1.22.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f3d7a87a78d46a2e3d5b72587ac14b4c16952dd0887dbb051451eceac774411e", size = 372080 }, - { url = "https://files.pythonhosted.org/packages/3f/3f/08e9b826ec2e099ea6e7c69a61272f4f6da62cb5b1b63590bb80ca2e4a40/yarl-1.22.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:852863707010316c973162e703bddabec35e8757e67fcb8ad58829de1ebc8590", size = 338696 }, - { url = "https://files.pythonhosted.org/packages/e3/9f/90360108e3b32bd76789088e99538febfea24a102380ae73827f62073543/yarl-1.22.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:131a085a53bfe839a477c0845acf21efc77457ba2bcf5899618136d64f3303a2", size = 387121 }, - { url = "https://files.pythonhosted.org/packages/98/92/ab8d4657bd5b46a38094cfaea498f18bb70ce6b63508fd7e909bd1f93066/yarl-1.22.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:078a8aefd263f4d4f923a9677b942b445a2be970ca24548a8102689a3a8ab8da", size = 394080 }, - { url = "https://files.pythonhosted.org/packages/f5/e7/d8c5a7752fef68205296201f8ec2bf718f5c805a7a7e9880576c67600658/yarl-1.22.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bca03b91c323036913993ff5c738d0842fc9c60c4648e5c8d98331526df89784", size = 372661 }, - { url = "https://files.pythonhosted.org/packages/b6/2e/f4d26183c8db0bb82d491b072f3127fb8c381a6206a3a56332714b79b751/yarl-1.22.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:68986a61557d37bb90d3051a45b91fa3d5c516d177dfc6dd6f2f436a07ff2b6b", size = 364645 }, - { url = "https://files.pythonhosted.org/packages/80/7c/428e5812e6b87cd00ee8e898328a62c95825bf37c7fa87f0b6bb2ad31304/yarl-1.22.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:4792b262d585ff0dff6bcb787f8492e40698443ec982a3568c2096433660c694", size = 355361 }, - { url = "https://files.pythonhosted.org/packages/ec/2a/249405fd26776f8b13c067378ef4d7dd49c9098d1b6457cdd152a99e96a9/yarl-1.22.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:ebd4549b108d732dba1d4ace67614b9545b21ece30937a63a65dd34efa19732d", size = 381451 }, - { url = "https://files.pythonhosted.org/packages/67/a8/fb6b1adbe98cf1e2dd9fad71003d3a63a1bc22459c6e15f5714eb9323b93/yarl-1.22.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:f87ac53513d22240c7d59203f25cc3beac1e574c6cd681bbfd321987b69f95fd", size = 383814 }, - { url = "https://files.pythonhosted.org/packages/d9/f9/3aa2c0e480fb73e872ae2814c43bc1e734740bb0d54e8cb2a95925f98131/yarl-1.22.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:22b029f2881599e2f1b06f8f1db2ee63bd309e2293ba2d566e008ba12778b8da", size = 370799 }, - { url = "https://files.pythonhosted.org/packages/50/3c/af9dba3b8b5eeb302f36f16f92791f3ea62e3f47763406abf6d5a4a3333b/yarl-1.22.0-cp314-cp314-win32.whl", hash = "sha256:6a635ea45ba4ea8238463b4f7d0e721bad669f80878b7bfd1f89266e2ae63da2", size = 82990 }, - { url = "https://files.pythonhosted.org/packages/ac/30/ac3a0c5bdc1d6efd1b41fa24d4897a4329b3b1e98de9449679dd327af4f0/yarl-1.22.0-cp314-cp314-win_amd64.whl", hash = "sha256:0d6e6885777af0f110b0e5d7e5dda8b704efed3894da26220b7f3d887b839a79", size = 88292 }, - { url = "https://files.pythonhosted.org/packages/df/0a/227ab4ff5b998a1b7410abc7b46c9b7a26b0ca9e86c34ba4b8d8bc7c63d5/yarl-1.22.0-cp314-cp314-win_arm64.whl", hash = "sha256:8218f4e98d3c10d683584cb40f0424f4b9fd6e95610232dd75e13743b070ee33", size = 82888 }, - { url = "https://files.pythonhosted.org/packages/06/5e/a15eb13db90abd87dfbefb9760c0f3f257ac42a5cac7e75dbc23bed97a9f/yarl-1.22.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:45c2842ff0e0d1b35a6bf1cd6c690939dacb617a70827f715232b2e0494d55d1", size = 146223 }, - { url = "https://files.pythonhosted.org/packages/18/82/9665c61910d4d84f41a5bf6837597c89e665fa88aa4941080704645932a9/yarl-1.22.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:d947071e6ebcf2e2bee8fce76e10faca8f7a14808ca36a910263acaacef08eca", size = 95981 }, - { url = "https://files.pythonhosted.org/packages/5d/9a/2f65743589809af4d0a6d3aa749343c4b5f4c380cc24a8e94a3c6625a808/yarl-1.22.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:334b8721303e61b00019474cc103bdac3d7b1f65e91f0bfedeec2d56dfe74b53", size = 97303 }, - { url = "https://files.pythonhosted.org/packages/b0/ab/5b13d3e157505c43c3b43b5a776cbf7b24a02bc4cccc40314771197e3508/yarl-1.22.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1e7ce67c34138a058fd092f67d07a72b8e31ff0c9236e751957465a24b28910c", size = 361820 }, - { url = "https://files.pythonhosted.org/packages/fb/76/242a5ef4677615cf95330cfc1b4610e78184400699bdda0acb897ef5e49a/yarl-1.22.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d77e1b2c6d04711478cb1c4ab90db07f1609ccf06a287d5607fcd90dc9863acf", size = 323203 }, - { url = "https://files.pythonhosted.org/packages/8c/96/475509110d3f0153b43d06164cf4195c64d16999e0c7e2d8a099adcd6907/yarl-1.22.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c4647674b6150d2cae088fc07de2738a84b8bcedebef29802cf0b0a82ab6face", size = 363173 }, - { url = "https://files.pythonhosted.org/packages/c9/66/59db471aecfbd559a1fd48aedd954435558cd98c7d0da8b03cc6c140a32c/yarl-1.22.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:efb07073be061c8f79d03d04139a80ba33cbd390ca8f0297aae9cce6411e4c6b", size = 373562 }, - { url = "https://files.pythonhosted.org/packages/03/1f/c5d94abc91557384719da10ff166b916107c1b45e4d0423a88457071dd88/yarl-1.22.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e51ac5435758ba97ad69617e13233da53908beccc6cfcd6c34bbed8dcbede486", size = 339828 }, - { url = "https://files.pythonhosted.org/packages/5f/97/aa6a143d3afba17b6465733681c70cf175af89f76ec8d9286e08437a7454/yarl-1.22.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:33e32a0dd0c8205efa8e83d04fc9f19313772b78522d1bdc7d9aed706bfd6138", size = 347551 }, - { url = "https://files.pythonhosted.org/packages/43/3c/45a2b6d80195959239a7b2a8810506d4eea5487dce61c2a3393e7fc3c52e/yarl-1.22.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:bf4a21e58b9cde0e401e683ebd00f6ed30a06d14e93f7c8fd059f8b6e8f87b6a", size = 334512 }, - { url = "https://files.pythonhosted.org/packages/86/a0/c2ab48d74599c7c84cb104ebd799c5813de252bea0f360ffc29d270c2caa/yarl-1.22.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:e4b582bab49ac33c8deb97e058cd67c2c50dac0dd134874106d9c774fd272529", size = 352400 }, - { url = "https://files.pythonhosted.org/packages/32/75/f8919b2eafc929567d3d8411f72bdb1a2109c01caaab4ebfa5f8ffadc15b/yarl-1.22.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:0b5bcc1a9c4839e7e30b7b30dd47fe5e7e44fb7054ec29b5bb8d526aa1041093", size = 357140 }, - { url = "https://files.pythonhosted.org/packages/cf/72/6a85bba382f22cf78add705d8c3731748397d986e197e53ecc7835e76de7/yarl-1.22.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:c0232bce2170103ec23c454e54a57008a9a72b5d1c3105dc2496750da8cfa47c", size = 341473 }, - { url = "https://files.pythonhosted.org/packages/35/18/55e6011f7c044dc80b98893060773cefcfdbf60dfefb8cb2f58b9bacbd83/yarl-1.22.0-cp314-cp314t-win32.whl", hash = "sha256:8009b3173bcd637be650922ac455946197d858b3630b6d8787aa9e5c4564533e", size = 89056 }, - { url = "https://files.pythonhosted.org/packages/f9/86/0f0dccb6e59a9e7f122c5afd43568b1d31b8ab7dda5f1b01fb5c7025c9a9/yarl-1.22.0-cp314-cp314t-win_amd64.whl", hash = "sha256:9fb17ea16e972c63d25d4a97f016d235c78dd2344820eb35bc034bc32012ee27", size = 96292 }, - { url = "https://files.pythonhosted.org/packages/48/b7/503c98092fb3b344a179579f55814b613c1fbb1c23b3ec14a7b008a66a6e/yarl-1.22.0-cp314-cp314t-win_arm64.whl", hash = "sha256:9f6d73c1436b934e3f01df1e1b21ff765cd1d28c77dfb9ace207f746d4610ee1", size = 85171 }, - { url = "https://files.pythonhosted.org/packages/73/ae/b48f95715333080afb75a4504487cbe142cae1268afc482d06692d605ae6/yarl-1.22.0-py3-none-any.whl", hash = "sha256:1380560bdba02b6b6c90de54133c81c9f2a453dee9912fe58c1dcced1edb7cff", size = 46814 }, +sdist = { url = "https://files.pythonhosted.org/packages/57/63/0c6ebca57330cd313f6102b16dd57ffaf3ec4c83403dcb45dbd15c6f3ea1/yarl-1.22.0.tar.gz", hash = "sha256:bebf8557577d4401ba8bd9ff33906f1376c877aa78d1fe216ad01b4d6745af71", size = 187169, upload-time = "2025-10-06T14:12:55.963Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ea/f3/d67de7260456ee105dc1d162d43a019ecad6b91e2f51809d6cddaa56690e/yarl-1.22.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8dee9c25c74997f6a750cd317b8ca63545169c098faee42c84aa5e506c819b53", size = 139980, upload-time = "2025-10-06T14:10:14.601Z" }, + { url = "https://files.pythonhosted.org/packages/01/88/04d98af0b47e0ef42597b9b28863b9060bb515524da0a65d5f4db160b2d5/yarl-1.22.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:01e73b85a5434f89fc4fe27dcda2aff08ddf35e4d47bbbea3bdcd25321af538a", size = 93424, upload-time = "2025-10-06T14:10:16.115Z" }, + { url = "https://files.pythonhosted.org/packages/18/91/3274b215fd8442a03975ce6bee5fe6aa57a8326b29b9d3d56234a1dca244/yarl-1.22.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:22965c2af250d20c873cdbee8ff958fb809940aeb2e74ba5f20aaf6b7ac8c70c", size = 93821, upload-time = "2025-10-06T14:10:17.993Z" }, + { url = "https://files.pythonhosted.org/packages/61/3a/caf4e25036db0f2da4ca22a353dfeb3c9d3c95d2761ebe9b14df8fc16eb0/yarl-1.22.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b4f15793aa49793ec8d1c708ab7f9eded1aa72edc5174cae703651555ed1b601", size = 373243, upload-time = "2025-10-06T14:10:19.44Z" }, + { url = "https://files.pythonhosted.org/packages/6e/9e/51a77ac7516e8e7803b06e01f74e78649c24ee1021eca3d6a739cb6ea49c/yarl-1.22.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e5542339dcf2747135c5c85f68680353d5cb9ffd741c0f2e8d832d054d41f35a", size = 342361, upload-time = "2025-10-06T14:10:21.124Z" }, + { url = "https://files.pythonhosted.org/packages/d4/f8/33b92454789dde8407f156c00303e9a891f1f51a0330b0fad7c909f87692/yarl-1.22.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5c401e05ad47a75869c3ab3e35137f8468b846770587e70d71e11de797d113df", size = 387036, upload-time = "2025-10-06T14:10:22.902Z" }, + { url = "https://files.pythonhosted.org/packages/d9/9a/c5db84ea024f76838220280f732970aa4ee154015d7f5c1bfb60a267af6f/yarl-1.22.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:243dda95d901c733f5b59214d28b0120893d91777cb8aa043e6ef059d3cddfe2", size = 397671, upload-time = "2025-10-06T14:10:24.523Z" }, + { url = "https://files.pythonhosted.org/packages/11/c9/cd8538dc2e7727095e0c1d867bad1e40c98f37763e6d995c1939f5fdc7b1/yarl-1.22.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bec03d0d388060058f5d291a813f21c011041938a441c593374da6077fe21b1b", size = 377059, upload-time = "2025-10-06T14:10:26.406Z" }, + { url = "https://files.pythonhosted.org/packages/a1/b9/ab437b261702ced75122ed78a876a6dec0a1b0f5e17a4ac7a9a2482d8abe/yarl-1.22.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b0748275abb8c1e1e09301ee3cf90c8a99678a4e92e4373705f2a2570d581273", size = 365356, upload-time = "2025-10-06T14:10:28.461Z" }, + { url = "https://files.pythonhosted.org/packages/b2/9d/8e1ae6d1d008a9567877b08f0ce4077a29974c04c062dabdb923ed98e6fe/yarl-1.22.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:47fdb18187e2a4e18fda2c25c05d8251a9e4a521edaed757fef033e7d8498d9a", size = 361331, upload-time = "2025-10-06T14:10:30.541Z" }, + { url = "https://files.pythonhosted.org/packages/ca/5a/09b7be3905962f145b73beb468cdd53db8aa171cf18c80400a54c5b82846/yarl-1.22.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:c7044802eec4524fde550afc28edda0dd5784c4c45f0be151a2d3ba017daca7d", size = 382590, upload-time = "2025-10-06T14:10:33.352Z" }, + { url = "https://files.pythonhosted.org/packages/aa/7f/59ec509abf90eda5048b0bc3e2d7b5099dffdb3e6b127019895ab9d5ef44/yarl-1.22.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:139718f35149ff544caba20fce6e8a2f71f1e39b92c700d8438a0b1d2a631a02", size = 385316, upload-time = "2025-10-06T14:10:35.034Z" }, + { url = "https://files.pythonhosted.org/packages/e5/84/891158426bc8036bfdfd862fabd0e0fa25df4176ec793e447f4b85cf1be4/yarl-1.22.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e1b51bebd221006d3d2f95fbe124b22b247136647ae5dcc8c7acafba66e5ee67", size = 374431, upload-time = "2025-10-06T14:10:37.76Z" }, + { url = "https://files.pythonhosted.org/packages/bb/49/03da1580665baa8bef5e8ed34c6df2c2aca0a2f28bf397ed238cc1bbc6f2/yarl-1.22.0-cp313-cp313-win32.whl", hash = "sha256:d3e32536234a95f513bd374e93d717cf6b2231a791758de6c509e3653f234c95", size = 81555, upload-time = "2025-10-06T14:10:39.649Z" }, + { url = "https://files.pythonhosted.org/packages/9a/ee/450914ae11b419eadd067c6183ae08381cfdfcb9798b90b2b713bbebddda/yarl-1.22.0-cp313-cp313-win_amd64.whl", hash = "sha256:47743b82b76d89a1d20b83e60d5c20314cbd5ba2befc9cda8f28300c4a08ed4d", size = 86965, upload-time = "2025-10-06T14:10:41.313Z" }, + { url = "https://files.pythonhosted.org/packages/98/4d/264a01eae03b6cf629ad69bae94e3b0e5344741e929073678e84bf7a3e3b/yarl-1.22.0-cp313-cp313-win_arm64.whl", hash = "sha256:5d0fcda9608875f7d052eff120c7a5da474a6796fe4d83e152e0e4d42f6d1a9b", size = 81205, upload-time = "2025-10-06T14:10:43.167Z" }, + { url = "https://files.pythonhosted.org/packages/88/fc/6908f062a2f77b5f9f6d69cecb1747260831ff206adcbc5b510aff88df91/yarl-1.22.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:719ae08b6972befcba4310e49edb1161a88cdd331e3a694b84466bd938a6ab10", size = 146209, upload-time = "2025-10-06T14:10:44.643Z" }, + { url = "https://files.pythonhosted.org/packages/65/47/76594ae8eab26210b4867be6f49129861ad33da1f1ebdf7051e98492bf62/yarl-1.22.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:47d8a5c446df1c4db9d21b49619ffdba90e77c89ec6e283f453856c74b50b9e3", size = 95966, upload-time = "2025-10-06T14:10:46.554Z" }, + { url = "https://files.pythonhosted.org/packages/ab/ce/05e9828a49271ba6b5b038b15b3934e996980dd78abdfeb52a04cfb9467e/yarl-1.22.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:cfebc0ac8333520d2d0423cbbe43ae43c8838862ddb898f5ca68565e395516e9", size = 97312, upload-time = "2025-10-06T14:10:48.007Z" }, + { url = "https://files.pythonhosted.org/packages/d1/c5/7dffad5e4f2265b29c9d7ec869c369e4223166e4f9206fc2243ee9eea727/yarl-1.22.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4398557cbf484207df000309235979c79c4356518fd5c99158c7d38203c4da4f", size = 361967, upload-time = "2025-10-06T14:10:49.997Z" }, + { url = "https://files.pythonhosted.org/packages/50/b2/375b933c93a54bff7fc041e1a6ad2c0f6f733ffb0c6e642ce56ee3b39970/yarl-1.22.0-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2ca6fd72a8cd803be290d42f2dec5cdcd5299eeb93c2d929bf060ad9efaf5de0", size = 323949, upload-time = "2025-10-06T14:10:52.004Z" }, + { url = "https://files.pythonhosted.org/packages/66/50/bfc2a29a1d78644c5a7220ce2f304f38248dc94124a326794e677634b6cf/yarl-1.22.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ca1f59c4e1ab6e72f0a23c13fca5430f889634166be85dbf1013683e49e3278e", size = 361818, upload-time = "2025-10-06T14:10:54.078Z" }, + { url = "https://files.pythonhosted.org/packages/46/96/f3941a46af7d5d0f0498f86d71275696800ddcdd20426298e572b19b91ff/yarl-1.22.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6c5010a52015e7c70f86eb967db0f37f3c8bd503a695a49f8d45700144667708", size = 372626, upload-time = "2025-10-06T14:10:55.767Z" }, + { url = "https://files.pythonhosted.org/packages/c1/42/8b27c83bb875cd89448e42cd627e0fb971fa1675c9ec546393d18826cb50/yarl-1.22.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d7672ecf7557476642c88497c2f8d8542f8e36596e928e9bcba0e42e1e7d71f", size = 341129, upload-time = "2025-10-06T14:10:57.985Z" }, + { url = "https://files.pythonhosted.org/packages/49/36/99ca3122201b382a3cf7cc937b95235b0ac944f7e9f2d5331d50821ed352/yarl-1.22.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:3b7c88eeef021579d600e50363e0b6ee4f7f6f728cd3486b9d0f3ee7b946398d", size = 346776, upload-time = "2025-10-06T14:10:59.633Z" }, + { url = "https://files.pythonhosted.org/packages/85/b4/47328bf996acd01a4c16ef9dcd2f59c969f495073616586f78cd5f2efb99/yarl-1.22.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:f4afb5c34f2c6fecdcc182dfcfc6af6cccf1aa923eed4d6a12e9d96904e1a0d8", size = 334879, upload-time = "2025-10-06T14:11:01.454Z" }, + { url = "https://files.pythonhosted.org/packages/c2/ad/b77d7b3f14a4283bffb8e92c6026496f6de49751c2f97d4352242bba3990/yarl-1.22.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:59c189e3e99a59cf8d83cbb31d4db02d66cda5a1a4374e8a012b51255341abf5", size = 350996, upload-time = "2025-10-06T14:11:03.452Z" }, + { url = "https://files.pythonhosted.org/packages/81/c8/06e1d69295792ba54d556f06686cbd6a7ce39c22307100e3fb4a2c0b0a1d/yarl-1.22.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:5a3bf7f62a289fa90f1990422dc8dff5a458469ea71d1624585ec3a4c8d6960f", size = 356047, upload-time = "2025-10-06T14:11:05.115Z" }, + { url = "https://files.pythonhosted.org/packages/4b/b8/4c0e9e9f597074b208d18cef227d83aac36184bfbc6eab204ea55783dbc5/yarl-1.22.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:de6b9a04c606978fdfe72666fa216ffcf2d1a9f6a381058d4378f8d7b1e5de62", size = 342947, upload-time = "2025-10-06T14:11:08.137Z" }, + { url = "https://files.pythonhosted.org/packages/e0/e5/11f140a58bf4c6ad7aca69a892bff0ee638c31bea4206748fc0df4ebcb3a/yarl-1.22.0-cp313-cp313t-win32.whl", hash = "sha256:1834bb90991cc2999f10f97f5f01317f99b143284766d197e43cd5b45eb18d03", size = 86943, upload-time = "2025-10-06T14:11:10.284Z" }, + { url = "https://files.pythonhosted.org/packages/31/74/8b74bae38ed7fe6793d0c15a0c8207bbb819cf287788459e5ed230996cdd/yarl-1.22.0-cp313-cp313t-win_amd64.whl", hash = "sha256:ff86011bd159a9d2dfc89c34cfd8aff12875980e3bd6a39ff097887520e60249", size = 93715, upload-time = "2025-10-06T14:11:11.739Z" }, + { url = "https://files.pythonhosted.org/packages/69/66/991858aa4b5892d57aef7ee1ba6b4d01ec3b7eb3060795d34090a3ca3278/yarl-1.22.0-cp313-cp313t-win_arm64.whl", hash = "sha256:7861058d0582b847bc4e3a4a4c46828a410bca738673f35a29ba3ca5db0b473b", size = 83857, upload-time = "2025-10-06T14:11:13.586Z" }, + { url = "https://files.pythonhosted.org/packages/46/b3/e20ef504049f1a1c54a814b4b9bed96d1ac0e0610c3b4da178f87209db05/yarl-1.22.0-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:34b36c2c57124530884d89d50ed2c1478697ad7473efd59cfd479945c95650e4", size = 140520, upload-time = "2025-10-06T14:11:15.465Z" }, + { url = "https://files.pythonhosted.org/packages/e4/04/3532d990fdbab02e5ede063676b5c4260e7f3abea2151099c2aa745acc4c/yarl-1.22.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:0dd9a702591ca2e543631c2a017e4a547e38a5c0f29eece37d9097e04a7ac683", size = 93504, upload-time = "2025-10-06T14:11:17.106Z" }, + { url = "https://files.pythonhosted.org/packages/11/63/ff458113c5c2dac9a9719ac68ee7c947cb621432bcf28c9972b1c0e83938/yarl-1.22.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:594fcab1032e2d2cc3321bb2e51271e7cd2b516c7d9aee780ece81b07ff8244b", size = 94282, upload-time = "2025-10-06T14:11:19.064Z" }, + { url = "https://files.pythonhosted.org/packages/a7/bc/315a56aca762d44a6aaaf7ad253f04d996cb6b27bad34410f82d76ea8038/yarl-1.22.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f3d7a87a78d46a2e3d5b72587ac14b4c16952dd0887dbb051451eceac774411e", size = 372080, upload-time = "2025-10-06T14:11:20.996Z" }, + { url = "https://files.pythonhosted.org/packages/3f/3f/08e9b826ec2e099ea6e7c69a61272f4f6da62cb5b1b63590bb80ca2e4a40/yarl-1.22.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:852863707010316c973162e703bddabec35e8757e67fcb8ad58829de1ebc8590", size = 338696, upload-time = "2025-10-06T14:11:22.847Z" }, + { url = "https://files.pythonhosted.org/packages/e3/9f/90360108e3b32bd76789088e99538febfea24a102380ae73827f62073543/yarl-1.22.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:131a085a53bfe839a477c0845acf21efc77457ba2bcf5899618136d64f3303a2", size = 387121, upload-time = "2025-10-06T14:11:24.889Z" }, + { url = "https://files.pythonhosted.org/packages/98/92/ab8d4657bd5b46a38094cfaea498f18bb70ce6b63508fd7e909bd1f93066/yarl-1.22.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:078a8aefd263f4d4f923a9677b942b445a2be970ca24548a8102689a3a8ab8da", size = 394080, upload-time = "2025-10-06T14:11:27.307Z" }, + { url = "https://files.pythonhosted.org/packages/f5/e7/d8c5a7752fef68205296201f8ec2bf718f5c805a7a7e9880576c67600658/yarl-1.22.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bca03b91c323036913993ff5c738d0842fc9c60c4648e5c8d98331526df89784", size = 372661, upload-time = "2025-10-06T14:11:29.387Z" }, + { url = "https://files.pythonhosted.org/packages/b6/2e/f4d26183c8db0bb82d491b072f3127fb8c381a6206a3a56332714b79b751/yarl-1.22.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:68986a61557d37bb90d3051a45b91fa3d5c516d177dfc6dd6f2f436a07ff2b6b", size = 364645, upload-time = "2025-10-06T14:11:31.423Z" }, + { url = "https://files.pythonhosted.org/packages/80/7c/428e5812e6b87cd00ee8e898328a62c95825bf37c7fa87f0b6bb2ad31304/yarl-1.22.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:4792b262d585ff0dff6bcb787f8492e40698443ec982a3568c2096433660c694", size = 355361, upload-time = "2025-10-06T14:11:33.055Z" }, + { url = "https://files.pythonhosted.org/packages/ec/2a/249405fd26776f8b13c067378ef4d7dd49c9098d1b6457cdd152a99e96a9/yarl-1.22.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:ebd4549b108d732dba1d4ace67614b9545b21ece30937a63a65dd34efa19732d", size = 381451, upload-time = "2025-10-06T14:11:35.136Z" }, + { url = "https://files.pythonhosted.org/packages/67/a8/fb6b1adbe98cf1e2dd9fad71003d3a63a1bc22459c6e15f5714eb9323b93/yarl-1.22.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:f87ac53513d22240c7d59203f25cc3beac1e574c6cd681bbfd321987b69f95fd", size = 383814, upload-time = "2025-10-06T14:11:37.094Z" }, + { url = "https://files.pythonhosted.org/packages/d9/f9/3aa2c0e480fb73e872ae2814c43bc1e734740bb0d54e8cb2a95925f98131/yarl-1.22.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:22b029f2881599e2f1b06f8f1db2ee63bd309e2293ba2d566e008ba12778b8da", size = 370799, upload-time = "2025-10-06T14:11:38.83Z" }, + { url = "https://files.pythonhosted.org/packages/50/3c/af9dba3b8b5eeb302f36f16f92791f3ea62e3f47763406abf6d5a4a3333b/yarl-1.22.0-cp314-cp314-win32.whl", hash = "sha256:6a635ea45ba4ea8238463b4f7d0e721bad669f80878b7bfd1f89266e2ae63da2", size = 82990, upload-time = "2025-10-06T14:11:40.624Z" }, + { url = "https://files.pythonhosted.org/packages/ac/30/ac3a0c5bdc1d6efd1b41fa24d4897a4329b3b1e98de9449679dd327af4f0/yarl-1.22.0-cp314-cp314-win_amd64.whl", hash = "sha256:0d6e6885777af0f110b0e5d7e5dda8b704efed3894da26220b7f3d887b839a79", size = 88292, upload-time = "2025-10-06T14:11:42.578Z" }, + { url = "https://files.pythonhosted.org/packages/df/0a/227ab4ff5b998a1b7410abc7b46c9b7a26b0ca9e86c34ba4b8d8bc7c63d5/yarl-1.22.0-cp314-cp314-win_arm64.whl", hash = "sha256:8218f4e98d3c10d683584cb40f0424f4b9fd6e95610232dd75e13743b070ee33", size = 82888, upload-time = "2025-10-06T14:11:44.863Z" }, + { url = "https://files.pythonhosted.org/packages/06/5e/a15eb13db90abd87dfbefb9760c0f3f257ac42a5cac7e75dbc23bed97a9f/yarl-1.22.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:45c2842ff0e0d1b35a6bf1cd6c690939dacb617a70827f715232b2e0494d55d1", size = 146223, upload-time = "2025-10-06T14:11:46.796Z" }, + { url = "https://files.pythonhosted.org/packages/18/82/9665c61910d4d84f41a5bf6837597c89e665fa88aa4941080704645932a9/yarl-1.22.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:d947071e6ebcf2e2bee8fce76e10faca8f7a14808ca36a910263acaacef08eca", size = 95981, upload-time = "2025-10-06T14:11:48.845Z" }, + { url = "https://files.pythonhosted.org/packages/5d/9a/2f65743589809af4d0a6d3aa749343c4b5f4c380cc24a8e94a3c6625a808/yarl-1.22.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:334b8721303e61b00019474cc103bdac3d7b1f65e91f0bfedeec2d56dfe74b53", size = 97303, upload-time = "2025-10-06T14:11:50.897Z" }, + { url = "https://files.pythonhosted.org/packages/b0/ab/5b13d3e157505c43c3b43b5a776cbf7b24a02bc4cccc40314771197e3508/yarl-1.22.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1e7ce67c34138a058fd092f67d07a72b8e31ff0c9236e751957465a24b28910c", size = 361820, upload-time = "2025-10-06T14:11:52.549Z" }, + { url = "https://files.pythonhosted.org/packages/fb/76/242a5ef4677615cf95330cfc1b4610e78184400699bdda0acb897ef5e49a/yarl-1.22.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d77e1b2c6d04711478cb1c4ab90db07f1609ccf06a287d5607fcd90dc9863acf", size = 323203, upload-time = "2025-10-06T14:11:54.225Z" }, + { url = "https://files.pythonhosted.org/packages/8c/96/475509110d3f0153b43d06164cf4195c64d16999e0c7e2d8a099adcd6907/yarl-1.22.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c4647674b6150d2cae088fc07de2738a84b8bcedebef29802cf0b0a82ab6face", size = 363173, upload-time = "2025-10-06T14:11:56.069Z" }, + { url = "https://files.pythonhosted.org/packages/c9/66/59db471aecfbd559a1fd48aedd954435558cd98c7d0da8b03cc6c140a32c/yarl-1.22.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:efb07073be061c8f79d03d04139a80ba33cbd390ca8f0297aae9cce6411e4c6b", size = 373562, upload-time = "2025-10-06T14:11:58.783Z" }, + { url = "https://files.pythonhosted.org/packages/03/1f/c5d94abc91557384719da10ff166b916107c1b45e4d0423a88457071dd88/yarl-1.22.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e51ac5435758ba97ad69617e13233da53908beccc6cfcd6c34bbed8dcbede486", size = 339828, upload-time = "2025-10-06T14:12:00.686Z" }, + { url = "https://files.pythonhosted.org/packages/5f/97/aa6a143d3afba17b6465733681c70cf175af89f76ec8d9286e08437a7454/yarl-1.22.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:33e32a0dd0c8205efa8e83d04fc9f19313772b78522d1bdc7d9aed706bfd6138", size = 347551, upload-time = "2025-10-06T14:12:02.628Z" }, + { url = "https://files.pythonhosted.org/packages/43/3c/45a2b6d80195959239a7b2a8810506d4eea5487dce61c2a3393e7fc3c52e/yarl-1.22.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:bf4a21e58b9cde0e401e683ebd00f6ed30a06d14e93f7c8fd059f8b6e8f87b6a", size = 334512, upload-time = "2025-10-06T14:12:04.871Z" }, + { url = "https://files.pythonhosted.org/packages/86/a0/c2ab48d74599c7c84cb104ebd799c5813de252bea0f360ffc29d270c2caa/yarl-1.22.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:e4b582bab49ac33c8deb97e058cd67c2c50dac0dd134874106d9c774fd272529", size = 352400, upload-time = "2025-10-06T14:12:06.624Z" }, + { url = "https://files.pythonhosted.org/packages/32/75/f8919b2eafc929567d3d8411f72bdb1a2109c01caaab4ebfa5f8ffadc15b/yarl-1.22.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:0b5bcc1a9c4839e7e30b7b30dd47fe5e7e44fb7054ec29b5bb8d526aa1041093", size = 357140, upload-time = "2025-10-06T14:12:08.362Z" }, + { url = "https://files.pythonhosted.org/packages/cf/72/6a85bba382f22cf78add705d8c3731748397d986e197e53ecc7835e76de7/yarl-1.22.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:c0232bce2170103ec23c454e54a57008a9a72b5d1c3105dc2496750da8cfa47c", size = 341473, upload-time = "2025-10-06T14:12:10.994Z" }, + { url = "https://files.pythonhosted.org/packages/35/18/55e6011f7c044dc80b98893060773cefcfdbf60dfefb8cb2f58b9bacbd83/yarl-1.22.0-cp314-cp314t-win32.whl", hash = "sha256:8009b3173bcd637be650922ac455946197d858b3630b6d8787aa9e5c4564533e", size = 89056, upload-time = "2025-10-06T14:12:13.317Z" }, + { url = "https://files.pythonhosted.org/packages/f9/86/0f0dccb6e59a9e7f122c5afd43568b1d31b8ab7dda5f1b01fb5c7025c9a9/yarl-1.22.0-cp314-cp314t-win_amd64.whl", hash = "sha256:9fb17ea16e972c63d25d4a97f016d235c78dd2344820eb35bc034bc32012ee27", size = 96292, upload-time = "2025-10-06T14:12:15.398Z" }, + { url = "https://files.pythonhosted.org/packages/48/b7/503c98092fb3b344a179579f55814b613c1fbb1c23b3ec14a7b008a66a6e/yarl-1.22.0-cp314-cp314t-win_arm64.whl", hash = "sha256:9f6d73c1436b934e3f01df1e1b21ff765cd1d28c77dfb9ace207f746d4610ee1", size = 85171, upload-time = "2025-10-06T14:12:16.935Z" }, + { url = "https://files.pythonhosted.org/packages/73/ae/b48f95715333080afb75a4504487cbe142cae1268afc482d06692d605ae6/yarl-1.22.0-py3-none-any.whl", hash = "sha256:1380560bdba02b6b6c90de54133c81c9f2a453dee9912fe58c1dcced1edb7cff", size = 46814, upload-time = "2025-10-06T14:12:53.872Z" }, ] [[package]] name = "zipp" version = "3.23.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e3/02/0f2892c661036d50ede074e376733dca2ae7c6eb617489437771209d4180/zipp-3.23.0.tar.gz", hash = "sha256:a07157588a12518c9d4034df3fbbee09c814741a33ff63c05fa29d26a2404166", size = 25547 } +sdist = { url = "https://files.pythonhosted.org/packages/e3/02/0f2892c661036d50ede074e376733dca2ae7c6eb617489437771209d4180/zipp-3.23.0.tar.gz", hash = "sha256:a07157588a12518c9d4034df3fbbee09c814741a33ff63c05fa29d26a2404166", size = 25547, upload-time = "2025-06-08T17:06:39.4Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/2e/54/647ade08bf0db230bfea292f893923872fd20be6ac6f53b2b936ba839d75/zipp-3.23.0-py3-none-any.whl", hash = "sha256:071652d6115ed432f5ce1d34c336c0adfd6a884660d1e9712a256d3d3bd4b14e", size = 10276 }, + { url = "https://files.pythonhosted.org/packages/2e/54/647ade08bf0db230bfea292f893923872fd20be6ac6f53b2b936ba839d75/zipp-3.23.0-py3-none-any.whl", hash = "sha256:071652d6115ed432f5ce1d34c336c0adfd6a884660d1e9712a256d3d3bd4b14e", size = 10276, upload-time = "2025-06-08T17:06:38.034Z" }, ] diff --git a/claude-agent/Dockerfile b/claude-agent/Dockerfile deleted file mode 100644 index 0f62057..0000000 --- a/claude-agent/Dockerfile +++ /dev/null @@ -1,69 +0,0 @@ -# Use uv with Python pre-installed -FROM ghcr.io/astral-sh/uv:python3.12-bookworm-slim - -# Install system dependencies including Node.js for Claude CLI and GitHub CLI -RUN apt-get update && apt-get install -y --no-install-recommends \ - curl \ - git \ - nodejs \ - npm \ - gnupg \ - && rm -rf /var/lib/apt/lists/* - -# Install GitHub CLI (gh) -SHELL ["/bin/bash", "-o", "pipefail", "-c"] -RUN curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg | dd of=/usr/share/keyrings/githubcli-archive-keyring.gpg \ - && chmod go+r /usr/share/keyrings/githubcli-archive-keyring.gpg \ - && echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" | tee /etc/apt/sources.list.d/github-cli.list > /dev/null \ - && apt-get update \ - && apt-get install -y --no-install-recommends gh \ - && rm -rf /var/lib/apt/lists/* - -# Install Claude Code CLI and create non-root user -RUN npm install -g @anthropic-ai/claude-code@latest \ - && useradd -m -s /bin/bash claude \ - && mkdir -p /home/claude/.claude \ - && chown -R claude:claude /home/claude/.claude - -# Set up working directory -WORKDIR /app - -# Enable bytecode compilation for faster startup -ENV UV_COMPILE_BYTECODE=1 - -# Disable Python output buffering for real-time logs -ENV PYTHONUNBUFFERED=1 - -# Copy from cache instead of linking (for mounted volumes) -ENV UV_LINK_MODE=copy - -# Install dependencies using lockfile with BuildKit cache -RUN --mount=type=cache,target=/root/.cache/uv \ - --mount=type=bind,source=uv.lock,target=uv.lock \ - --mount=type=bind,source=pyproject.toml,target=pyproject.toml \ - uv sync --locked --no-install-project --no-dev - -# Copy application code -COPY --chown=claude:claude server.py /app/server.py -COPY --chown=claude:claude job_runner.py /app/job_runner.py -COPY --chown=claude:claude entrypoint.sh /entrypoint.sh -RUN chmod +x /entrypoint.sh /app/server.py /app/job_runner.py - -# Place venv executables at front of path -ENV PATH="/app/.venv/bin:$PATH" - -# Make /app writable by claude for DevSpace sync -RUN chown -R claude:claude /app - -# Reset entrypoint (don't invoke uv) -ENTRYPOINT [] - -USER claude -WORKDIR /workspace - -EXPOSE 8001 - -HEALTHCHECK --interval=30s --timeout=5s --start-period=5s --retries=3 \ - CMD curl -f http://localhost:8001/health || exit 1 - -CMD ["/entrypoint.sh"] diff --git a/claude-agent/README.md b/claude-agent/README.md deleted file mode 100644 index 09f16b9..0000000 --- a/claude-agent/README.md +++ /dev/null @@ -1,17 +0,0 @@ -# Claude Agent Container - -This container runs the Claude Code CLI for the mainloop backend. - -## Current State - -The service accepts requests from the backend, runs the current Claude integration in its workspace, and returns or streams results. Kubernetes jobs can also execute a bounded request and report the result through a callback. - -## Usage - -The backend communicates with this container via the internal Docker network. - -## Configuration - -- Claude Code uses the authentication configured for the runtime environment -- Workspace is mounted at `/workspace` -- Configuration from `~/.claude` is mounted read-only diff --git a/claude-agent/entrypoint.sh b/claude-agent/entrypoint.sh deleted file mode 100644 index 327414f..0000000 --- a/claude-agent/entrypoint.sh +++ /dev/null @@ -1,42 +0,0 @@ -#!/bin/bash -set -e - -echo "Starting Claude Agent Worker API..." -echo "Workspace: /workspace" -echo "Listening on port 8001" - -# Create Claude credentials file from env var if token is provided -# This is required for the Claude Agent SDK to authenticate -if [[ -n ${CLAUDE_CODE_OAUTH_TOKEN} ]]; then - mkdir -p ~/.claude - cat >~/.claude/.credentials.json < dict: - """Execute the task using Claude Agent SDK.""" - print(f"[job_runner] Model: {CLAUDE_MODEL}") - print(f"[job_runner] Prompt:\n{TASK_PROMPT[:500]}...") - - options = ClaudeAgentOptions( - model=CLAUDE_MODEL, - permission_mode="bypassPermissions", - cwd=WORKSPACE, - ) - - collected_text: list[str] = [] - session_id: str | None = None - cost_usd: float | None = None - - async for message in query(prompt=TASK_PROMPT, options=options): - if isinstance(message, AssistantMessage): - for block in message.content: - if isinstance(block, TextBlock): - print(f"[claude] {block.text[:200]}...") - collected_text.append(block.text) - elif isinstance(message, ResultMessage): - session_id = message.session_id - cost_usd = message.total_cost_usd - if message.is_error: - raise RuntimeError(message.result or "Claude execution failed") - - output = "\n".join(collected_text) if collected_text else "No response generated." - - return { - "output": output, - "session_id": session_id, - "cost_usd": cost_usd, - } - - -async def send_result( - status: str, result: dict | None = None, error: str | None = None -): - """Send the result back to the backend via HTTP callback.""" - if not CALLBACK_URL: - print("[job_runner] No callback URL, skipping result POST") - return - - payload = { - "session_id": SESSION_ID, - "status": status, - "result": result, - "error": error, - "completed_at": datetime.now(timezone.utc).isoformat(), - } - - print(f"[job_runner] Sending result to {CALLBACK_URL}") - print(f"[job_runner] Status: {status}") - - async with httpx.AsyncClient(timeout=30.0) as client: - for attempt in range(3): - try: - response = await client.post( - CALLBACK_URL, - json=payload, - headers={"Content-Type": "application/json"}, - ) - response.raise_for_status() - print("[job_runner] Result sent successfully") - return - except httpx.RequestError as e: - print(f"[job_runner] Attempt {attempt + 1} failed: {e}") - if attempt < 2: - await asyncio.sleep(2**attempt) - else: - print("[job_runner] Failed to send result after 3 attempts") - raise - - -async def main(): - """Execute the job runner workflow.""" - print(f"[job_runner] Starting job for session {SESSION_ID}") - print(f"[job_runner] Working directory: {WORKSPACE}") - - # Validate required env vars - if not SESSION_ID: - print("[job_runner] ERROR: SESSION_ID not set") - sys.exit(1) - if not TASK_PROMPT: - print("[job_runner] ERROR: TASK_PROMPT not set") - sys.exit(1) - - # Ensure workspace exists - Path(WORKSPACE).mkdir(parents=True, exist_ok=True) - os.chdir(WORKSPACE) - - try: - result = await execute_task() - await send_result( - status="completed", - result=result, - ) - print("[job_runner] Job completed successfully") - - except Exception as e: - print(f"[job_runner] ERROR: {e}") - await send_result( - status="failed", - error=str(e), - ) - sys.exit(1) - - -if __name__ == "__main__": - asyncio.run(main()) diff --git a/claude-agent/pyproject.toml b/claude-agent/pyproject.toml deleted file mode 100644 index 06e1c5c..0000000 --- a/claude-agent/pyproject.toml +++ /dev/null @@ -1,17 +0,0 @@ -[project] -name = "claude-agent" -version = "0.1.0" -description = "Claude Code CLI container API" -requires-python = ">=3.12" -dependencies = [ - "fastapi>=0.115.0", - "uvicorn[standard]>=0.32.0", - "pydantic>=2.10.0", - "claude-agent-sdk>=0.1.18", - "githubkit>=0.11.0", - "GitPython>=3.1.0", -] - -[build-system] -requires = ["hatchling"] -build-backend = "hatchling.build" diff --git a/claude-agent/server.py b/claude-agent/server.py deleted file mode 100644 index 9046348..0000000 --- a/claude-agent/server.py +++ /dev/null @@ -1,199 +0,0 @@ -#!/usr/bin/env python3 -""" -Claude Agent Worker API using the Agent SDK. -Each worker maintains its own independent session with context compaction support. -""" -import json -import logging -from pathlib import Path - -from claude_agent_sdk import ( - AssistantMessage, - ClaudeAgentOptions, - ResultMessage, - SystemMessage, - TextBlock, - query, -) -from fastapi import FastAPI, HTTPException -from fastapi.responses import StreamingResponse -from pydantic import BaseModel - -logging.basicConfig(level=logging.INFO) -logger = logging.getLogger(__name__) - -app = FastAPI(title="Claude Agent Worker API") - -CREDENTIALS_PATH = Path.home() / ".claude" / ".credentials.json" - - -class ExecuteRequest(BaseModel): - """Request to execute Claude Code.""" - - prompt: str - workspace: str = "/workspace" - model: str = "haiku" - session_id: str | None = None # Resume from existing session - max_turns: int | None = None # Limit agent turns - - -class ExecuteResponse(BaseModel): - """Response from Claude Code execution.""" - - output: str - session_id: str | None = None - cost_usd: float | None = None - error: str | None = None - compacted: bool = False # Whether context was compacted during execution - compaction_count: int = 0 # Number of compactions that occurred - - -class AuthStatus(BaseModel): - """Authentication status.""" - - authenticated: bool - expires_at: int | None = None - subscription_type: str | None = None - error: str | None = None - - -def get_credentials() -> dict | None: - """Read credentials from file.""" - if CREDENTIALS_PATH.exists(): - try: - return json.loads(CREDENTIALS_PATH.read_text()) - except Exception: - return None - return None - - -def check_auth() -> AuthStatus: - """Check if we have valid authentication.""" - creds = get_credentials() - if not creds: - return AuthStatus(authenticated=False, error="No credentials found") - - oauth = creds.get("claudeAiOauth", {}) - if not oauth.get("accessToken"): - return AuthStatus(authenticated=False, error="No access token") - - return AuthStatus( - authenticated=True, - expires_at=oauth.get("expiresAt"), - subscription_type=oauth.get("subscriptionType"), - ) - - -@app.get("/health") -async def health(): - """Health check endpoint.""" - auth = check_auth() - return { - "status": "ok", - "service": "claude-agent-worker", - "authenticated": auth.authenticated, - } - - -@app.get("/auth/status", response_model=AuthStatus) -async def auth_status(): - """Check authentication status.""" - return check_auth() - - -@app.post("/execute", response_model=ExecuteResponse) -async def execute_claude(request: ExecuteRequest): - """Execute a prompt using Claude Agent SDK with session and compaction support.""" - try: - options = ClaudeAgentOptions( - model=request.model, - permission_mode="bypassPermissions", - cwd=request.workspace, - resume=request.session_id, # Resume existing session if provided - max_turns=request.max_turns, # Limit agent turns - ) - - collected_text: list[str] = [] - session_id: str | None = None - cost_usd: float | None = None - compaction_count: int = 0 - - async for message in query(prompt=request.prompt, options=options): - if isinstance(message, AssistantMessage): - for block in message.content: - if isinstance(block, TextBlock): - collected_text.append(block.text) - elif isinstance(message, ResultMessage): - session_id = message.session_id - cost_usd = message.total_cost_usd - if message.is_error: - return ExecuteResponse( - output="", - session_id=session_id, - cost_usd=cost_usd, - error=message.result or "Unknown error", - compacted=compaction_count > 0, - compaction_count=compaction_count, - ) - elif isinstance(message, SystemMessage): - # Track compaction events (context was automatically summarized) - if message.subtype == "compact_boundary": - compaction_count += 1 - data = message.data or {} - pre_tokens = data.get("pre_tokens", 0) - trigger = data.get("trigger", "unknown") - logger.info( - f"Context compacted ({trigger}): {pre_tokens} tokens summarized" - ) - - return ExecuteResponse( - output="\n".join(collected_text) if collected_text else "", - session_id=session_id, - cost_usd=cost_usd, - compacted=compaction_count > 0, - compaction_count=compaction_count, - ) - - except Exception as e: - logger.exception(f"Execute failed: {e}") - raise HTTPException(status_code=500, detail=str(e)) from e - - -@app.post("/execute/stream") -async def execute_claude_stream(request: ExecuteRequest): - """Stream execution results using Claude Agent SDK.""" - - async def generate(): - try: - options = ClaudeAgentOptions( - model=request.model, - permission_mode="bypassPermissions", - cwd=request.workspace, - ) - - async for message in query(prompt=request.prompt, options=options): - if isinstance(message, AssistantMessage): - for block in message.content: - if isinstance(block, TextBlock): - yield f"data: {json.dumps({'type': 'text', 'content': block.text})}\n\n" - elif isinstance(message, ResultMessage): - yield f"data: {json.dumps({'type': 'result', 'session_id': message.session_id, 'cost_usd': message.total_cost_usd, 'is_error': message.is_error})}\n\n" - - yield "data: [DONE]\n\n" - except Exception as e: - yield f"data: {json.dumps({'type': 'error', 'error': str(e)})}\n\n" - - return StreamingResponse( - generate(), - media_type="text/event-stream", - headers={ - "Cache-Control": "no-cache", - "Connection": "keep-alive", - }, - ) - - -if __name__ == "__main__": - import uvicorn - - uvicorn.run(app, host="0.0.0.0", port=8001) diff --git a/claude-agent/test_plan_mode.py b/claude-agent/test_plan_mode.py deleted file mode 100644 index ad95c6e..0000000 --- a/claude-agent/test_plan_mode.py +++ /dev/null @@ -1,197 +0,0 @@ -#!/usr/bin/env python3 -""" -Tests for native plan mode support in job_runner. - -Unit tests that can run without K8s or Claude credentials. -""" - -import os -import unittest -from unittest.mock import MagicMock, patch - -# Set up environment before importing job_runner -os.environ.setdefault("TASK_ID", "test-task-123") -os.environ.setdefault("TASK_PROMPT", "Add a new feature to handle user authentication") -os.environ.setdefault("REPO_URL", "https://github.com/test/repo") -os.environ.setdefault("MODE", "plan") - -import job_runner - - -class TestPlanPrompt(unittest.TestCase): - """Test plan prompt generation.""" - - def test_plan_prompt_with_repo(self): - """Plan prompt should include repo and simplified instructions.""" - with patch.object(job_runner, "REPO_URL", "https://github.com/test/repo"): - with patch.object(job_runner, "TASK_PROMPT", "Add authentication"): - with patch.object(job_runner, "TASK_ID", "abc12345"): - with patch.object(job_runner, "FEEDBACK_CONTEXT", ""): - prompt = job_runner.build_plan_prompt() - - # Should contain task info - self.assertIn("Task ID: abc12345", prompt) - self.assertIn("Add authentication", prompt) - self.assertIn("https://github.com/test/repo", prompt) - - # Should have simplified instructions (not GitHub issue creation) - self.assertIn("Clone the repository", prompt) - self.assertIn("implementation plan", prompt) - self.assertIn("Approach", prompt) - self.assertIn("Files to modify", prompt) - - # Should NOT contain old prompt instructions - self.assertNotIn("gh issue create", prompt) - self.assertNotIn("mainloop-plan", prompt) - - def test_plan_prompt_without_repo(self): - """Plan prompt should work without repo URL.""" - with patch.object(job_runner, "REPO_URL", ""): - with patch.object(job_runner, "TASK_PROMPT", "Generic task"): - with patch.object(job_runner, "TASK_ID", "xyz789"): - with patch.object(job_runner, "FEEDBACK_CONTEXT", ""): - prompt = job_runner.build_plan_prompt() - - self.assertIn("Generic task", prompt) - self.assertIn("Create an implementation plan", prompt) - - def test_plan_prompt_with_feedback(self): - """Plan prompt should include feedback context for revisions.""" - with patch.object(job_runner, "REPO_URL", "https://github.com/test/repo"): - with patch.object(job_runner, "TASK_PROMPT", "Add feature"): - with patch.object(job_runner, "TASK_ID", "rev123"): - with patch.object( - job_runner, - "FEEDBACK_CONTEXT", - "Please add more detail about error handling", - ): - prompt = job_runner.build_plan_prompt() - - self.assertIn("Feedback on your previous plan", prompt) - self.assertIn("Please add more detail about error handling", prompt) - - -class TestPermissionMode(unittest.TestCase): - """Test permission mode selection. - - All batch job modes use bypassPermissions since there's no human - to approve permission requests in a K8s Job context. - """ - - def test_all_modes_use_bypass(self): - """All modes should use permission_mode='bypassPermissions' for batch jobs.""" - # Batch jobs can't get human approval, so all modes bypass - for mode in ["plan", "implement", "feedback", "fix"]: - with patch.object(job_runner, "MODE", mode): - # This mirrors the actual logic in execute_task() - perm_mode = "bypassPermissions" - self.assertEqual( - perm_mode, - "bypassPermissions", - f"Mode {mode} should use bypassPermissions", - ) - - -class TestGitHubIssueCreation(unittest.TestCase): - """Test GitHub issue creation from plan content.""" - - @patch("subprocess.run") - def test_create_issue_success(self, mock_run): - """Should create issue and return URL on success.""" - # Mock successful gh issue create - mock_run.return_value = MagicMock( - returncode=0, - stdout="https://github.com/test/repo/issues/42\n", - stderr="", - ) - - with patch.object(job_runner, "REPO_URL", "https://github.com/test/repo"): - with patch.object(job_runner, "TASK_PROMPT", "Add authentication"): - with patch.object(job_runner, "WORKSPACE", "/workspace"): - result = job_runner.create_github_issue_from_plan( - "## Plan content here" - ) - - self.assertEqual(result, "https://github.com/test/repo/issues/42") - - # Verify gh issue create was called - create_call = mock_run.call_args_list[0] - self.assertEqual(create_call[0][0][0:3], ["gh", "issue", "create"]) - - @patch("subprocess.run") - def test_create_issue_failure(self, mock_run): - """Should return None on gh CLI failure.""" - mock_run.return_value = MagicMock( - returncode=1, - stdout="", - stderr="error: could not create issue", - ) - - with patch.object(job_runner, "REPO_URL", "https://github.com/test/repo"): - with patch.object(job_runner, "TASK_PROMPT", "Add feature"): - with patch.object(job_runner, "WORKSPACE", "/workspace"): - result = job_runner.create_github_issue_from_plan("## Plan") - - self.assertIsNone(result) - - def test_create_issue_no_repo(self): - """Should return None when no repo URL.""" - with patch.object(job_runner, "REPO_URL", ""): - result = job_runner.create_github_issue_from_plan("## Plan") - - self.assertIsNone(result) - - @patch("subprocess.run") - def test_issue_body_includes_commands(self, mock_run): - """Issue body should include /implement and /revise commands.""" - mock_run.return_value = MagicMock( - returncode=0, - stdout="https://github.com/test/repo/issues/1\n", - stderr="", - ) - - with patch.object(job_runner, "REPO_URL", "https://github.com/test/repo"): - with patch.object(job_runner, "TASK_PROMPT", "Task"): - with patch.object(job_runner, "WORKSPACE", "/workspace"): - job_runner.create_github_issue_from_plan("## My Plan") - - # Get the body argument from the call - create_call = mock_run.call_args_list[0] - body_idx = create_call[0][0].index("--body") + 1 - body = create_call[0][0][body_idx] - - self.assertIn("/implement", body) - self.assertIn("/revise", body) - self.assertIn("## My Plan", body) - - -class TestUrlExtraction(unittest.TestCase): - """Test URL extraction from output.""" - - def test_extract_pr_url(self): - """Should extract PR URL from output.""" - output = "Created PR: https://github.com/owner/repo/pull/123\nDone!" - result = job_runner.extract_pr_url(output) - self.assertEqual(result, "https://github.com/owner/repo/pull/123") - - def test_extract_pr_url_none(self): - """Should return None when no PR URL found.""" - output = "No PR created" - result = job_runner.extract_pr_url(output) - self.assertIsNone(result) - - def test_extract_issue_url(self): - """Should extract issue URL from output.""" - output = "Created issue: https://github.com/owner/repo/issues/456" - result = job_runner.extract_issue_url(output) - self.assertEqual(result, "https://github.com/owner/repo/issues/456") - - def test_extract_issue_url_none(self): - """Should return None when no issue URL found.""" - output = "No issue created" - result = job_runner.extract_issue_url(output) - self.assertIsNone(result) - - -if __name__ == "__main__": - unittest.main(verbosity=2) diff --git a/claude-agent/uv.lock b/claude-agent/uv.lock deleted file mode 100644 index 4e39254..0000000 --- a/claude-agent/uv.lock +++ /dev/null @@ -1,995 +0,0 @@ -version = 1 -revision = 1 -requires-python = ">=3.12" - -[[package]] -name = "annotated-doc" -version = "0.0.4" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/57/ba/046ceea27344560984e26a590f90bc7f4a75b06701f653222458922b558c/annotated_doc-0.0.4.tar.gz", hash = "sha256:fbcda96e87e9c92ad167c2e53839e57503ecfda18804ea28102353485033faa4", size = 7288 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/1e/d3/26bf1008eb3d2daa8ef4cacc7f3bfdc11818d111f7e2d0201bc6e3b49d45/annotated_doc-0.0.4-py3-none-any.whl", hash = "sha256:571ac1dc6991c450b25a9c2d84a3705e2ae7a53467b5d111c24fa8baabbed320", size = 5303 }, -] - -[[package]] -name = "annotated-types" -version = "0.7.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643 }, -] - -[[package]] -name = "anyio" -version = "4.12.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "idna" }, - { name = "typing-extensions", marker = "python_full_version < '3.13'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/16/ce/8a777047513153587e5434fd752e89334ac33e379aa3497db860eeb60377/anyio-4.12.0.tar.gz", hash = "sha256:73c693b567b0c55130c104d0b43a9baf3aa6a31fc6110116509f27bf75e21ec0", size = 228266 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/7f/9c/36c5c37947ebfb8c7f22e0eb6e4d188ee2d53aa3880f3f2744fb894f0cb1/anyio-4.12.0-py3-none-any.whl", hash = "sha256:dad2376a628f98eeca4881fc56cd06affd18f659b17a747d3ff0307ced94b1bb", size = 113362 }, -] - -[[package]] -name = "anysqlite" -version = "0.0.5" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "anyio" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/0f/4b/cd5d66b9f87e773bc71344a368b9472987e33514e6627e28342b9c3e7c43/anysqlite-0.0.5.tar.gz", hash = "sha256:9dfcf87baf6b93426ad1d9118088c41dbf24ef01b445eea4a5d486bac2755cce", size = 3432 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/0b/31/349eae2bc9d9331dd8951684cf94528d91efaa71129dc30822ac111dfc66/anysqlite-0.0.5-py3-none-any.whl", hash = "sha256:cb345dc4f76f6b37f768d7a0b3e9cf5c700dfcb7a6356af8ab46a11f666edbe7", size = 3907 }, -] - -[[package]] -name = "attrs" -version = "25.4.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/6b/5c/685e6633917e101e5dcb62b9dd76946cbb57c26e133bae9e0cd36033c0a9/attrs-25.4.0.tar.gz", hash = "sha256:16d5969b87f0859ef33a48b35d55ac1be6e42ae49d5e853b597db70c35c57e11", size = 934251 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/3a/2a/7cc015f5b9f5db42b7d48157e23356022889fc354a2813c15934b7cb5c0e/attrs-25.4.0-py3-none-any.whl", hash = "sha256:adcf7e2a1fb3b36ac48d97835bb6d8ade15b8dcce26aba8bf1d14847b57a3373", size = 67615 }, -] - -[[package]] -name = "certifi" -version = "2025.11.12" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a2/8c/58f469717fa48465e4a50c014a0400602d3c437d7c0c468e17ada824da3a/certifi-2025.11.12.tar.gz", hash = "sha256:d8ab5478f2ecd78af242878415affce761ca6bc54a22a27e026d7c25357c3316", size = 160538 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/70/7d/9bc192684cea499815ff478dfcdc13835ddf401365057044fb721ec6bddb/certifi-2025.11.12-py3-none-any.whl", hash = "sha256:97de8790030bbd5c2d96b7ec782fc2f7820ef8dba6db909ccf95449f2d062d4b", size = 159438 }, -] - -[[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 } -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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { url = "https://files.pythonhosted.org/packages/7b/2b/2b6435f76bfeb6bbf055596976da087377ede68df465419d192acf00c437/cffi-2.0.0-cp312-cp312-win32.whl", hash = "sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18", size = 172932 }, - { url = "https://files.pythonhosted.org/packages/f8/ed/13bd4418627013bec4ed6e54283b1959cf6db888048c7cf4b4c3b5b36002/cffi-2.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5", size = 183557 }, - { url = "https://files.pythonhosted.org/packages/95/31/9f7f93ad2f8eff1dbc1c3656d7ca5bfd8fb52c9d786b4dcf19b2d02217fa/cffi-2.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6", size = 177762 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { url = "https://files.pythonhosted.org/packages/eb/6d/bf9bda840d5f1dfdbf0feca87fbdb64a918a69bca42cfa0ba7b137c48cb8/cffi-2.0.0-cp313-cp313-win32.whl", hash = "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27", size = 172909 }, - { url = "https://files.pythonhosted.org/packages/37/18/6519e1ee6f5a1e579e04b9ddb6f1676c17368a7aba48299c3759bbc3c8b3/cffi-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75", size = 183402 }, - { url = "https://files.pythonhosted.org/packages/cb/0e/02ceeec9a7d6ee63bb596121c2c8e9b3a9e150936f4fbef6ca1943e6137c/cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91", size = 177780 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { url = "https://files.pythonhosted.org/packages/3e/aa/df335faa45b395396fcbc03de2dfcab242cd61a9900e914fe682a59170b1/cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f", size = 175328 }, - { url = "https://files.pythonhosted.org/packages/bb/92/882c2d30831744296ce713f0feb4c1cd30f346ef747b530b5318715cc367/cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25", size = 185650 }, - { url = "https://files.pythonhosted.org/packages/9f/2c/98ece204b9d35a7366b5b2c6539c350313ca13932143e79dc133ba757104/cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad", size = 180687 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { url = "https://files.pythonhosted.org/packages/a0/1d/ec1a60bd1a10daa292d3cd6bb0b359a81607154fb8165f3ec95fe003b85c/cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e", size = 180487 }, - { url = "https://files.pythonhosted.org/packages/bf/41/4c1168c74fac325c0c8156f04b6749c8b6a8f405bbf91413ba088359f60d/cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6", size = 191726 }, - { url = "https://files.pythonhosted.org/packages/ae/3a/dbeec9d1ee0844c679f6bb5d6ad4e9f198b1224f4e7a32825f47f6192b0c/cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9", size = 184195 }, -] - -[[package]] -name = "claude-agent" -version = "0.1.0" -source = { editable = "." } -dependencies = [ - { name = "claude-agent-sdk" }, - { name = "fastapi" }, - { name = "githubkit" }, - { name = "gitpython" }, - { name = "pydantic" }, - { name = "uvicorn", extra = ["standard"] }, -] - -[package.metadata] -requires-dist = [ - { name = "claude-agent-sdk", specifier = ">=0.1.18" }, - { name = "fastapi", specifier = ">=0.115.0" }, - { name = "githubkit", specifier = ">=0.11.0" }, - { name = "gitpython", specifier = ">=3.1.0" }, - { name = "pydantic", specifier = ">=2.10.0" }, - { name = "uvicorn", extras = ["standard"], specifier = ">=0.32.0" }, -] - -[[package]] -name = "claude-agent-sdk" -version = "0.1.18" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "anyio" }, - { name = "mcp" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/fd/3d/a8c6ad873e8448696d44441c9eb2c24dded620fb32415d68f576a542ccde/claude_agent_sdk-0.1.18.tar.gz", hash = "sha256:4fcb8730cc77dea562fbe9aa48c65eced3ef58a6bb1f34f77e50e8258902477d", size = 56162 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/06/14/f529f7c4bab7c71dcbcc8c66f12f491e644ee8a027ac5111d13705df207e/claude_agent_sdk-0.1.18-py3-none-macosx_11_0_arm64.whl", hash = "sha256:9e45b4e3c20c072c3e3325fa60bab9a4b5a7cbbce64ca274b8d7d0af42dd9dd8", size = 54560828 }, - { url = "https://files.pythonhosted.org/packages/2c/68/6e83005aa7bb9056bfad0aef0605249f877dc0c78724c9c0fadebff600fb/claude_agent_sdk-0.1.18-py3-none-manylinux_2_17_aarch64.whl", hash = "sha256:3c41bd8f38848609ae0d5da8d7327a4c2d7057a363feafb6fd70df611ea204cc", size = 68743107 }, - { url = "https://files.pythonhosted.org/packages/fb/85/7d6dd85f402135a610894734c442f1166ffed61d03eced39d6bfd14efccd/claude_agent_sdk-0.1.18-py3-none-manylinux_2_17_x86_64.whl", hash = "sha256:983f15e51253f40c55136a86d7cc63e023a3576428b05fa1459093d461b2d215", size = 70444964 }, - { url = "https://files.pythonhosted.org/packages/3c/fa/d2b22b7a713c4c049cbd5f9f635836ea5429ff65c1f3bcf4658a8e1c1cf5/claude_agent_sdk-0.1.18-py3-none-win_amd64.whl", hash = "sha256:36f5b84d5c3c8773ee9b56aeb5ab345d1033231db37f80d1f20ac15239bef41c", size = 72637215 }, -] - -[[package]] -name = "click" -version = "8.3.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "colorama", marker = "sys_platform == 'win32'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/3d/fa/656b739db8587d7b5dfa22e22ed02566950fbfbcdc20311993483657a5c0/click-8.3.1.tar.gz", hash = "sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a", size = 295065 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/98/78/01c019cdb5d6498122777c1a43056ebb3ebfeef2076d9d026bfe15583b2b/click-8.3.1-py3-none-any.whl", hash = "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6", size = 108274 }, -] - -[[package]] -name = "colorama" -version = "0.4.6" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 }, -] - -[[package]] -name = "cryptography" -version = "46.0.3" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/9f/33/c00162f49c0e2fe8064a62cb92b93e50c74a72bc370ab92f86112b33ff62/cryptography-46.0.3.tar.gz", hash = "sha256:a8b17438104fed022ce745b362294d9ce35b4c2e45c1d958ad4a4b019285f4a1", size = 749258 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/1d/42/9c391dd801d6cf0d561b5890549d4b27bafcc53b39c31a817e69d87c625b/cryptography-46.0.3-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:109d4ddfadf17e8e7779c39f9b18111a09efb969a301a31e987416a0191ed93a", size = 7225004 }, - { url = "https://files.pythonhosted.org/packages/1c/67/38769ca6b65f07461eb200e85fc1639b438bdc667be02cf7f2cd6a64601c/cryptography-46.0.3-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:09859af8466b69bc3c27bdf4f5d84a665e0f7ab5088412e9e2ec49758eca5cbc", size = 4296667 }, - { url = "https://files.pythonhosted.org/packages/5c/49/498c86566a1d80e978b42f0d702795f69887005548c041636df6ae1ca64c/cryptography-46.0.3-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:01ca9ff2885f3acc98c29f1860552e37f6d7c7d013d7334ff2a9de43a449315d", size = 4450807 }, - { url = "https://files.pythonhosted.org/packages/4b/0a/863a3604112174c8624a2ac3c038662d9e59970c7f926acdcfaed8d61142/cryptography-46.0.3-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:6eae65d4c3d33da080cff9c4ab1f711b15c1d9760809dad6ea763f3812d254cb", size = 4299615 }, - { url = "https://files.pythonhosted.org/packages/64/02/b73a533f6b64a69f3cd3872acb6ebc12aef924d8d103133bb3ea750dc703/cryptography-46.0.3-cp311-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e5bf0ed4490068a2e72ac03d786693adeb909981cc596425d09032d372bcc849", size = 4016800 }, - { url = "https://files.pythonhosted.org/packages/25/d5/16e41afbfa450cde85a3b7ec599bebefaef16b5c6ba4ec49a3532336ed72/cryptography-46.0.3-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:5ecfccd2329e37e9b7112a888e76d9feca2347f12f37918facbb893d7bb88ee8", size = 4984707 }, - { url = "https://files.pythonhosted.org/packages/c9/56/e7e69b427c3878352c2fb9b450bd0e19ed552753491d39d7d0a2f5226d41/cryptography-46.0.3-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:a2c0cd47381a3229c403062f764160d57d4d175e022c1df84e168c6251a22eec", size = 4482541 }, - { url = "https://files.pythonhosted.org/packages/78/f6/50736d40d97e8483172f1bb6e698895b92a223dba513b0ca6f06b2365339/cryptography-46.0.3-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:549e234ff32571b1f4076ac269fcce7a808d3bf98b76c8dd560e42dbc66d7d91", size = 4299464 }, - { url = "https://files.pythonhosted.org/packages/00/de/d8e26b1a855f19d9994a19c702fa2e93b0456beccbcfe437eda00e0701f2/cryptography-46.0.3-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:c0a7bb1a68a5d3471880e264621346c48665b3bf1c3759d682fc0864c540bd9e", size = 4950838 }, - { url = "https://files.pythonhosted.org/packages/8f/29/798fc4ec461a1c9e9f735f2fc58741b0daae30688f41b2497dcbc9ed1355/cryptography-46.0.3-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:10b01676fc208c3e6feeb25a8b83d81767e8059e1fe86e1dc62d10a3018fa926", size = 4481596 }, - { url = "https://files.pythonhosted.org/packages/15/8d/03cd48b20a573adfff7652b76271078e3045b9f49387920e7f1f631d125e/cryptography-46.0.3-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:0abf1ffd6e57c67e92af68330d05760b7b7efb243aab8377e583284dbab72c71", size = 4426782 }, - { url = "https://files.pythonhosted.org/packages/fa/b1/ebacbfe53317d55cf33165bda24c86523497a6881f339f9aae5c2e13e57b/cryptography-46.0.3-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a04bee9ab6a4da801eb9b51f1b708a1b5b5c9eb48c03f74198464c66f0d344ac", size = 4698381 }, - { url = "https://files.pythonhosted.org/packages/96/92/8a6a9525893325fc057a01f654d7efc2c64b9de90413adcf605a85744ff4/cryptography-46.0.3-cp311-abi3-win32.whl", hash = "sha256:f260d0d41e9b4da1ed1e0f1ce571f97fe370b152ab18778e9e8f67d6af432018", size = 3055988 }, - { url = "https://files.pythonhosted.org/packages/7e/bf/80fbf45253ea585a1e492a6a17efcb93467701fa79e71550a430c5e60df0/cryptography-46.0.3-cp311-abi3-win_amd64.whl", hash = "sha256:a9a3008438615669153eb86b26b61e09993921ebdd75385ddd748702c5adfddb", size = 3514451 }, - { url = "https://files.pythonhosted.org/packages/2e/af/9b302da4c87b0beb9db4e756386a7c6c5b8003cd0e742277888d352ae91d/cryptography-46.0.3-cp311-abi3-win_arm64.whl", hash = "sha256:5d7f93296ee28f68447397bf5198428c9aeeab45705a55d53a6343455dcb2c3c", size = 2928007 }, - { url = "https://files.pythonhosted.org/packages/f5/e2/a510aa736755bffa9d2f75029c229111a1d02f8ecd5de03078f4c18d91a3/cryptography-46.0.3-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:00a5e7e87938e5ff9ff5447ab086a5706a957137e6e433841e9d24f38a065217", size = 7158012 }, - { url = "https://files.pythonhosted.org/packages/73/dc/9aa866fbdbb95b02e7f9d086f1fccfeebf8953509b87e3f28fff927ff8a0/cryptography-46.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c8daeb2d2174beb4575b77482320303f3d39b8e81153da4f0fb08eb5fe86a6c5", size = 4288728 }, - { url = "https://files.pythonhosted.org/packages/c5/fd/bc1daf8230eaa075184cbbf5f8cd00ba9db4fd32d63fb83da4671b72ed8a/cryptography-46.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:39b6755623145ad5eff1dab323f4eae2a32a77a7abef2c5089a04a3d04366715", size = 4435078 }, - { url = "https://files.pythonhosted.org/packages/82/98/d3bd5407ce4c60017f8ff9e63ffee4200ab3e23fe05b765cab805a7db008/cryptography-46.0.3-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:db391fa7c66df6762ee3f00c95a89e6d428f4d60e7abc8328f4fe155b5ac6e54", size = 4293460 }, - { url = "https://files.pythonhosted.org/packages/26/e9/e23e7900983c2b8af7a08098db406cf989d7f09caea7897e347598d4cd5b/cryptography-46.0.3-cp314-cp314t-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:78a97cf6a8839a48c49271cdcbd5cf37ca2c1d6b7fdd86cc864f302b5e9bf459", size = 3995237 }, - { url = "https://files.pythonhosted.org/packages/91/15/af68c509d4a138cfe299d0d7ddb14afba15233223ebd933b4bbdbc7155d3/cryptography-46.0.3-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:dfb781ff7eaa91a6f7fd41776ec37c5853c795d3b358d4896fdbb5df168af422", size = 4967344 }, - { url = "https://files.pythonhosted.org/packages/ca/e3/8643d077c53868b681af077edf6b3cb58288b5423610f21c62aadcbe99f4/cryptography-46.0.3-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:6f61efb26e76c45c4a227835ddeae96d83624fb0d29eb5df5b96e14ed1a0afb7", size = 4466564 }, - { url = "https://files.pythonhosted.org/packages/0e/43/c1e8726fa59c236ff477ff2b5dc071e54b21e5a1e51aa2cee1676f1c986f/cryptography-46.0.3-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:23b1a8f26e43f47ceb6d6a43115f33a5a37d57df4ea0ca295b780ae8546e8044", size = 4292415 }, - { url = "https://files.pythonhosted.org/packages/42/f9/2f8fefdb1aee8a8e3256a0568cffc4e6d517b256a2fe97a029b3f1b9fe7e/cryptography-46.0.3-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:b419ae593c86b87014b9be7396b385491ad7f320bde96826d0dd174459e54665", size = 4931457 }, - { url = "https://files.pythonhosted.org/packages/79/30/9b54127a9a778ccd6d27c3da7563e9f2d341826075ceab89ae3b41bf5be2/cryptography-46.0.3-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:50fc3343ac490c6b08c0cf0d704e881d0d660be923fd3076db3e932007e726e3", size = 4466074 }, - { url = "https://files.pythonhosted.org/packages/ac/68/b4f4a10928e26c941b1b6a179143af9f4d27d88fe84a6a3c53592d2e76bf/cryptography-46.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:22d7e97932f511d6b0b04f2bfd818d73dcd5928db509460aaf48384778eb6d20", size = 4420569 }, - { url = "https://files.pythonhosted.org/packages/a3/49/3746dab4c0d1979888f125226357d3262a6dd40e114ac29e3d2abdf1ec55/cryptography-46.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:d55f3dffadd674514ad19451161118fd010988540cee43d8bc20675e775925de", size = 4681941 }, - { url = "https://files.pythonhosted.org/packages/fd/30/27654c1dbaf7e4a3531fa1fc77986d04aefa4d6d78259a62c9dc13d7ad36/cryptography-46.0.3-cp314-cp314t-win32.whl", hash = "sha256:8a6e050cb6164d3f830453754094c086ff2d0b2f3a897a1d9820f6139a1f0914", size = 3022339 }, - { url = "https://files.pythonhosted.org/packages/f6/30/640f34ccd4d2a1bc88367b54b926b781b5a018d65f404d409aba76a84b1c/cryptography-46.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:760f83faa07f8b64e9c33fc963d790a2edb24efb479e3520c14a45741cd9b2db", size = 3494315 }, - { url = "https://files.pythonhosted.org/packages/ba/8b/88cc7e3bd0a8e7b861f26981f7b820e1f46aa9d26cc482d0feba0ecb4919/cryptography-46.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:516ea134e703e9fe26bcd1277a4b59ad30586ea90c365a87781d7887a646fe21", size = 2919331 }, - { url = "https://files.pythonhosted.org/packages/fd/23/45fe7f376a7df8daf6da3556603b36f53475a99ce4faacb6ba2cf3d82021/cryptography-46.0.3-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:cb3d760a6117f621261d662bccc8ef5bc32ca673e037c83fbe565324f5c46936", size = 7218248 }, - { url = "https://files.pythonhosted.org/packages/27/32/b68d27471372737054cbd34c84981f9edbc24fe67ca225d389799614e27f/cryptography-46.0.3-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:4b7387121ac7d15e550f5cb4a43aef2559ed759c35df7336c402bb8275ac9683", size = 4294089 }, - { url = "https://files.pythonhosted.org/packages/26/42/fa8389d4478368743e24e61eea78846a0006caffaf72ea24a15159215a14/cryptography-46.0.3-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:15ab9b093e8f09daab0f2159bb7e47532596075139dd74365da52ecc9cb46c5d", size = 4440029 }, - { url = "https://files.pythonhosted.org/packages/5f/eb/f483db0ec5ac040824f269e93dd2bd8a21ecd1027e77ad7bdf6914f2fd80/cryptography-46.0.3-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:46acf53b40ea38f9c6c229599a4a13f0d46a6c3fa9ef19fc1a124d62e338dfa0", size = 4297222 }, - { url = "https://files.pythonhosted.org/packages/fd/cf/da9502c4e1912cb1da3807ea3618a6829bee8207456fbbeebc361ec38ba3/cryptography-46.0.3-cp38-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:10ca84c4668d066a9878890047f03546f3ae0a6b8b39b697457b7757aaf18dbc", size = 4012280 }, - { url = "https://files.pythonhosted.org/packages/6b/8f/9adb86b93330e0df8b3dcf03eae67c33ba89958fc2e03862ef1ac2b42465/cryptography-46.0.3-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:36e627112085bb3b81b19fed209c05ce2a52ee8b15d161b7c643a7d5a88491f3", size = 4978958 }, - { url = "https://files.pythonhosted.org/packages/d1/a0/5fa77988289c34bdb9f913f5606ecc9ada1adb5ae870bd0d1054a7021cc4/cryptography-46.0.3-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:1000713389b75c449a6e979ffc7dcc8ac90b437048766cef052d4d30b8220971", size = 4473714 }, - { url = "https://files.pythonhosted.org/packages/14/e5/fc82d72a58d41c393697aa18c9abe5ae1214ff6f2a5c18ac470f92777895/cryptography-46.0.3-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:b02cf04496f6576afffef5ddd04a0cb7d49cf6be16a9059d793a30b035f6b6ac", size = 4296970 }, - { url = "https://files.pythonhosted.org/packages/78/06/5663ed35438d0b09056973994f1aec467492b33bd31da36e468b01ec1097/cryptography-46.0.3-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:71e842ec9bc7abf543b47cf86b9a743baa95f4677d22baa4c7d5c69e49e9bc04", size = 4940236 }, - { url = "https://files.pythonhosted.org/packages/fc/59/873633f3f2dcd8a053b8dd1d38f783043b5fce589c0f6988bf55ef57e43e/cryptography-46.0.3-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:402b58fc32614f00980b66d6e56a5b4118e6cb362ae8f3fda141ba4689bd4506", size = 4472642 }, - { url = "https://files.pythonhosted.org/packages/3d/39/8e71f3930e40f6877737d6f69248cf74d4e34b886a3967d32f919cc50d3b/cryptography-46.0.3-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:ef639cb3372f69ec44915fafcd6698b6cc78fbe0c2ea41be867f6ed612811963", size = 4423126 }, - { url = "https://files.pythonhosted.org/packages/cd/c7/f65027c2810e14c3e7268353b1681932b87e5a48e65505d8cc17c99e36ae/cryptography-46.0.3-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:3b51b8ca4f1c6453d8829e1eb7299499ca7f313900dd4d89a24b8b87c0a780d4", size = 4686573 }, - { url = "https://files.pythonhosted.org/packages/0a/6e/1c8331ddf91ca4730ab3086a0f1be19c65510a33b5a441cb334e7a2d2560/cryptography-46.0.3-cp38-abi3-win32.whl", hash = "sha256:6276eb85ef938dc035d59b87c8a7dc559a232f954962520137529d77b18ff1df", size = 3036695 }, - { url = "https://files.pythonhosted.org/packages/90/45/b0d691df20633eff80955a0fc7695ff9051ffce8b69741444bd9ed7bd0db/cryptography-46.0.3-cp38-abi3-win_amd64.whl", hash = "sha256:416260257577718c05135c55958b674000baef9a1c7d9e8f306ec60d71db850f", size = 3501720 }, - { url = "https://files.pythonhosted.org/packages/e8/cb/2da4cc83f5edb9c3257d09e1e7ab7b23f049c7962cae8d842bbef0a9cec9/cryptography-46.0.3-cp38-abi3-win_arm64.whl", hash = "sha256:d89c3468de4cdc4f08a57e214384d0471911a3830fcdaf7a8cc587e42a866372", size = 2918740 }, -] - -[[package]] -name = "fastapi" -version = "0.127.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "annotated-doc" }, - { name = "pydantic" }, - { name = "starlette" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/96/8a/6b9ba6eb8ff3817caae83120495965d9e70afb4d6348cb120e464ee199f4/fastapi-0.127.1.tar.gz", hash = "sha256:946a87ee5d931883b562b6bada787d6c8178becee2683cb3f9b980d593206359", size = 391876 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d2/f3/a6858d147ed2645c095d11dc2440f94a5f1cd8f4df888e3377e6b5281a0f/fastapi-0.127.1-py3-none-any.whl", hash = "sha256:31d670a4f9373cc6d7994420f98e4dc46ea693145207abc39696746c83a44430", size = 112332 }, -] - -[[package]] -name = "gitdb" -version = "4.0.12" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "smmap" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/72/94/63b0fc47eb32792c7ba1fe1b694daec9a63620db1e313033d18140c2320a/gitdb-4.0.12.tar.gz", hash = "sha256:5ef71f855d191a3326fcfbc0d5da835f26b13fbcba60c32c21091c349ffdb571", size = 394684 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a0/61/5c78b91c3143ed5c14207f463aecfc8f9dbb5092fb2869baf37c273b2705/gitdb-4.0.12-py3-none-any.whl", hash = "sha256:67073e15955400952c6565cc3e707c554a4eea2e428946f7a4c162fab9bd9bcf", size = 62794 }, -] - -[[package]] -name = "githubkit" -version = "0.14.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "anyio" }, - { name = "hishel" }, - { name = "httpx" }, - { name = "pydantic" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/55/3b/d8d4bd504b9d353f40d83f9c52d6e7b5efbc2b2c54979584e045cee87555/githubkit-0.14.1.tar.gz", hash = "sha256:7f5f9463c901c44871ffe0c1ddb1f2959f8cdbe015e0ca5ba0bfcaf1858651af", size = 2643130 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/8f/d1/c2e6e05ea979d1ef1c0ab99cf21e3ae5728d7c2db16d4a1fea9f8f345d77/githubkit-0.14.1-py3-none-any.whl", hash = "sha256:e0055a287d86543ba64db65a44a828b93e21ff221bc12a77913349e4816f5195", size = 6446837 }, -] - -[[package]] -name = "gitpython" -version = "3.1.46" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "gitdb" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/df/b5/59d16470a1f0dfe8c793f9ef56fd3826093fc52b3bd96d6b9d6c26c7e27b/gitpython-3.1.46.tar.gz", hash = "sha256:400124c7d0ef4ea03f7310ac2fbf7151e09ff97f2a3288d64a440c584a29c37f", size = 215371 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/6a/09/e21df6aef1e1ffc0c816f0522ddc3f6dcded766c3261813131c78a704470/gitpython-3.1.46-py3-none-any.whl", hash = "sha256:79812ed143d9d25b6d176a10bb511de0f9c67b1fa641d82097b0ab90398a2058", size = 208620 }, -] - -[[package]] -name = "h11" -version = "0.16.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515 }, -] - -[[package]] -name = "hishel" -version = "0.1.5" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "anyio" }, - { name = "anysqlite" }, - { name = "httpx" }, - { name = "msgpack" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/e5/64/a104ccac48f123f853254483617b16e0efc1649bd7e35bcdc5a5a5ef0ae2/hishel-0.1.5.tar.gz", hash = "sha256:9d40c682cd94fd6e1394fb05713ae20a75ed8aeba6f5272380444039ce6257f2", size = 75468 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/70/83/4f8b77839e62114bb034375ee8e08cfb6af1164754b925b271d3f1ec06ee/hishel-0.1.5-py3-none-any.whl", hash = "sha256:0bfbe9a2b9342090eba82ba6de88258092e1c4c7b730cd4cb4b570e4b40e44a7", size = 92486 }, -] - -[[package]] -name = "httpcore" -version = "1.0.9" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "certifi" }, - { name = "h11" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784 }, -] - -[[package]] -name = "httptools" -version = "0.7.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b5/46/120a669232c7bdedb9d52d4aeae7e6c7dfe151e99dc70802e2fc7a5e1993/httptools-0.7.1.tar.gz", hash = "sha256:abd72556974f8e7c74a259655924a717a2365b236c882c3f6f8a45fe94703ac9", size = 258961 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/53/7f/403e5d787dc4942316e515e949b0c8a013d84078a915910e9f391ba9b3ed/httptools-0.7.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:38e0c83a2ea9746ebbd643bdfb521b9aa4a91703e2cd705c20443405d2fd16a5", size = 206280 }, - { url = "https://files.pythonhosted.org/packages/2a/0d/7f3fd28e2ce311ccc998c388dd1c53b18120fda3b70ebb022b135dc9839b/httptools-0.7.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f25bbaf1235e27704f1a7b86cd3304eabc04f569c828101d94a0e605ef7205a5", size = 110004 }, - { url = "https://files.pythonhosted.org/packages/84/a6/b3965e1e146ef5762870bbe76117876ceba51a201e18cc31f5703e454596/httptools-0.7.1-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2c15f37ef679ab9ecc06bfc4e6e8628c32a8e4b305459de7cf6785acd57e4d03", size = 517655 }, - { url = "https://files.pythonhosted.org/packages/11/7d/71fee6f1844e6fa378f2eddde6c3e41ce3a1fb4b2d81118dd544e3441ec0/httptools-0.7.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7fe6e96090df46b36ccfaf746f03034e5ab723162bc51b0a4cf58305324036f2", size = 511440 }, - { url = "https://files.pythonhosted.org/packages/22/a5/079d216712a4f3ffa24af4a0381b108aa9c45b7a5cc6eb141f81726b1823/httptools-0.7.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f72fdbae2dbc6e68b8239defb48e6a5937b12218e6ffc2c7846cc37befa84362", size = 495186 }, - { url = "https://files.pythonhosted.org/packages/e9/9e/025ad7b65278745dee3bd0ebf9314934c4592560878308a6121f7f812084/httptools-0.7.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e99c7b90a29fd82fea9ef57943d501a16f3404d7b9ee81799d41639bdaae412c", size = 499192 }, - { url = "https://files.pythonhosted.org/packages/6d/de/40a8f202b987d43afc4d54689600ff03ce65680ede2f31df348d7f368b8f/httptools-0.7.1-cp312-cp312-win_amd64.whl", hash = "sha256:3e14f530fefa7499334a79b0cf7e7cd2992870eb893526fb097d51b4f2d0f321", size = 86694 }, - { url = "https://files.pythonhosted.org/packages/09/8f/c77b1fcbfd262d422f12da02feb0d218fa228d52485b77b953832105bb90/httptools-0.7.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:6babce6cfa2a99545c60bfef8bee0cc0545413cb0018f617c8059a30ad985de3", size = 202889 }, - { url = "https://files.pythonhosted.org/packages/0a/1a/22887f53602feaa066354867bc49a68fc295c2293433177ee90870a7d517/httptools-0.7.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:601b7628de7504077dd3dcb3791c6b8694bbd967148a6d1f01806509254fb1ca", size = 108180 }, - { url = "https://files.pythonhosted.org/packages/32/6a/6aaa91937f0010d288d3d124ca2946d48d60c3a5ee7ca62afe870e3ea011/httptools-0.7.1-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:04c6c0e6c5fb0739c5b8a9eb046d298650a0ff38cf42537fc372b28dc7e4472c", size = 478596 }, - { url = "https://files.pythonhosted.org/packages/6d/70/023d7ce117993107be88d2cbca566a7c1323ccbaf0af7eabf2064fe356f6/httptools-0.7.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:69d4f9705c405ae3ee83d6a12283dc9feba8cc6aaec671b412917e644ab4fa66", size = 473268 }, - { url = "https://files.pythonhosted.org/packages/32/4d/9dd616c38da088e3f436e9a616e1d0cc66544b8cdac405cc4e81c8679fc7/httptools-0.7.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:44c8f4347d4b31269c8a9205d8a5ee2df5322b09bbbd30f8f862185bb6b05346", size = 455517 }, - { url = "https://files.pythonhosted.org/packages/1d/3a/a6c595c310b7df958e739aae88724e24f9246a514d909547778d776799be/httptools-0.7.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:465275d76db4d554918aba40bf1cbebe324670f3dfc979eaffaa5d108e2ed650", size = 458337 }, - { url = "https://files.pythonhosted.org/packages/fd/82/88e8d6d2c51edc1cc391b6e044c6c435b6aebe97b1abc33db1b0b24cd582/httptools-0.7.1-cp313-cp313-win_amd64.whl", hash = "sha256:322d00c2068d125bd570f7bf78b2d367dad02b919d8581d7476d8b75b294e3e6", size = 85743 }, - { url = "https://files.pythonhosted.org/packages/34/50/9d095fcbb6de2d523e027a2f304d4551855c2f46e0b82befd718b8b20056/httptools-0.7.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:c08fe65728b8d70b6923ce31e3956f859d5e1e8548e6f22ec520a962c6757270", size = 203619 }, - { url = "https://files.pythonhosted.org/packages/07/f0/89720dc5139ae54b03f861b5e2c55a37dba9a5da7d51e1e824a1f343627f/httptools-0.7.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:7aea2e3c3953521c3c51106ee11487a910d45586e351202474d45472db7d72d3", size = 108714 }, - { url = "https://files.pythonhosted.org/packages/b3/cb/eea88506f191fb552c11787c23f9a405f4c7b0c5799bf73f2249cd4f5228/httptools-0.7.1-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0e68b8582f4ea9166be62926077a3334064d422cf08ab87d8b74664f8e9058e1", size = 472909 }, - { url = "https://files.pythonhosted.org/packages/e0/4a/a548bdfae6369c0d078bab5769f7b66f17f1bfaa6fa28f81d6be6959066b/httptools-0.7.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:df091cf961a3be783d6aebae963cc9b71e00d57fa6f149025075217bc6a55a7b", size = 470831 }, - { url = "https://files.pythonhosted.org/packages/4d/31/14df99e1c43bd132eec921c2e7e11cda7852f65619bc0fc5bdc2d0cb126c/httptools-0.7.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f084813239e1eb403ddacd06a30de3d3e09a9b76e7894dcda2b22f8a726e9c60", size = 452631 }, - { url = "https://files.pythonhosted.org/packages/22/d2/b7e131f7be8d854d48cb6d048113c30f9a46dca0c9a8b08fcb3fcd588cdc/httptools-0.7.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7347714368fb2b335e9063bc2b96f2f87a9ceffcd9758ac295f8bbcd3ffbc0ca", size = 452910 }, - { url = "https://files.pythonhosted.org/packages/53/cf/878f3b91e4e6e011eff6d1fa9ca39f7eb17d19c9d7971b04873734112f30/httptools-0.7.1-cp314-cp314-win_amd64.whl", hash = "sha256:cfabda2a5bb85aa2a904ce06d974a3f30fb36cc63d7feaddec05d2050acede96", size = 88205 }, -] - -[[package]] -name = "httpx" -version = "0.28.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "anyio" }, - { name = "certifi" }, - { name = "httpcore" }, - { name = "idna" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517 }, -] - -[[package]] -name = "httpx-sse" -version = "0.4.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/0f/4c/751061ffa58615a32c31b2d82e8482be8dd4a89154f003147acee90f2be9/httpx_sse-0.4.3.tar.gz", hash = "sha256:9b1ed0127459a66014aec3c56bebd93da3c1bc8bb6618c8082039a44889a755d", size = 15943 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d2/fd/6668e5aec43ab844de6fc74927e155a3b37bf40d7c3790e49fc0406b6578/httpx_sse-0.4.3-py3-none-any.whl", hash = "sha256:0ac1c9fe3c0afad2e0ebb25a934a59f4c7823b60792691f779fad2c5568830fc", size = 8960 }, -] - -[[package]] -name = "idna" -version = "3.11" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008 }, -] - -[[package]] -name = "jsonschema" -version = "4.25.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "attrs" }, - { name = "jsonschema-specifications" }, - { name = "referencing" }, - { name = "rpds-py" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/74/69/f7185de793a29082a9f3c7728268ffb31cb5095131a9c139a74078e27336/jsonschema-4.25.1.tar.gz", hash = "sha256:e4a9655ce0da0c0b67a085847e00a3a51449e1157f4f75e9fb5aa545e122eb85", size = 357342 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/bf/9c/8c95d856233c1f82500c2450b8c68576b4cf1c871db3afac5c34ff84e6fd/jsonschema-4.25.1-py3-none-any.whl", hash = "sha256:3fba0169e345c7175110351d456342c364814cfcf3b964ba4587f22915230a63", size = 90040 }, -] - -[[package]] -name = "jsonschema-specifications" -version = "2025.9.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "referencing" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/19/74/a633ee74eb36c44aa6d1095e7cc5569bebf04342ee146178e2d36600708b/jsonschema_specifications-2025.9.1.tar.gz", hash = "sha256:b540987f239e745613c7a9176f3edb72b832a4ac465cf02712288397832b5e8d", size = 32855 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl", hash = "sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe", size = 18437 }, -] - -[[package]] -name = "mcp" -version = "1.25.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "anyio" }, - { name = "httpx" }, - { name = "httpx-sse" }, - { name = "jsonschema" }, - { name = "pydantic" }, - { name = "pydantic-settings" }, - { name = "pyjwt", extra = ["crypto"] }, - { name = "python-multipart" }, - { name = "pywin32", marker = "sys_platform == 'win32'" }, - { name = "sse-starlette" }, - { name = "starlette" }, - { name = "typing-extensions" }, - { name = "typing-inspection" }, - { name = "uvicorn", marker = "sys_platform != 'emscripten'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/d5/2d/649d80a0ecf6a1f82632ca44bec21c0461a9d9fc8934d38cb5b319f2db5e/mcp-1.25.0.tar.gz", hash = "sha256:56310361ebf0364e2d438e5b45f7668cbb124e158bb358333cd06e49e83a6802", size = 605387 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e2/fc/6dc7659c2ae5ddf280477011f4213a74f806862856b796ef08f028e664bf/mcp-1.25.0-py3-none-any.whl", hash = "sha256:b37c38144a666add0862614cc79ec276e97d72aa8ca26d622818d4e278b9721a", size = 233076 }, -] - -[[package]] -name = "msgpack" -version = "1.1.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/4d/f2/bfb55a6236ed8725a96b0aa3acbd0ec17588e6a2c3b62a93eb513ed8783f/msgpack-1.1.2.tar.gz", hash = "sha256:3b60763c1373dd60f398488069bcdc703cd08a711477b5d480eecc9f9626f47e", size = 173581 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ad/bd/8b0d01c756203fbab65d265859749860682ccd2a59594609aeec3a144efa/msgpack-1.1.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:70a0dff9d1f8da25179ffcf880e10cf1aad55fdb63cd59c9a49a1b82290062aa", size = 81939 }, - { url = "https://files.pythonhosted.org/packages/34/68/ba4f155f793a74c1483d4bdef136e1023f7bcba557f0db4ef3db3c665cf1/msgpack-1.1.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:446abdd8b94b55c800ac34b102dffd2f6aa0ce643c55dfc017ad89347db3dbdb", size = 85064 }, - { url = "https://files.pythonhosted.org/packages/f2/60/a064b0345fc36c4c3d2c743c82d9100c40388d77f0b48b2f04d6041dbec1/msgpack-1.1.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c63eea553c69ab05b6747901b97d620bb2a690633c77f23feb0c6a947a8a7b8f", size = 417131 }, - { url = "https://files.pythonhosted.org/packages/65/92/a5100f7185a800a5d29f8d14041f61475b9de465ffcc0f3b9fba606e4505/msgpack-1.1.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:372839311ccf6bdaf39b00b61288e0557916c3729529b301c52c2d88842add42", size = 427556 }, - { url = "https://files.pythonhosted.org/packages/f5/87/ffe21d1bf7d9991354ad93949286f643b2bb6ddbeab66373922b44c3b8cc/msgpack-1.1.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2929af52106ca73fcb28576218476ffbb531a036c2adbcf54a3664de124303e9", size = 404920 }, - { url = "https://files.pythonhosted.org/packages/ff/41/8543ed2b8604f7c0d89ce066f42007faac1eaa7d79a81555f206a5cdb889/msgpack-1.1.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:be52a8fc79e45b0364210eef5234a7cf8d330836d0a64dfbb878efa903d84620", size = 415013 }, - { url = "https://files.pythonhosted.org/packages/41/0d/2ddfaa8b7e1cee6c490d46cb0a39742b19e2481600a7a0e96537e9c22f43/msgpack-1.1.2-cp312-cp312-win32.whl", hash = "sha256:1fff3d825d7859ac888b0fbda39a42d59193543920eda9d9bea44d958a878029", size = 65096 }, - { url = "https://files.pythonhosted.org/packages/8c/ec/d431eb7941fb55a31dd6ca3404d41fbb52d99172df2e7707754488390910/msgpack-1.1.2-cp312-cp312-win_amd64.whl", hash = "sha256:1de460f0403172cff81169a30b9a92b260cb809c4cb7e2fc79ae8d0510c78b6b", size = 72708 }, - { url = "https://files.pythonhosted.org/packages/c5/31/5b1a1f70eb0e87d1678e9624908f86317787b536060641d6798e3cf70ace/msgpack-1.1.2-cp312-cp312-win_arm64.whl", hash = "sha256:be5980f3ee0e6bd44f3a9e9dea01054f175b50c3e6cdb692bc9424c0bbb8bf69", size = 64119 }, - { url = "https://files.pythonhosted.org/packages/6b/31/b46518ecc604d7edf3a4f94cb3bf021fc62aa301f0cb849936968164ef23/msgpack-1.1.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4efd7b5979ccb539c221a4c4e16aac1a533efc97f3b759bb5a5ac9f6d10383bf", size = 81212 }, - { url = "https://files.pythonhosted.org/packages/92/dc/c385f38f2c2433333345a82926c6bfa5ecfff3ef787201614317b58dd8be/msgpack-1.1.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:42eefe2c3e2af97ed470eec850facbe1b5ad1d6eacdbadc42ec98e7dcf68b4b7", size = 84315 }, - { url = "https://files.pythonhosted.org/packages/d3/68/93180dce57f684a61a88a45ed13047558ded2be46f03acb8dec6d7c513af/msgpack-1.1.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1fdf7d83102bf09e7ce3357de96c59b627395352a4024f6e2458501f158bf999", size = 412721 }, - { url = "https://files.pythonhosted.org/packages/5d/ba/459f18c16f2b3fc1a1ca871f72f07d70c07bf768ad0a507a698b8052ac58/msgpack-1.1.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fac4be746328f90caa3cd4bc67e6fe36ca2bf61d5c6eb6d895b6527e3f05071e", size = 424657 }, - { url = "https://files.pythonhosted.org/packages/38/f8/4398c46863b093252fe67368b44edc6c13b17f4e6b0e4929dbf0bdb13f23/msgpack-1.1.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:fffee09044073e69f2bad787071aeec727183e7580443dfeb8556cbf1978d162", size = 402668 }, - { url = "https://files.pythonhosted.org/packages/28/ce/698c1eff75626e4124b4d78e21cca0b4cc90043afb80a507626ea354ab52/msgpack-1.1.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5928604de9b032bc17f5099496417f113c45bc6bc21b5c6920caf34b3c428794", size = 419040 }, - { url = "https://files.pythonhosted.org/packages/67/32/f3cd1667028424fa7001d82e10ee35386eea1408b93d399b09fb0aa7875f/msgpack-1.1.2-cp313-cp313-win32.whl", hash = "sha256:a7787d353595c7c7e145e2331abf8b7ff1e6673a6b974ded96e6d4ec09f00c8c", size = 65037 }, - { url = "https://files.pythonhosted.org/packages/74/07/1ed8277f8653c40ebc65985180b007879f6a836c525b3885dcc6448ae6cb/msgpack-1.1.2-cp313-cp313-win_amd64.whl", hash = "sha256:a465f0dceb8e13a487e54c07d04ae3ba131c7c5b95e2612596eafde1dccf64a9", size = 72631 }, - { url = "https://files.pythonhosted.org/packages/e5/db/0314e4e2db56ebcf450f277904ffd84a7988b9e5da8d0d61ab2d057df2b6/msgpack-1.1.2-cp313-cp313-win_arm64.whl", hash = "sha256:e69b39f8c0aa5ec24b57737ebee40be647035158f14ed4b40e6f150077e21a84", size = 64118 }, - { url = "https://files.pythonhosted.org/packages/22/71/201105712d0a2ff07b7873ed3c220292fb2ea5120603c00c4b634bcdafb3/msgpack-1.1.2-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:e23ce8d5f7aa6ea6d2a2b326b4ba46c985dbb204523759984430db7114f8aa00", size = 81127 }, - { url = "https://files.pythonhosted.org/packages/1b/9f/38ff9e57a2eade7bf9dfee5eae17f39fc0e998658050279cbb14d97d36d9/msgpack-1.1.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:6c15b7d74c939ebe620dd8e559384be806204d73b4f9356320632d783d1f7939", size = 84981 }, - { url = "https://files.pythonhosted.org/packages/8e/a9/3536e385167b88c2cc8f4424c49e28d49a6fc35206d4a8060f136e71f94c/msgpack-1.1.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:99e2cb7b9031568a2a5c73aa077180f93dd2e95b4f8d3b8e14a73ae94a9e667e", size = 411885 }, - { url = "https://files.pythonhosted.org/packages/2f/40/dc34d1a8d5f1e51fc64640b62b191684da52ca469da9cd74e84936ffa4a6/msgpack-1.1.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:180759d89a057eab503cf62eeec0aa61c4ea1200dee709f3a8e9397dbb3b6931", size = 419658 }, - { url = "https://files.pythonhosted.org/packages/3b/ef/2b92e286366500a09a67e03496ee8b8ba00562797a52f3c117aa2b29514b/msgpack-1.1.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:04fb995247a6e83830b62f0b07bf36540c213f6eac8e851166d8d86d83cbd014", size = 403290 }, - { url = "https://files.pythonhosted.org/packages/78/90/e0ea7990abea5764e4655b8177aa7c63cdfa89945b6e7641055800f6c16b/msgpack-1.1.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:8e22ab046fa7ede9e36eeb4cfad44d46450f37bb05d5ec482b02868f451c95e2", size = 415234 }, - { url = "https://files.pythonhosted.org/packages/72/4e/9390aed5db983a2310818cd7d3ec0aecad45e1f7007e0cda79c79507bb0d/msgpack-1.1.2-cp314-cp314-win32.whl", hash = "sha256:80a0ff7d4abf5fecb995fcf235d4064b9a9a8a40a3ab80999e6ac1e30b702717", size = 66391 }, - { url = "https://files.pythonhosted.org/packages/6e/f1/abd09c2ae91228c5f3998dbd7f41353def9eac64253de3c8105efa2082f7/msgpack-1.1.2-cp314-cp314-win_amd64.whl", hash = "sha256:9ade919fac6a3e7260b7f64cea89df6bec59104987cbea34d34a2fa15d74310b", size = 73787 }, - { url = "https://files.pythonhosted.org/packages/6a/b0/9d9f667ab48b16ad4115c1935d94023b82b3198064cb84a123e97f7466c1/msgpack-1.1.2-cp314-cp314-win_arm64.whl", hash = "sha256:59415c6076b1e30e563eb732e23b994a61c159cec44deaf584e5cc1dd662f2af", size = 66453 }, - { url = "https://files.pythonhosted.org/packages/16/67/93f80545eb1792b61a217fa7f06d5e5cb9e0055bed867f43e2b8e012e137/msgpack-1.1.2-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:897c478140877e5307760b0ea66e0932738879e7aa68144d9b78ea4c8302a84a", size = 85264 }, - { url = "https://files.pythonhosted.org/packages/87/1c/33c8a24959cf193966ef11a6f6a2995a65eb066bd681fd085afd519a57ce/msgpack-1.1.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a668204fa43e6d02f89dbe79a30b0d67238d9ec4c5bd8a940fc3a004a47b721b", size = 89076 }, - { url = "https://files.pythonhosted.org/packages/fc/6b/62e85ff7193663fbea5c0254ef32f0c77134b4059f8da89b958beb7696f3/msgpack-1.1.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5559d03930d3aa0f3aacb4c42c776af1a2ace2611871c84a75afe436695e6245", size = 435242 }, - { url = "https://files.pythonhosted.org/packages/c1/47/5c74ecb4cc277cf09f64e913947871682ffa82b3b93c8dad68083112f412/msgpack-1.1.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:70c5a7a9fea7f036b716191c29047374c10721c389c21e9ffafad04df8c52c90", size = 432509 }, - { url = "https://files.pythonhosted.org/packages/24/a4/e98ccdb56dc4e98c929a3f150de1799831c0a800583cde9fa022fa90602d/msgpack-1.1.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:f2cb069d8b981abc72b41aea1c580ce92d57c673ec61af4c500153a626cb9e20", size = 415957 }, - { url = "https://files.pythonhosted.org/packages/da/28/6951f7fb67bc0a4e184a6b38ab71a92d9ba58080b27a77d3e2fb0be5998f/msgpack-1.1.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:d62ce1f483f355f61adb5433ebfd8868c5f078d1a52d042b0a998682b4fa8c27", size = 422910 }, - { url = "https://files.pythonhosted.org/packages/f0/03/42106dcded51f0a0b5284d3ce30a671e7bd3f7318d122b2ead66ad289fed/msgpack-1.1.2-cp314-cp314t-win32.whl", hash = "sha256:1d1418482b1ee984625d88aa9585db570180c286d942da463533b238b98b812b", size = 75197 }, - { url = "https://files.pythonhosted.org/packages/15/86/d0071e94987f8db59d4eeb386ddc64d0bb9b10820a8d82bcd3e53eeb2da6/msgpack-1.1.2-cp314-cp314t-win_amd64.whl", hash = "sha256:5a46bf7e831d09470ad92dff02b8b1ac92175ca36b087f904a0519857c6be3ff", size = 85772 }, - { url = "https://files.pythonhosted.org/packages/81/f2/08ace4142eb281c12701fc3b93a10795e4d4dc7f753911d836675050f886/msgpack-1.1.2-cp314-cp314t-win_arm64.whl", hash = "sha256:d99ef64f349d5ec3293688e91486c5fdb925ed03807f64d98d205d2713c60b46", size = 70868 }, -] - -[[package]] -name = "pycparser" -version = "2.23" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/fe/cf/d2d3b9f5699fb1e4615c8e32ff220203e43b248e1dfcc6736ad9057731ca/pycparser-2.23.tar.gz", hash = "sha256:78816d4f24add8f10a06d6f05b4d424ad9e96cfebf68a4ddc99c65c0720d00c2", size = 173734 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a0/e3/59cd50310fc9b59512193629e1984c1f95e5c8ae6e5d8c69532ccc65a7fe/pycparser-2.23-py3-none-any.whl", hash = "sha256:e5c6e8d3fbad53479cab09ac03729e0a9faf2bee3db8208a550daf5af81a5934", size = 118140 }, -] - -[[package]] -name = "pydantic" -version = "2.12.5" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "annotated-types" }, - { name = "pydantic-core" }, - { name = "typing-extensions" }, - { name = "typing-inspection" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/69/44/36f1a6e523abc58ae5f928898e4aca2e0ea509b5aa6f6f392a5d882be928/pydantic-2.12.5.tar.gz", hash = "sha256:4d351024c75c0f085a9febbb665ce8c0c6ec5d30e903bdb6394b7ede26aebb49", size = 821591 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/5a/87/b70ad306ebb6f9b585f114d0ac2137d792b48be34d732d60e597c2f8465a/pydantic-2.12.5-py3-none-any.whl", hash = "sha256:e561593fccf61e8a20fc46dfc2dfe075b8be7d0188df33f221ad1f0139180f9d", size = 463580 }, -] - -[[package]] -name = "pydantic-core" -version = "2.41.5" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/71/70/23b021c950c2addd24ec408e9ab05d59b035b39d97cdc1130e1bce647bb6/pydantic_core-2.41.5.tar.gz", hash = "sha256:08daa51ea16ad373ffd5e7606252cc32f07bc72b28284b6bc9c6df804816476e", size = 460952 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/5f/5d/5f6c63eebb5afee93bcaae4ce9a898f3373ca23df3ccaef086d0233a35a7/pydantic_core-2.41.5-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:f41a7489d32336dbf2199c8c0a215390a751c5b014c2c1c5366e817202e9cdf7", size = 2110990 }, - { url = "https://files.pythonhosted.org/packages/aa/32/9c2e8ccb57c01111e0fd091f236c7b371c1bccea0fa85247ac55b1e2b6b6/pydantic_core-2.41.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:070259a8818988b9a84a449a2a7337c7f430a22acc0859c6b110aa7212a6d9c0", size = 1896003 }, - { url = "https://files.pythonhosted.org/packages/68/b8/a01b53cb0e59139fbc9e4fda3e9724ede8de279097179be4ff31f1abb65a/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e96cea19e34778f8d59fe40775a7a574d95816eb150850a85a7a4c8f4b94ac69", size = 1919200 }, - { url = "https://files.pythonhosted.org/packages/38/de/8c36b5198a29bdaade07b5985e80a233a5ac27137846f3bc2d3b40a47360/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ed2e99c456e3fadd05c991f8f437ef902e00eedf34320ba2b0842bd1c3ca3a75", size = 2052578 }, - { url = "https://files.pythonhosted.org/packages/00/b5/0e8e4b5b081eac6cb3dbb7e60a65907549a1ce035a724368c330112adfdd/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:65840751b72fbfd82c3c640cff9284545342a4f1eb1586ad0636955b261b0b05", size = 2208504 }, - { url = "https://files.pythonhosted.org/packages/77/56/87a61aad59c7c5b9dc8caad5a41a5545cba3810c3e828708b3d7404f6cef/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e536c98a7626a98feb2d3eaf75944ef6f3dbee447e1f841eae16f2f0a72d8ddc", size = 2335816 }, - { url = "https://files.pythonhosted.org/packages/0d/76/941cc9f73529988688a665a5c0ecff1112b3d95ab48f81db5f7606f522d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eceb81a8d74f9267ef4081e246ffd6d129da5d87e37a77c9bde550cb04870c1c", size = 2075366 }, - { url = "https://files.pythonhosted.org/packages/d3/43/ebef01f69baa07a482844faaa0a591bad1ef129253ffd0cdaa9d8a7f72d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d38548150c39b74aeeb0ce8ee1d8e82696f4a4e16ddc6de7b1d8823f7de4b9b5", size = 2171698 }, - { url = "https://files.pythonhosted.org/packages/b1/87/41f3202e4193e3bacfc2c065fab7706ebe81af46a83d3e27605029c1f5a6/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c23e27686783f60290e36827f9c626e63154b82b116d7fe9adba1fda36da706c", size = 2132603 }, - { url = "https://files.pythonhosted.org/packages/49/7d/4c00df99cb12070b6bccdef4a195255e6020a550d572768d92cc54dba91a/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:482c982f814460eabe1d3bb0adfdc583387bd4691ef00b90575ca0d2b6fe2294", size = 2329591 }, - { url = "https://files.pythonhosted.org/packages/cc/6a/ebf4b1d65d458f3cda6a7335d141305dfa19bdc61140a884d165a8a1bbc7/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:bfea2a5f0b4d8d43adf9d7b8bf019fb46fdd10a2e5cde477fbcb9d1fa08c68e1", size = 2319068 }, - { url = "https://files.pythonhosted.org/packages/49/3b/774f2b5cd4192d5ab75870ce4381fd89cf218af999515baf07e7206753f0/pydantic_core-2.41.5-cp312-cp312-win32.whl", hash = "sha256:b74557b16e390ec12dca509bce9264c3bbd128f8a2c376eaa68003d7f327276d", size = 1985908 }, - { url = "https://files.pythonhosted.org/packages/86/45/00173a033c801cacf67c190fef088789394feaf88a98a7035b0e40d53dc9/pydantic_core-2.41.5-cp312-cp312-win_amd64.whl", hash = "sha256:1962293292865bca8e54702b08a4f26da73adc83dd1fcf26fbc875b35d81c815", size = 2020145 }, - { url = "https://files.pythonhosted.org/packages/f9/22/91fbc821fa6d261b376a3f73809f907cec5ca6025642c463d3488aad22fb/pydantic_core-2.41.5-cp312-cp312-win_arm64.whl", hash = "sha256:1746d4a3d9a794cacae06a5eaaccb4b8643a131d45fbc9af23e353dc0a5ba5c3", size = 1976179 }, - { url = "https://files.pythonhosted.org/packages/87/06/8806241ff1f70d9939f9af039c6c35f2360cf16e93c2ca76f184e76b1564/pydantic_core-2.41.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:941103c9be18ac8daf7b7adca8228f8ed6bb7a1849020f643b3a14d15b1924d9", size = 2120403 }, - { url = "https://files.pythonhosted.org/packages/94/02/abfa0e0bda67faa65fef1c84971c7e45928e108fe24333c81f3bfe35d5f5/pydantic_core-2.41.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:112e305c3314f40c93998e567879e887a3160bb8689ef3d2c04b6cc62c33ac34", size = 1896206 }, - { url = "https://files.pythonhosted.org/packages/15/df/a4c740c0943e93e6500f9eb23f4ca7ec9bf71b19e608ae5b579678c8d02f/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cbaad15cb0c90aa221d43c00e77bb33c93e8d36e0bf74760cd00e732d10a6a0", size = 1919307 }, - { url = "https://files.pythonhosted.org/packages/9a/e3/6324802931ae1d123528988e0e86587c2072ac2e5394b4bc2bc34b61ff6e/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:03ca43e12fab6023fc79d28ca6b39b05f794ad08ec2feccc59a339b02f2b3d33", size = 2063258 }, - { url = "https://files.pythonhosted.org/packages/c9/d4/2230d7151d4957dd79c3044ea26346c148c98fbf0ee6ebd41056f2d62ab5/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dc799088c08fa04e43144b164feb0c13f9a0bc40503f8df3e9fde58a3c0c101e", size = 2214917 }, - { url = "https://files.pythonhosted.org/packages/e6/9f/eaac5df17a3672fef0081b6c1bb0b82b33ee89aa5cec0d7b05f52fd4a1fa/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:97aeba56665b4c3235a0e52b2c2f5ae9cd071b8a8310ad27bddb3f7fb30e9aa2", size = 2332186 }, - { url = "https://files.pythonhosted.org/packages/cf/4e/35a80cae583a37cf15604b44240e45c05e04e86f9cfd766623149297e971/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:406bf18d345822d6c21366031003612b9c77b3e29ffdb0f612367352aab7d586", size = 2073164 }, - { url = "https://files.pythonhosted.org/packages/bf/e3/f6e262673c6140dd3305d144d032f7bd5f7497d3871c1428521f19f9efa2/pydantic_core-2.41.5-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b93590ae81f7010dbe380cdeab6f515902ebcbefe0b9327cc4804d74e93ae69d", size = 2179146 }, - { url = "https://files.pythonhosted.org/packages/75/c7/20bd7fc05f0c6ea2056a4565c6f36f8968c0924f19b7d97bbfea55780e73/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:01a3d0ab748ee531f4ea6c3e48ad9dac84ddba4b0d82291f87248f2f9de8d740", size = 2137788 }, - { url = "https://files.pythonhosted.org/packages/3a/8d/34318ef985c45196e004bc46c6eab2eda437e744c124ef0dbe1ff2c9d06b/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:6561e94ba9dacc9c61bce40e2d6bdc3bfaa0259d3ff36ace3b1e6901936d2e3e", size = 2340133 }, - { url = "https://files.pythonhosted.org/packages/9c/59/013626bf8c78a5a5d9350d12e7697d3d4de951a75565496abd40ccd46bee/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:915c3d10f81bec3a74fbd4faebe8391013ba61e5a1a8d48c4455b923bdda7858", size = 2324852 }, - { url = "https://files.pythonhosted.org/packages/1a/d9/c248c103856f807ef70c18a4f986693a46a8ffe1602e5d361485da502d20/pydantic_core-2.41.5-cp313-cp313-win32.whl", hash = "sha256:650ae77860b45cfa6e2cdafc42618ceafab3a2d9a3811fcfbd3bbf8ac3c40d36", size = 1994679 }, - { url = "https://files.pythonhosted.org/packages/9e/8b/341991b158ddab181cff136acd2552c9f35bd30380422a639c0671e99a91/pydantic_core-2.41.5-cp313-cp313-win_amd64.whl", hash = "sha256:79ec52ec461e99e13791ec6508c722742ad745571f234ea6255bed38c6480f11", size = 2019766 }, - { url = "https://files.pythonhosted.org/packages/73/7d/f2f9db34af103bea3e09735bb40b021788a5e834c81eedb541991badf8f5/pydantic_core-2.41.5-cp313-cp313-win_arm64.whl", hash = "sha256:3f84d5c1b4ab906093bdc1ff10484838aca54ef08de4afa9de0f5f14d69639cd", size = 1981005 }, - { url = "https://files.pythonhosted.org/packages/ea/28/46b7c5c9635ae96ea0fbb779e271a38129df2550f763937659ee6c5dbc65/pydantic_core-2.41.5-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:3f37a19d7ebcdd20b96485056ba9e8b304e27d9904d233d7b1015db320e51f0a", size = 2119622 }, - { url = "https://files.pythonhosted.org/packages/74/1a/145646e5687e8d9a1e8d09acb278c8535ebe9e972e1f162ed338a622f193/pydantic_core-2.41.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1d1d9764366c73f996edd17abb6d9d7649a7eb690006ab6adbda117717099b14", size = 1891725 }, - { url = "https://files.pythonhosted.org/packages/23/04/e89c29e267b8060b40dca97bfc64a19b2a3cf99018167ea1677d96368273/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25e1c2af0fce638d5f1988b686f3b3ea8cd7de5f244ca147c777769e798a9cd1", size = 1915040 }, - { url = "https://files.pythonhosted.org/packages/84/a3/15a82ac7bd97992a82257f777b3583d3e84bdb06ba6858f745daa2ec8a85/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:506d766a8727beef16b7adaeb8ee6217c64fc813646b424d0804d67c16eddb66", size = 2063691 }, - { url = "https://files.pythonhosted.org/packages/74/9b/0046701313c6ef08c0c1cf0e028c67c770a4e1275ca73131563c5f2a310a/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4819fa52133c9aa3c387b3328f25c1facc356491e6135b459f1de698ff64d869", size = 2213897 }, - { url = "https://files.pythonhosted.org/packages/8a/cd/6bac76ecd1b27e75a95ca3a9a559c643b3afcd2dd62086d4b7a32a18b169/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2b761d210c9ea91feda40d25b4efe82a1707da2ef62901466a42492c028553a2", size = 2333302 }, - { url = "https://files.pythonhosted.org/packages/4c/d2/ef2074dc020dd6e109611a8be4449b98cd25e1b9b8a303c2f0fca2f2bcf7/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22f0fb8c1c583a3b6f24df2470833b40207e907b90c928cc8d3594b76f874375", size = 2064877 }, - { url = "https://files.pythonhosted.org/packages/18/66/e9db17a9a763d72f03de903883c057b2592c09509ccfe468187f2a2eef29/pydantic_core-2.41.5-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2782c870e99878c634505236d81e5443092fba820f0373997ff75f90f68cd553", size = 2180680 }, - { url = "https://files.pythonhosted.org/packages/d3/9e/3ce66cebb929f3ced22be85d4c2399b8e85b622db77dad36b73c5387f8f8/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:0177272f88ab8312479336e1d777f6b124537d47f2123f89cb37e0accea97f90", size = 2138960 }, - { url = "https://files.pythonhosted.org/packages/a6/62/205a998f4327d2079326b01abee48e502ea739d174f0a89295c481a2272e/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:63510af5e38f8955b8ee5687740d6ebf7c2a0886d15a6d65c32814613681bc07", size = 2339102 }, - { url = "https://files.pythonhosted.org/packages/3c/0d/f05e79471e889d74d3d88f5bd20d0ed189ad94c2423d81ff8d0000aab4ff/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:e56ba91f47764cc14f1daacd723e3e82d1a89d783f0f5afe9c364b8bb491ccdb", size = 2326039 }, - { url = "https://files.pythonhosted.org/packages/ec/e1/e08a6208bb100da7e0c4b288eed624a703f4d129bde2da475721a80cab32/pydantic_core-2.41.5-cp314-cp314-win32.whl", hash = "sha256:aec5cf2fd867b4ff45b9959f8b20ea3993fc93e63c7363fe6851424c8a7e7c23", size = 1995126 }, - { url = "https://files.pythonhosted.org/packages/48/5d/56ba7b24e9557f99c9237e29f5c09913c81eeb2f3217e40e922353668092/pydantic_core-2.41.5-cp314-cp314-win_amd64.whl", hash = "sha256:8e7c86f27c585ef37c35e56a96363ab8de4e549a95512445b85c96d3e2f7c1bf", size = 2015489 }, - { url = "https://files.pythonhosted.org/packages/4e/bb/f7a190991ec9e3e0ba22e4993d8755bbc4a32925c0b5b42775c03e8148f9/pydantic_core-2.41.5-cp314-cp314-win_arm64.whl", hash = "sha256:e672ba74fbc2dc8eea59fb6d4aed6845e6905fc2a8afe93175d94a83ba2a01a0", size = 1977288 }, - { url = "https://files.pythonhosted.org/packages/92/ed/77542d0c51538e32e15afe7899d79efce4b81eee631d99850edc2f5e9349/pydantic_core-2.41.5-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8566def80554c3faa0e65ac30ab0932b9e3a5cd7f8323764303d468e5c37595a", size = 2120255 }, - { url = "https://files.pythonhosted.org/packages/bb/3d/6913dde84d5be21e284439676168b28d8bbba5600d838b9dca99de0fad71/pydantic_core-2.41.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b80aa5095cd3109962a298ce14110ae16b8c1aece8b72f9dafe81cf597ad80b3", size = 1863760 }, - { url = "https://files.pythonhosted.org/packages/5a/f0/e5e6b99d4191da102f2b0eb9687aaa7f5bea5d9964071a84effc3e40f997/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3006c3dd9ba34b0c094c544c6006cc79e87d8612999f1a5d43b769b89181f23c", size = 1878092 }, - { url = "https://files.pythonhosted.org/packages/71/48/36fb760642d568925953bcc8116455513d6e34c4beaa37544118c36aba6d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:72f6c8b11857a856bcfa48c86f5368439f74453563f951e473514579d44aa612", size = 2053385 }, - { url = "https://files.pythonhosted.org/packages/20/25/92dc684dd8eb75a234bc1c764b4210cf2646479d54b47bf46061657292a8/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5cb1b2f9742240e4bb26b652a5aeb840aa4b417c7748b6f8387927bc6e45e40d", size = 2218832 }, - { url = "https://files.pythonhosted.org/packages/e2/09/f53e0b05023d3e30357d82eb35835d0f6340ca344720a4599cd663dca599/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd3d54f38609ff308209bd43acea66061494157703364ae40c951f83ba99a1a9", size = 2327585 }, - { url = "https://files.pythonhosted.org/packages/aa/4e/2ae1aa85d6af35a39b236b1b1641de73f5a6ac4d5a7509f77b814885760c/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ff4321e56e879ee8d2a879501c8e469414d948f4aba74a2d4593184eb326660", size = 2041078 }, - { url = "https://files.pythonhosted.org/packages/cd/13/2e215f17f0ef326fc72afe94776edb77525142c693767fc347ed6288728d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d0d2568a8c11bf8225044aa94409e21da0cb09dcdafe9ecd10250b2baad531a9", size = 2173914 }, - { url = "https://files.pythonhosted.org/packages/02/7a/f999a6dcbcd0e5660bc348a3991c8915ce6599f4f2c6ac22f01d7a10816c/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:a39455728aabd58ceabb03c90e12f71fd30fa69615760a075b9fec596456ccc3", size = 2129560 }, - { url = "https://files.pythonhosted.org/packages/3a/b1/6c990ac65e3b4c079a4fb9f5b05f5b013afa0f4ed6780a3dd236d2cbdc64/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:239edca560d05757817c13dc17c50766136d21f7cd0fac50295499ae24f90fdf", size = 2329244 }, - { url = "https://files.pythonhosted.org/packages/d9/02/3c562f3a51afd4d88fff8dffb1771b30cfdfd79befd9883ee094f5b6c0d8/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:2a5e06546e19f24c6a96a129142a75cee553cc018ffee48a460059b1185f4470", size = 2331955 }, - { url = "https://files.pythonhosted.org/packages/5c/96/5fb7d8c3c17bc8c62fdb031c47d77a1af698f1d7a406b0f79aaa1338f9ad/pydantic_core-2.41.5-cp314-cp314t-win32.whl", hash = "sha256:b4ececa40ac28afa90871c2cc2b9ffd2ff0bf749380fbdf57d165fd23da353aa", size = 1988906 }, - { url = "https://files.pythonhosted.org/packages/22/ed/182129d83032702912c2e2d8bbe33c036f342cc735737064668585dac28f/pydantic_core-2.41.5-cp314-cp314t-win_amd64.whl", hash = "sha256:80aa89cad80b32a912a65332f64a4450ed00966111b6615ca6816153d3585a8c", size = 1981607 }, - { url = "https://files.pythonhosted.org/packages/9f/ed/068e41660b832bb0b1aa5b58011dea2a3fe0ba7861ff38c4d4904c1c1a99/pydantic_core-2.41.5-cp314-cp314t-win_arm64.whl", hash = "sha256:35b44f37a3199f771c3eaa53051bc8a70cd7b54f333531c59e29fd4db5d15008", size = 1974769 }, -] - -[[package]] -name = "pydantic-settings" -version = "2.12.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pydantic" }, - { name = "python-dotenv" }, - { name = "typing-inspection" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/43/4b/ac7e0aae12027748076d72a8764ff1c9d82ca75a7a52622e67ed3f765c54/pydantic_settings-2.12.0.tar.gz", hash = "sha256:005538ef951e3c2a68e1c08b292b5f2e71490def8589d4221b95dab00dafcfd0", size = 194184 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c1/60/5d4751ba3f4a40a6891f24eec885f51afd78d208498268c734e256fb13c4/pydantic_settings-2.12.0-py3-none-any.whl", hash = "sha256:fddb9fd99a5b18da837b29710391e945b1e30c135477f484084ee513adb93809", size = 51880 }, -] - -[[package]] -name = "pyjwt" -version = "2.10.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e7/46/bd74733ff231675599650d3e47f361794b22ef3e3770998dda30d3b63726/pyjwt-2.10.1.tar.gz", hash = "sha256:3cc5772eb20009233caf06e9d8a0577824723b44e6648ee0a2aedb6cf9381953", size = 87785 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/61/ad/689f02752eeec26aed679477e80e632ef1b682313be70793d798c1d5fc8f/PyJWT-2.10.1-py3-none-any.whl", hash = "sha256:dcdd193e30abefd5debf142f9adfcdd2b58004e644f25406ffaebd50bd98dacb", size = 22997 }, -] - -[package.optional-dependencies] -crypto = [ - { name = "cryptography" }, -] - -[[package]] -name = "python-dotenv" -version = "1.2.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f0/26/19cadc79a718c5edbec86fd4919a6b6d3f681039a2f6d66d14be94e75fb9/python_dotenv-1.2.1.tar.gz", hash = "sha256:42667e897e16ab0d66954af0e60a9caa94f0fd4ecf3aaf6d2d260eec1aa36ad6", size = 44221 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/14/1b/a298b06749107c305e1fe0f814c6c74aea7b2f1e10989cb30f544a1b3253/python_dotenv-1.2.1-py3-none-any.whl", hash = "sha256:b81ee9561e9ca4004139c6cbba3a238c32b03e4894671e181b671e8cb8425d61", size = 21230 }, -] - -[[package]] -name = "python-multipart" -version = "0.0.21" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/78/96/804520d0850c7db98e5ccb70282e29208723f0964e88ffd9d0da2f52ea09/python_multipart-0.0.21.tar.gz", hash = "sha256:7137ebd4d3bbf70ea1622998f902b97a29434a9e8dc40eb203bbcf7c2a2cba92", size = 37196 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/aa/76/03af049af4dcee5d27442f71b6924f01f3efb5d2bd34f23fcd563f2cc5f5/python_multipart-0.0.21-py3-none-any.whl", hash = "sha256:cf7a6713e01c87aa35387f4774e812c4361150938d20d232800f75ffcf266090", size = 24541 }, -] - -[[package]] -name = "pywin32" -version = "311" -source = { registry = "https://pypi.org/simple" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e7/ab/01ea1943d4eba0f850c3c61e78e8dd59757ff815ff3ccd0a84de5f541f42/pywin32-311-cp312-cp312-win32.whl", hash = "sha256:750ec6e621af2b948540032557b10a2d43b0cee2ae9758c54154d711cc852d31", size = 8706543 }, - { url = "https://files.pythonhosted.org/packages/d1/a8/a0e8d07d4d051ec7502cd58b291ec98dcc0c3fff027caad0470b72cfcc2f/pywin32-311-cp312-cp312-win_amd64.whl", hash = "sha256:b8c095edad5c211ff31c05223658e71bf7116daa0ecf3ad85f3201ea3190d067", size = 9495040 }, - { url = "https://files.pythonhosted.org/packages/ba/3a/2ae996277b4b50f17d61f0603efd8253cb2d79cc7ae159468007b586396d/pywin32-311-cp312-cp312-win_arm64.whl", hash = "sha256:e286f46a9a39c4a18b319c28f59b61de793654af2f395c102b4f819e584b5852", size = 8710102 }, - { url = "https://files.pythonhosted.org/packages/a5/be/3fd5de0979fcb3994bfee0d65ed8ca9506a8a1260651b86174f6a86f52b3/pywin32-311-cp313-cp313-win32.whl", hash = "sha256:f95ba5a847cba10dd8c4d8fefa9f2a6cf283b8b88ed6178fa8a6c1ab16054d0d", size = 8705700 }, - { url = "https://files.pythonhosted.org/packages/e3/28/e0a1909523c6890208295a29e05c2adb2126364e289826c0a8bc7297bd5c/pywin32-311-cp313-cp313-win_amd64.whl", hash = "sha256:718a38f7e5b058e76aee1c56ddd06908116d35147e133427e59a3983f703a20d", size = 9494700 }, - { url = "https://files.pythonhosted.org/packages/04/bf/90339ac0f55726dce7d794e6d79a18a91265bdf3aa70b6b9ca52f35e022a/pywin32-311-cp313-cp313-win_arm64.whl", hash = "sha256:7b4075d959648406202d92a2310cb990fea19b535c7f4a78d3f5e10b926eeb8a", size = 8709318 }, - { url = "https://files.pythonhosted.org/packages/c9/31/097f2e132c4f16d99a22bfb777e0fd88bd8e1c634304e102f313af69ace5/pywin32-311-cp314-cp314-win32.whl", hash = "sha256:b7a2c10b93f8986666d0c803ee19b5990885872a7de910fc460f9b0c2fbf92ee", size = 8840714 }, - { url = "https://files.pythonhosted.org/packages/90/4b/07c77d8ba0e01349358082713400435347df8426208171ce297da32c313d/pywin32-311-cp314-cp314-win_amd64.whl", hash = "sha256:3aca44c046bd2ed8c90de9cb8427f581c479e594e99b5c0bb19b29c10fd6cb87", size = 9656800 }, - { url = "https://files.pythonhosted.org/packages/c0/d2/21af5c535501a7233e734b8af901574572da66fcc254cb35d0609c9080dd/pywin32-311-cp314-cp314-win_arm64.whl", hash = "sha256:a508e2d9025764a8270f93111a970e1d0fbfc33f4153b388bb649b7eec4f9b42", size = 8932540 }, -] - -[[package]] -name = "pyyaml" -version = "6.0.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063 }, - { url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973 }, - { url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116 }, - { url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011 }, - { url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870 }, - { url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089 }, - { url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181 }, - { url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658 }, - { url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003 }, - { url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344 }, - { url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669 }, - { url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252 }, - { url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081 }, - { url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159 }, - { url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626 }, - { url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613 }, - { url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115 }, - { url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427 }, - { url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090 }, - { url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246 }, - { url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814 }, - { url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809 }, - { url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454 }, - { url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355 }, - { url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175 }, - { url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228 }, - { url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194 }, - { url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429 }, - { url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912 }, - { url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108 }, - { url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641 }, - { url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901 }, - { url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132 }, - { url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261 }, - { url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272 }, - { url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923 }, - { url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062 }, - { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341 }, -] - -[[package]] -name = "referencing" -version = "0.37.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "attrs" }, - { name = "rpds-py" }, - { name = "typing-extensions", marker = "python_full_version < '3.13'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/22/f5/df4e9027acead3ecc63e50fe1e36aca1523e1719559c499951bb4b53188f/referencing-0.37.0.tar.gz", hash = "sha256:44aefc3142c5b842538163acb373e24cce6632bd54bdb01b21ad5863489f50d8", size = 78036 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/2c/58/ca301544e1fa93ed4f80d724bf5b194f6e4b945841c5bfd555878eea9fcb/referencing-0.37.0-py3-none-any.whl", hash = "sha256:381329a9f99628c9069361716891d34ad94af76e461dcb0335825aecc7692231", size = 26766 }, -] - -[[package]] -name = "rpds-py" -version = "0.30.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/20/af/3f2f423103f1113b36230496629986e0ef7e199d2aa8392452b484b38ced/rpds_py-0.30.0.tar.gz", hash = "sha256:dd8ff7cf90014af0c0f787eea34794ebf6415242ee1d6fa91eaba725cc441e84", size = 69469 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/03/e7/98a2f4ac921d82f33e03f3835f5bf3a4a40aa1bfdc57975e74a97b2b4bdd/rpds_py-0.30.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a161f20d9a43006833cd7068375a94d035714d73a172b681d8881820600abfad", size = 375086 }, - { url = "https://files.pythonhosted.org/packages/4d/a1/bca7fd3d452b272e13335db8d6b0b3ecde0f90ad6f16f3328c6fb150c889/rpds_py-0.30.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6abc8880d9d036ecaafe709079969f56e876fcf107f7a8e9920ba6d5a3878d05", size = 359053 }, - { url = "https://files.pythonhosted.org/packages/65/1c/ae157e83a6357eceff62ba7e52113e3ec4834a84cfe07fa4b0757a7d105f/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca28829ae5f5d569bb62a79512c842a03a12576375d5ece7d2cadf8abe96ec28", size = 390763 }, - { url = "https://files.pythonhosted.org/packages/d4/36/eb2eb8515e2ad24c0bd43c3ee9cd74c33f7ca6430755ccdb240fd3144c44/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a1010ed9524c73b94d15919ca4d41d8780980e1765babf85f9a2f90d247153dd", size = 408951 }, - { url = "https://files.pythonhosted.org/packages/d6/65/ad8dc1784a331fabbd740ef6f71ce2198c7ed0890dab595adb9ea2d775a1/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f8d1736cfb49381ba528cd5baa46f82fdc65c06e843dab24dd70b63d09121b3f", size = 514622 }, - { url = "https://files.pythonhosted.org/packages/63/8e/0cfa7ae158e15e143fe03993b5bcd743a59f541f5952e1546b1ac1b5fd45/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d948b135c4693daff7bc2dcfc4ec57237a29bd37e60c2fabf5aff2bbacf3e2f1", size = 414492 }, - { url = "https://files.pythonhosted.org/packages/60/1b/6f8f29f3f995c7ffdde46a626ddccd7c63aefc0efae881dc13b6e5d5bb16/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47f236970bccb2233267d89173d3ad2703cd36a0e2a6e92d0560d333871a3d23", size = 394080 }, - { url = "https://files.pythonhosted.org/packages/6d/d5/a266341051a7a3ca2f4b750a3aa4abc986378431fc2da508c5034d081b70/rpds_py-0.30.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:2e6ecb5a5bcacf59c3f912155044479af1d0b6681280048b338b28e364aca1f6", size = 408680 }, - { url = "https://files.pythonhosted.org/packages/10/3b/71b725851df9ab7a7a4e33cf36d241933da66040d195a84781f49c50490c/rpds_py-0.30.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a8fa71a2e078c527c3e9dc9fc5a98c9db40bcc8a92b4e8858e36d329f8684b51", size = 423589 }, - { url = "https://files.pythonhosted.org/packages/00/2b/e59e58c544dc9bd8bd8384ecdb8ea91f6727f0e37a7131baeff8d6f51661/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:73c67f2db7bc334e518d097c6d1e6fed021bbc9b7d678d6cc433478365d1d5f5", size = 573289 }, - { url = "https://files.pythonhosted.org/packages/da/3e/a18e6f5b460893172a7d6a680e86d3b6bc87a54c1f0b03446a3c8c7b588f/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5ba103fb455be00f3b1c2076c9d4264bfcb037c976167a6047ed82f23153f02e", size = 599737 }, - { url = "https://files.pythonhosted.org/packages/5c/e2/714694e4b87b85a18e2c243614974413c60aa107fd815b8cbc42b873d1d7/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7cee9c752c0364588353e627da8a7e808a66873672bcb5f52890c33fd965b394", size = 563120 }, - { url = "https://files.pythonhosted.org/packages/6f/ab/d5d5e3bcedb0a77f4f613706b750e50a5a3ba1c15ccd3665ecc636c968fd/rpds_py-0.30.0-cp312-cp312-win32.whl", hash = "sha256:1ab5b83dbcf55acc8b08fc62b796ef672c457b17dbd7820a11d6c52c06839bdf", size = 223782 }, - { url = "https://files.pythonhosted.org/packages/39/3b/f786af9957306fdc38a74cef405b7b93180f481fb48453a114bb6465744a/rpds_py-0.30.0-cp312-cp312-win_amd64.whl", hash = "sha256:a090322ca841abd453d43456ac34db46e8b05fd9b3b4ac0c78bcde8b089f959b", size = 240463 }, - { url = "https://files.pythonhosted.org/packages/f3/d2/b91dc748126c1559042cfe41990deb92c4ee3e2b415f6b5234969ffaf0cc/rpds_py-0.30.0-cp312-cp312-win_arm64.whl", hash = "sha256:669b1805bd639dd2989b281be2cfd951c6121b65e729d9b843e9639ef1fd555e", size = 230868 }, - { url = "https://files.pythonhosted.org/packages/ed/dc/d61221eb88ff410de3c49143407f6f3147acf2538c86f2ab7ce65ae7d5f9/rpds_py-0.30.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:f83424d738204d9770830d35290ff3273fbb02b41f919870479fab14b9d303b2", size = 374887 }, - { url = "https://files.pythonhosted.org/packages/fd/32/55fb50ae104061dbc564ef15cc43c013dc4a9f4527a1f4d99baddf56fe5f/rpds_py-0.30.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e7536cd91353c5273434b4e003cbda89034d67e7710eab8761fd918ec6c69cf8", size = 358904 }, - { url = "https://files.pythonhosted.org/packages/58/70/faed8186300e3b9bdd138d0273109784eea2396c68458ed580f885dfe7ad/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2771c6c15973347f50fece41fc447c054b7ac2ae0502388ce3b6738cd366e3d4", size = 389945 }, - { url = "https://files.pythonhosted.org/packages/bd/a8/073cac3ed2c6387df38f71296d002ab43496a96b92c823e76f46b8af0543/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0a59119fc6e3f460315fe9d08149f8102aa322299deaa5cab5b40092345c2136", size = 407783 }, - { url = "https://files.pythonhosted.org/packages/77/57/5999eb8c58671f1c11eba084115e77a8899d6e694d2a18f69f0ba471ec8b/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:76fec018282b4ead0364022e3c54b60bf368b9d926877957a8624b58419169b7", size = 515021 }, - { url = "https://files.pythonhosted.org/packages/e0/af/5ab4833eadc36c0a8ed2bc5c0de0493c04f6c06de223170bd0798ff98ced/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:692bef75a5525db97318e8cd061542b5a79812d711ea03dbc1f6f8dbb0c5f0d2", size = 414589 }, - { url = "https://files.pythonhosted.org/packages/b7/de/f7192e12b21b9e9a68a6d0f249b4af3fdcdff8418be0767a627564afa1f1/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9027da1ce107104c50c81383cae773ef5c24d296dd11c99e2629dbd7967a20c6", size = 394025 }, - { url = "https://files.pythonhosted.org/packages/91/c4/fc70cd0249496493500e7cc2de87504f5aa6509de1e88623431fec76d4b6/rpds_py-0.30.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:9cf69cdda1f5968a30a359aba2f7f9aa648a9ce4b580d6826437f2b291cfc86e", size = 408895 }, - { url = "https://files.pythonhosted.org/packages/58/95/d9275b05ab96556fefff73a385813eb66032e4c99f411d0795372d9abcea/rpds_py-0.30.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a4796a717bf12b9da9d3ad002519a86063dcac8988b030e405704ef7d74d2d9d", size = 422799 }, - { url = "https://files.pythonhosted.org/packages/06/c1/3088fc04b6624eb12a57eb814f0d4997a44b0d208d6cace713033ff1a6ba/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5d4c2aa7c50ad4728a094ebd5eb46c452e9cb7edbfdb18f9e1221f597a73e1e7", size = 572731 }, - { url = "https://files.pythonhosted.org/packages/d8/42/c612a833183b39774e8ac8fecae81263a68b9583ee343db33ab571a7ce55/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ba81a9203d07805435eb06f536d95a266c21e5b2dfbf6517748ca40c98d19e31", size = 599027 }, - { url = "https://files.pythonhosted.org/packages/5f/60/525a50f45b01d70005403ae0e25f43c0384369ad24ffe46e8d9068b50086/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:945dccface01af02675628334f7cf49c2af4c1c904748efc5cf7bbdf0b579f95", size = 563020 }, - { url = "https://files.pythonhosted.org/packages/0b/5d/47c4655e9bcd5ca907148535c10e7d489044243cc9941c16ed7cd53be91d/rpds_py-0.30.0-cp313-cp313-win32.whl", hash = "sha256:b40fb160a2db369a194cb27943582b38f79fc4887291417685f3ad693c5a1d5d", size = 223139 }, - { url = "https://files.pythonhosted.org/packages/f2/e1/485132437d20aa4d3e1d8b3fb5a5e65aa8139f1e097080c2a8443201742c/rpds_py-0.30.0-cp313-cp313-win_amd64.whl", hash = "sha256:806f36b1b605e2d6a72716f321f20036b9489d29c51c91f4dd29a3e3afb73b15", size = 240224 }, - { url = "https://files.pythonhosted.org/packages/24/95/ffd128ed1146a153d928617b0ef673960130be0009c77d8fbf0abe306713/rpds_py-0.30.0-cp313-cp313-win_arm64.whl", hash = "sha256:d96c2086587c7c30d44f31f42eae4eac89b60dabbac18c7669be3700f13c3ce1", size = 230645 }, - { url = "https://files.pythonhosted.org/packages/ff/1b/b10de890a0def2a319a2626334a7f0ae388215eb60914dbac8a3bae54435/rpds_py-0.30.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:eb0b93f2e5c2189ee831ee43f156ed34e2a89a78a66b98cadad955972548be5a", size = 364443 }, - { url = "https://files.pythonhosted.org/packages/0d/bf/27e39f5971dc4f305a4fb9c672ca06f290f7c4e261c568f3dea16a410d47/rpds_py-0.30.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:922e10f31f303c7c920da8981051ff6d8c1a56207dbdf330d9047f6d30b70e5e", size = 353375 }, - { url = "https://files.pythonhosted.org/packages/40/58/442ada3bba6e8e6615fc00483135c14a7538d2ffac30e2d933ccf6852232/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cdc62c8286ba9bf7f47befdcea13ea0e26bf294bda99758fd90535cbaf408000", size = 383850 }, - { url = "https://files.pythonhosted.org/packages/14/14/f59b0127409a33c6ef6f5c1ebd5ad8e32d7861c9c7adfa9a624fc3889f6c/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:47f9a91efc418b54fb8190a6b4aa7813a23fb79c51f4bb84e418f5476c38b8db", size = 392812 }, - { url = "https://files.pythonhosted.org/packages/b3/66/e0be3e162ac299b3a22527e8913767d869e6cc75c46bd844aa43fb81ab62/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1f3587eb9b17f3789ad50824084fa6f81921bbf9a795826570bda82cb3ed91f2", size = 517841 }, - { url = "https://files.pythonhosted.org/packages/3d/55/fa3b9cf31d0c963ecf1ba777f7cf4b2a2c976795ac430d24a1f43d25a6ba/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:39c02563fc592411c2c61d26b6c5fe1e51eaa44a75aa2c8735ca88b0d9599daa", size = 408149 }, - { url = "https://files.pythonhosted.org/packages/60/ca/780cf3b1a32b18c0f05c441958d3758f02544f1d613abf9488cd78876378/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:51a1234d8febafdfd33a42d97da7a43f5dcb120c1060e352a3fbc0c6d36e2083", size = 383843 }, - { url = "https://files.pythonhosted.org/packages/82/86/d5f2e04f2aa6247c613da0c1dd87fcd08fa17107e858193566048a1e2f0a/rpds_py-0.30.0-cp313-cp313t-manylinux_2_31_riscv64.whl", hash = "sha256:eb2c4071ab598733724c08221091e8d80e89064cd472819285a9ab0f24bcedb9", size = 396507 }, - { url = "https://files.pythonhosted.org/packages/4b/9a/453255d2f769fe44e07ea9785c8347edaf867f7026872e76c1ad9f7bed92/rpds_py-0.30.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6bdfdb946967d816e6adf9a3d8201bfad269c67efe6cefd7093ef959683c8de0", size = 414949 }, - { url = "https://files.pythonhosted.org/packages/a3/31/622a86cdc0c45d6df0e9ccb6becdba5074735e7033c20e401a6d9d0e2ca0/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c77afbd5f5250bf27bf516c7c4a016813eb2d3e116139aed0096940c5982da94", size = 565790 }, - { url = "https://files.pythonhosted.org/packages/1c/5d/15bbf0fb4a3f58a3b1c67855ec1efcc4ceaef4e86644665fff03e1b66d8d/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:61046904275472a76c8c90c9ccee9013d70a6d0f73eecefd38c1ae7c39045a08", size = 590217 }, - { url = "https://files.pythonhosted.org/packages/6d/61/21b8c41f68e60c8cc3b2e25644f0e3681926020f11d06ab0b78e3c6bbff1/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4c5f36a861bc4b7da6516dbdf302c55313afa09b81931e8280361a4f6c9a2d27", size = 555806 }, - { url = "https://files.pythonhosted.org/packages/f9/39/7e067bb06c31de48de3eb200f9fc7c58982a4d3db44b07e73963e10d3be9/rpds_py-0.30.0-cp313-cp313t-win32.whl", hash = "sha256:3d4a69de7a3e50ffc214ae16d79d8fbb0922972da0356dcf4d0fdca2878559c6", size = 211341 }, - { url = "https://files.pythonhosted.org/packages/0a/4d/222ef0b46443cf4cf46764d9c630f3fe4abaa7245be9417e56e9f52b8f65/rpds_py-0.30.0-cp313-cp313t-win_amd64.whl", hash = "sha256:f14fc5df50a716f7ece6a80b6c78bb35ea2ca47c499e422aa4463455dd96d56d", size = 225768 }, - { url = "https://files.pythonhosted.org/packages/86/81/dad16382ebbd3d0e0328776d8fd7ca94220e4fa0798d1dc5e7da48cb3201/rpds_py-0.30.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:68f19c879420aa08f61203801423f6cd5ac5f0ac4ac82a2368a9fcd6a9a075e0", size = 362099 }, - { url = "https://files.pythonhosted.org/packages/2b/60/19f7884db5d5603edf3c6bce35408f45ad3e97e10007df0e17dd57af18f8/rpds_py-0.30.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ec7c4490c672c1a0389d319b3a9cfcd098dcdc4783991553c332a15acf7249be", size = 353192 }, - { url = "https://files.pythonhosted.org/packages/bf/c4/76eb0e1e72d1a9c4703c69607cec123c29028bff28ce41588792417098ac/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f251c812357a3fed308d684a5079ddfb9d933860fc6de89f2b7ab00da481e65f", size = 384080 }, - { url = "https://files.pythonhosted.org/packages/72/87/87ea665e92f3298d1b26d78814721dc39ed8d2c74b86e83348d6b48a6f31/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ac98b175585ecf4c0348fd7b29c3864bda53b805c773cbf7bfdaffc8070c976f", size = 394841 }, - { url = "https://files.pythonhosted.org/packages/77/ad/7783a89ca0587c15dcbf139b4a8364a872a25f861bdb88ed99f9b0dec985/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3e62880792319dbeb7eb866547f2e35973289e7d5696c6e295476448f5b63c87", size = 516670 }, - { url = "https://files.pythonhosted.org/packages/5b/3c/2882bdac942bd2172f3da574eab16f309ae10a3925644e969536553cb4ee/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4e7fc54e0900ab35d041b0601431b0a0eb495f0851a0639b6ef90f7741b39a18", size = 408005 }, - { url = "https://files.pythonhosted.org/packages/ce/81/9a91c0111ce1758c92516a3e44776920b579d9a7c09b2b06b642d4de3f0f/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47e77dc9822d3ad616c3d5759ea5631a75e5809d5a28707744ef79d7a1bcfcad", size = 382112 }, - { url = "https://files.pythonhosted.org/packages/cf/8e/1da49d4a107027e5fbc64daeab96a0706361a2918da10cb41769244b805d/rpds_py-0.30.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:b4dc1a6ff022ff85ecafef7979a2c6eb423430e05f1165d6688234e62ba99a07", size = 399049 }, - { url = "https://files.pythonhosted.org/packages/df/5a/7ee239b1aa48a127570ec03becbb29c9d5a9eb092febbd1699d567cae859/rpds_py-0.30.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4559c972db3a360808309e06a74628b95eaccbf961c335c8fe0d590cf587456f", size = 415661 }, - { url = "https://files.pythonhosted.org/packages/70/ea/caa143cf6b772f823bc7929a45da1fa83569ee49b11d18d0ada7f5ee6fd6/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:0ed177ed9bded28f8deb6ab40c183cd1192aa0de40c12f38be4d59cd33cb5c65", size = 565606 }, - { url = "https://files.pythonhosted.org/packages/64/91/ac20ba2d69303f961ad8cf55bf7dbdb4763f627291ba3d0d7d67333cced9/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ad1fa8db769b76ea911cb4e10f049d80bf518c104f15b3edb2371cc65375c46f", size = 591126 }, - { url = "https://files.pythonhosted.org/packages/21/20/7ff5f3c8b00c8a95f75985128c26ba44503fb35b8e0259d812766ea966c7/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:46e83c697b1f1c72b50e5ee5adb4353eef7406fb3f2043d64c33f20ad1c2fc53", size = 553371 }, - { url = "https://files.pythonhosted.org/packages/72/c7/81dadd7b27c8ee391c132a6b192111ca58d866577ce2d9b0ca157552cce0/rpds_py-0.30.0-cp314-cp314-win32.whl", hash = "sha256:ee454b2a007d57363c2dfd5b6ca4a5d7e2c518938f8ed3b706e37e5d470801ed", size = 215298 }, - { url = "https://files.pythonhosted.org/packages/3e/d2/1aaac33287e8cfb07aab2e6b8ac1deca62f6f65411344f1433c55e6f3eb8/rpds_py-0.30.0-cp314-cp314-win_amd64.whl", hash = "sha256:95f0802447ac2d10bcc69f6dc28fe95fdf17940367b21d34e34c737870758950", size = 228604 }, - { url = "https://files.pythonhosted.org/packages/e8/95/ab005315818cc519ad074cb7784dae60d939163108bd2b394e60dc7b5461/rpds_py-0.30.0-cp314-cp314-win_arm64.whl", hash = "sha256:613aa4771c99f03346e54c3f038e4cc574ac09a3ddfb0e8878487335e96dead6", size = 222391 }, - { url = "https://files.pythonhosted.org/packages/9e/68/154fe0194d83b973cdedcdcc88947a2752411165930182ae41d983dcefa6/rpds_py-0.30.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:7e6ecfcb62edfd632e56983964e6884851786443739dbfe3582947e87274f7cb", size = 364868 }, - { url = "https://files.pythonhosted.org/packages/83/69/8bbc8b07ec854d92a8b75668c24d2abcb1719ebf890f5604c61c9369a16f/rpds_py-0.30.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a1d0bc22a7cdc173fedebb73ef81e07faef93692b8c1ad3733b67e31e1b6e1b8", size = 353747 }, - { url = "https://files.pythonhosted.org/packages/ab/00/ba2e50183dbd9abcce9497fa5149c62b4ff3e22d338a30d690f9af970561/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0d08f00679177226c4cb8c5265012eea897c8ca3b93f429e546600c971bcbae7", size = 383795 }, - { url = "https://files.pythonhosted.org/packages/05/6f/86f0272b84926bcb0e4c972262f54223e8ecc556b3224d281e6598fc9268/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5965af57d5848192c13534f90f9dd16464f3c37aaf166cc1da1cae1fd5a34898", size = 393330 }, - { url = "https://files.pythonhosted.org/packages/cb/e9/0e02bb2e6dc63d212641da45df2b0bf29699d01715913e0d0f017ee29438/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9a4e86e34e9ab6b667c27f3211ca48f73dba7cd3d90f8d5b11be56e5dbc3fb4e", size = 518194 }, - { url = "https://files.pythonhosted.org/packages/ee/ca/be7bca14cf21513bdf9c0606aba17d1f389ea2b6987035eb4f62bd923f25/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e5d3e6b26f2c785d65cc25ef1e5267ccbe1b069c5c21b8cc724efee290554419", size = 408340 }, - { url = "https://files.pythonhosted.org/packages/c2/c7/736e00ebf39ed81d75544c0da6ef7b0998f8201b369acf842f9a90dc8fce/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:626a7433c34566535b6e56a1b39a7b17ba961e97ce3b80ec62e6f1312c025551", size = 383765 }, - { url = "https://files.pythonhosted.org/packages/4a/3f/da50dfde9956aaf365c4adc9533b100008ed31aea635f2b8d7b627e25b49/rpds_py-0.30.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:acd7eb3f4471577b9b5a41baf02a978e8bdeb08b4b355273994f8b87032000a8", size = 396834 }, - { url = "https://files.pythonhosted.org/packages/4e/00/34bcc2565b6020eab2623349efbdec810676ad571995911f1abdae62a3a0/rpds_py-0.30.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fe5fa731a1fa8a0a56b0977413f8cacac1768dad38d16b3a296712709476fbd5", size = 415470 }, - { url = "https://files.pythonhosted.org/packages/8c/28/882e72b5b3e6f718d5453bd4d0d9cf8df36fddeb4ddbbab17869d5868616/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:74a3243a411126362712ee1524dfc90c650a503502f135d54d1b352bd01f2404", size = 565630 }, - { url = "https://files.pythonhosted.org/packages/3b/97/04a65539c17692de5b85c6e293520fd01317fd878ea1995f0367d4532fb1/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:3e8eeb0544f2eb0d2581774be4c3410356eba189529a6b3e36bbbf9696175856", size = 591148 }, - { url = "https://files.pythonhosted.org/packages/85/70/92482ccffb96f5441aab93e26c4d66489eb599efdcf96fad90c14bbfb976/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:dbd936cde57abfee19ab3213cf9c26be06d60750e60a8e4dd85d1ab12c8b1f40", size = 556030 }, - { url = "https://files.pythonhosted.org/packages/20/53/7c7e784abfa500a2b6b583b147ee4bb5a2b3747a9166bab52fec4b5b5e7d/rpds_py-0.30.0-cp314-cp314t-win32.whl", hash = "sha256:dc824125c72246d924f7f796b4f63c1e9dc810c7d9e2355864b3c3a73d59ade0", size = 211570 }, - { url = "https://files.pythonhosted.org/packages/d0/02/fa464cdfbe6b26e0600b62c528b72d8608f5cc49f96b8d6e38c95d60c676/rpds_py-0.30.0-cp314-cp314t-win_amd64.whl", hash = "sha256:27f4b0e92de5bfbc6f86e43959e6edd1425c33b5e69aab0984a72047f2bcf1e3", size = 226532 }, -] - -[[package]] -name = "smmap" -version = "5.0.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/44/cd/a040c4b3119bbe532e5b0732286f805445375489fceaec1f48306068ee3b/smmap-5.0.2.tar.gz", hash = "sha256:26ea65a03958fa0c8a1c7e8c7a58fdc77221b8910f6be2131affade476898ad5", size = 22329 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/04/be/d09147ad1ec7934636ad912901c5fd7667e1c858e19d355237db0d0cd5e4/smmap-5.0.2-py3-none-any.whl", hash = "sha256:b30115f0def7d7531d22a0fb6502488d879e75b260a9db4d0819cfb25403af5e", size = 24303 }, -] - -[[package]] -name = "sse-starlette" -version = "3.1.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "anyio" }, - { name = "starlette" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/62/08/8f554b0e5bad3e4e880521a1686d96c05198471eed860b0eb89b57ea3636/sse_starlette-3.1.1.tar.gz", hash = "sha256:bffa531420c1793ab224f63648c059bcadc412bf9fdb1301ac8de1cf9a67b7fb", size = 24306 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e3/31/4c281581a0f8de137b710a07f65518b34bcf333b201cfa06cfda9af05f8a/sse_starlette-3.1.1-py3-none-any.whl", hash = "sha256:bb38f71ae74cfd86b529907a9fda5632195dfa6ae120f214ea4c890c7ee9d436", size = 12442 }, -] - -[[package]] -name = "starlette" -version = "0.50.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "anyio" }, - { name = "typing-extensions", marker = "python_full_version < '3.13'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/ba/b8/73a0e6a6e079a9d9cfa64113d771e421640b6f679a52eeb9b32f72d871a1/starlette-0.50.0.tar.gz", hash = "sha256:a2a17b22203254bcbc2e1f926d2d55f3f9497f769416b3190768befe598fa3ca", size = 2646985 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d9/52/1064f510b141bd54025f9b55105e26d1fa970b9be67ad766380a3c9b74b0/starlette-0.50.0-py3-none-any.whl", hash = "sha256:9e5391843ec9b6e472eed1365a78c8098cfceb7a74bfd4d6b1c0c0095efb3bca", size = 74033 }, -] - -[[package]] -name = "typing-extensions" -version = "4.15.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614 }, -] - -[[package]] -name = "typing-inspection" -version = "0.4.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611 }, -] - -[[package]] -name = "uvicorn" -version = "0.40.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "click" }, - { name = "h11" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/c3/d1/8f3c683c9561a4e6689dd3b1d345c815f10f86acd044ee1fb9a4dcd0b8c5/uvicorn-0.40.0.tar.gz", hash = "sha256:839676675e87e73694518b5574fd0f24c9d97b46bea16df7b8c05ea1a51071ea", size = 81761 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/3d/d8/2083a1daa7439a66f3a48589a57d576aa117726762618f6bb09fe3798796/uvicorn-0.40.0-py3-none-any.whl", hash = "sha256:c6c8f55bc8bf13eb6fa9ff87ad62308bbbc33d0b67f84293151efe87e0d5f2ee", size = 68502 }, -] - -[package.optional-dependencies] -standard = [ - { name = "colorama", marker = "sys_platform == 'win32'" }, - { name = "httptools" }, - { name = "python-dotenv" }, - { name = "pyyaml" }, - { name = "uvloop", marker = "platform_python_implementation != 'PyPy' and sys_platform != 'cygwin' and sys_platform != 'win32'" }, - { name = "watchfiles" }, - { name = "websockets" }, -] - -[[package]] -name = "uvloop" -version = "0.22.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/06/f0/18d39dbd1971d6d62c4629cc7fa67f74821b0dc1f5a77af43719de7936a7/uvloop-0.22.1.tar.gz", hash = "sha256:6c84bae345b9147082b17371e3dd5d42775bddce91f885499017f4607fdaf39f", size = 2443250 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/3d/ff/7f72e8170be527b4977b033239a83a68d5c881cc4775fca255c677f7ac5d/uvloop-0.22.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:fe94b4564e865d968414598eea1a6de60adba0c040ba4ed05ac1300de402cd42", size = 1359936 }, - { url = "https://files.pythonhosted.org/packages/c3/c6/e5d433f88fd54d81ef4be58b2b7b0cea13c442454a1db703a1eea0db1a59/uvloop-0.22.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:51eb9bd88391483410daad430813d982010f9c9c89512321f5b60e2cddbdddd6", size = 752769 }, - { url = "https://files.pythonhosted.org/packages/24/68/a6ac446820273e71aa762fa21cdcc09861edd3536ff47c5cd3b7afb10eeb/uvloop-0.22.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:700e674a166ca5778255e0e1dc4e9d79ab2acc57b9171b79e65feba7184b3370", size = 4317413 }, - { url = "https://files.pythonhosted.org/packages/5f/6f/e62b4dfc7ad6518e7eff2516f680d02a0f6eb62c0c212e152ca708a0085e/uvloop-0.22.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7b5b1ac819a3f946d3b2ee07f09149578ae76066d70b44df3fa990add49a82e4", size = 4426307 }, - { url = "https://files.pythonhosted.org/packages/90/60/97362554ac21e20e81bcef1150cb2a7e4ffdaf8ea1e5b2e8bf7a053caa18/uvloop-0.22.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e047cc068570bac9866237739607d1313b9253c3051ad84738cbb095be0537b2", size = 4131970 }, - { url = "https://files.pythonhosted.org/packages/99/39/6b3f7d234ba3964c428a6e40006340f53ba37993f46ed6e111c6e9141d18/uvloop-0.22.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:512fec6815e2dd45161054592441ef76c830eddaad55c8aa30952e6fe1ed07c0", size = 4296343 }, - { url = "https://files.pythonhosted.org/packages/89/8c/182a2a593195bfd39842ea68ebc084e20c850806117213f5a299dfc513d9/uvloop-0.22.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:561577354eb94200d75aca23fbde86ee11be36b00e52a4eaf8f50fb0c86b7705", size = 1358611 }, - { url = "https://files.pythonhosted.org/packages/d2/14/e301ee96a6dc95224b6f1162cd3312f6d1217be3907b79173b06785f2fe7/uvloop-0.22.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1cdf5192ab3e674ca26da2eada35b288d2fa49fdd0f357a19f0e7c4e7d5077c8", size = 751811 }, - { url = "https://files.pythonhosted.org/packages/b7/02/654426ce265ac19e2980bfd9ea6590ca96a56f10c76e63801a2df01c0486/uvloop-0.22.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6e2ea3d6190a2968f4a14a23019d3b16870dd2190cd69c8180f7c632d21de68d", size = 4288562 }, - { url = "https://files.pythonhosted.org/packages/15/c0/0be24758891ef825f2065cd5db8741aaddabe3e248ee6acc5e8a80f04005/uvloop-0.22.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0530a5fbad9c9e4ee3f2b33b148c6a64d47bbad8000ea63704fa8260f4cf728e", size = 4366890 }, - { url = "https://files.pythonhosted.org/packages/d2/53/8369e5219a5855869bcee5f4d317f6da0e2c669aecf0ef7d371e3d084449/uvloop-0.22.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bc5ef13bbc10b5335792360623cc378d52d7e62c2de64660616478c32cd0598e", size = 4119472 }, - { url = "https://files.pythonhosted.org/packages/f8/ba/d69adbe699b768f6b29a5eec7b47dd610bd17a69de51b251126a801369ea/uvloop-0.22.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1f38ec5e3f18c8a10ded09742f7fb8de0108796eb673f30ce7762ce1b8550cad", size = 4239051 }, - { url = "https://files.pythonhosted.org/packages/90/cd/b62bdeaa429758aee8de8b00ac0dd26593a9de93d302bff3d21439e9791d/uvloop-0.22.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3879b88423ec7e97cd4eba2a443aa26ed4e59b45e6b76aabf13fe2f27023a142", size = 1362067 }, - { url = "https://files.pythonhosted.org/packages/0d/f8/a132124dfda0777e489ca86732e85e69afcd1ff7686647000050ba670689/uvloop-0.22.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:4baa86acedf1d62115c1dc6ad1e17134476688f08c6efd8a2ab076e815665c74", size = 752423 }, - { url = "https://files.pythonhosted.org/packages/a3/94/94af78c156f88da4b3a733773ad5ba0b164393e357cc4bd0ab2e2677a7d6/uvloop-0.22.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:297c27d8003520596236bdb2335e6b3f649480bd09e00d1e3a99144b691d2a35", size = 4272437 }, - { url = "https://files.pythonhosted.org/packages/b5/35/60249e9fd07b32c665192cec7af29e06c7cd96fa1d08b84f012a56a0b38e/uvloop-0.22.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c1955d5a1dd43198244d47664a5858082a3239766a839b2102a269aaff7a4e25", size = 4292101 }, - { url = "https://files.pythonhosted.org/packages/02/62/67d382dfcb25d0a98ce73c11ed1a6fba5037a1a1d533dcbb7cab033a2636/uvloop-0.22.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b31dc2fccbd42adc73bc4e7cdbae4fc5086cf378979e53ca5d0301838c5682c6", size = 4114158 }, - { url = "https://files.pythonhosted.org/packages/f0/7a/f1171b4a882a5d13c8b7576f348acfe6074d72eaf52cccef752f748d4a9f/uvloop-0.22.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:93f617675b2d03af4e72a5333ef89450dfaa5321303ede6e67ba9c9d26878079", size = 4177360 }, - { url = "https://files.pythonhosted.org/packages/79/7b/b01414f31546caf0919da80ad57cbfe24c56b151d12af68cee1b04922ca8/uvloop-0.22.1-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:37554f70528f60cad66945b885eb01f1bb514f132d92b6eeed1c90fd54ed6289", size = 1454790 }, - { url = "https://files.pythonhosted.org/packages/d4/31/0bb232318dd838cad3fa8fb0c68c8b40e1145b32025581975e18b11fab40/uvloop-0.22.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:b76324e2dc033a0b2f435f33eb88ff9913c156ef78e153fb210e03c13da746b3", size = 796783 }, - { url = "https://files.pythonhosted.org/packages/42/38/c9b09f3271a7a723a5de69f8e237ab8e7803183131bc57c890db0b6bb872/uvloop-0.22.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:badb4d8e58ee08dad957002027830d5c3b06aea446a6a3744483c2b3b745345c", size = 4647548 }, - { url = "https://files.pythonhosted.org/packages/c1/37/945b4ca0ac27e3dc4952642d4c900edd030b3da6c9634875af6e13ae80e5/uvloop-0.22.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b91328c72635f6f9e0282e4a57da7470c7350ab1c9f48546c0f2866205349d21", size = 4467065 }, - { url = "https://files.pythonhosted.org/packages/97/cc/48d232f33d60e2e2e0b42f4e73455b146b76ebe216487e862700457fbf3c/uvloop-0.22.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:daf620c2995d193449393d6c62131b3fbd40a63bf7b307a1527856ace637fe88", size = 4328384 }, - { url = "https://files.pythonhosted.org/packages/e4/16/c1fd27e9549f3c4baf1dc9c20c456cd2f822dbf8de9f463824b0c0357e06/uvloop-0.22.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6cde23eeda1a25c75b2e07d39970f3374105d5eafbaab2a4482be82f272d5a5e", size = 4296730 }, -] - -[[package]] -name = "watchfiles" -version = "1.1.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "anyio" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/c2/c9/8869df9b2a2d6c59d79220a4db37679e74f807c559ffe5265e08b227a210/watchfiles-1.1.1.tar.gz", hash = "sha256:a173cb5c16c4f40ab19cecf48a534c409f7ea983ab8fed0741304a1c0a31b3f2", size = 94440 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/74/d5/f039e7e3c639d9b1d09b07ea412a6806d38123f0508e5f9b48a87b0a76cc/watchfiles-1.1.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:8c89f9f2f740a6b7dcc753140dd5e1ab9215966f7a3530d0c0705c83b401bd7d", size = 404745 }, - { url = "https://files.pythonhosted.org/packages/a5/96/a881a13aa1349827490dab2d363c8039527060cfcc2c92cc6d13d1b1049e/watchfiles-1.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:bd404be08018c37350f0d6e34676bd1e2889990117a2b90070b3007f172d0610", size = 391769 }, - { url = "https://files.pythonhosted.org/packages/4b/5b/d3b460364aeb8da471c1989238ea0e56bec24b6042a68046adf3d9ddb01c/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8526e8f916bb5b9a0a777c8317c23ce65de259422bba5b31325a6fa6029d33af", size = 449374 }, - { url = "https://files.pythonhosted.org/packages/b9/44/5769cb62d4ed055cb17417c0a109a92f007114a4e07f30812a73a4efdb11/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2edc3553362b1c38d9f06242416a5d8e9fe235c204a4072e988ce2e5bb1f69f6", size = 459485 }, - { url = "https://files.pythonhosted.org/packages/19/0c/286b6301ded2eccd4ffd0041a1b726afda999926cf720aab63adb68a1e36/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:30f7da3fb3f2844259cba4720c3fc7138eb0f7b659c38f3bfa65084c7fc7abce", size = 488813 }, - { url = "https://files.pythonhosted.org/packages/c7/2b/8530ed41112dd4a22f4dcfdb5ccf6a1baad1ff6eed8dc5a5f09e7e8c41c7/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f8979280bdafff686ba5e4d8f97840f929a87ed9cdf133cbbd42f7766774d2aa", size = 594816 }, - { url = "https://files.pythonhosted.org/packages/ce/d2/f5f9fb49489f184f18470d4f99f4e862a4b3e9ac2865688eb2099e3d837a/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dcc5c24523771db3a294c77d94771abcfcb82a0e0ee8efd910c37c59ec1b31bb", size = 475186 }, - { url = "https://files.pythonhosted.org/packages/cf/68/5707da262a119fb06fbe214d82dd1fe4a6f4af32d2d14de368d0349eb52a/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1db5d7ae38ff20153d542460752ff397fcf5c96090c1230803713cf3147a6803", size = 456812 }, - { url = "https://files.pythonhosted.org/packages/66/ab/3cbb8756323e8f9b6f9acb9ef4ec26d42b2109bce830cc1f3468df20511d/watchfiles-1.1.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:28475ddbde92df1874b6c5c8aaeb24ad5be47a11f87cde5a28ef3835932e3e94", size = 630196 }, - { url = "https://files.pythonhosted.org/packages/78/46/7152ec29b8335f80167928944a94955015a345440f524d2dfe63fc2f437b/watchfiles-1.1.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:36193ed342f5b9842edd3532729a2ad55c4160ffcfa3700e0d54be496b70dd43", size = 622657 }, - { url = "https://files.pythonhosted.org/packages/0a/bf/95895e78dd75efe9a7f31733607f384b42eb5feb54bd2eb6ed57cc2e94f4/watchfiles-1.1.1-cp312-cp312-win32.whl", hash = "sha256:859e43a1951717cc8de7f4c77674a6d389b106361585951d9e69572823f311d9", size = 272042 }, - { url = "https://files.pythonhosted.org/packages/87/0a/90eb755f568de2688cb220171c4191df932232c20946966c27a59c400850/watchfiles-1.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:91d4c9a823a8c987cce8fa2690923b069966dabb196dd8d137ea2cede885fde9", size = 288410 }, - { url = "https://files.pythonhosted.org/packages/36/76/f322701530586922fbd6723c4f91ace21364924822a8772c549483abed13/watchfiles-1.1.1-cp312-cp312-win_arm64.whl", hash = "sha256:a625815d4a2bdca61953dbba5a39d60164451ef34c88d751f6c368c3ea73d404", size = 278209 }, - { url = "https://files.pythonhosted.org/packages/bb/f4/f750b29225fe77139f7ae5de89d4949f5a99f934c65a1f1c0b248f26f747/watchfiles-1.1.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:130e4876309e8686a5e37dba7d5e9bc77e6ed908266996ca26572437a5271e18", size = 404321 }, - { url = "https://files.pythonhosted.org/packages/2b/f9/f07a295cde762644aa4c4bb0f88921d2d141af45e735b965fb2e87858328/watchfiles-1.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5f3bde70f157f84ece3765b42b4a52c6ac1a50334903c6eaf765362f6ccca88a", size = 391783 }, - { url = "https://files.pythonhosted.org/packages/bc/11/fc2502457e0bea39a5c958d86d2cb69e407a4d00b85735ca724bfa6e0d1a/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:14e0b1fe858430fc0251737ef3824c54027bedb8c37c38114488b8e131cf8219", size = 449279 }, - { url = "https://files.pythonhosted.org/packages/e3/1f/d66bc15ea0b728df3ed96a539c777acfcad0eb78555ad9efcaa1274688f0/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f27db948078f3823a6bb3b465180db8ebecf26dd5dae6f6180bd87383b6b4428", size = 459405 }, - { url = "https://files.pythonhosted.org/packages/be/90/9f4a65c0aec3ccf032703e6db02d89a157462fbb2cf20dd415128251cac0/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:059098c3a429f62fc98e8ec62b982230ef2c8df68c79e826e37b895bc359a9c0", size = 488976 }, - { url = "https://files.pythonhosted.org/packages/37/57/ee347af605d867f712be7029bb94c8c071732a4b44792e3176fa3c612d39/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bfb5862016acc9b869bb57284e6cb35fdf8e22fe59f7548858e2f971d045f150", size = 595506 }, - { url = "https://files.pythonhosted.org/packages/a8/78/cc5ab0b86c122047f75e8fc471c67a04dee395daf847d3e59381996c8707/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:319b27255aacd9923b8a276bb14d21a5f7ff82564c744235fc5eae58d95422ae", size = 474936 }, - { url = "https://files.pythonhosted.org/packages/62/da/def65b170a3815af7bd40a3e7010bf6ab53089ef1b75d05dd5385b87cf08/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c755367e51db90e75b19454b680903631d41f9e3607fbd941d296a020c2d752d", size = 456147 }, - { url = "https://files.pythonhosted.org/packages/57/99/da6573ba71166e82d288d4df0839128004c67d2778d3b566c138695f5c0b/watchfiles-1.1.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c22c776292a23bfc7237a98f791b9ad3144b02116ff10d820829ce62dff46d0b", size = 630007 }, - { url = "https://files.pythonhosted.org/packages/a8/51/7439c4dd39511368849eb1e53279cd3454b4a4dbace80bab88feeb83c6b5/watchfiles-1.1.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:3a476189be23c3686bc2f4321dd501cb329c0a0469e77b7b534ee10129ae6374", size = 622280 }, - { url = "https://files.pythonhosted.org/packages/95/9c/8ed97d4bba5db6fdcdb2b298d3898f2dd5c20f6b73aee04eabe56c59677e/watchfiles-1.1.1-cp313-cp313-win32.whl", hash = "sha256:bf0a91bfb5574a2f7fc223cf95eeea79abfefa404bf1ea5e339c0c1560ae99a0", size = 272056 }, - { url = "https://files.pythonhosted.org/packages/1f/f3/c14e28429f744a260d8ceae18bf58c1d5fa56b50d006a7a9f80e1882cb0d/watchfiles-1.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:52e06553899e11e8074503c8e716d574adeeb7e68913115c4b3653c53f9bae42", size = 288162 }, - { url = "https://files.pythonhosted.org/packages/dc/61/fe0e56c40d5cd29523e398d31153218718c5786b5e636d9ae8ae79453d27/watchfiles-1.1.1-cp313-cp313-win_arm64.whl", hash = "sha256:ac3cc5759570cd02662b15fbcd9d917f7ecd47efe0d6b40474eafd246f91ea18", size = 277909 }, - { url = "https://files.pythonhosted.org/packages/79/42/e0a7d749626f1e28c7108a99fb9bf524b501bbbeb9b261ceecde644d5a07/watchfiles-1.1.1-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:563b116874a9a7ce6f96f87cd0b94f7faf92d08d0021e837796f0a14318ef8da", size = 403389 }, - { url = "https://files.pythonhosted.org/packages/15/49/08732f90ce0fbbc13913f9f215c689cfc9ced345fb1bcd8829a50007cc8d/watchfiles-1.1.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3ad9fe1dae4ab4212d8c91e80b832425e24f421703b5a42ef2e4a1e215aff051", size = 389964 }, - { url = "https://files.pythonhosted.org/packages/27/0d/7c315d4bd5f2538910491a0393c56bf70d333d51bc5b34bee8e68e8cea19/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce70f96a46b894b36eba678f153f052967a0d06d5b5a19b336ab0dbbd029f73e", size = 448114 }, - { url = "https://files.pythonhosted.org/packages/c3/24/9e096de47a4d11bc4df41e9d1e61776393eac4cb6eb11b3e23315b78b2cc/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:cb467c999c2eff23a6417e58d75e5828716f42ed8289fe6b77a7e5a91036ca70", size = 460264 }, - { url = "https://files.pythonhosted.org/packages/cc/0f/e8dea6375f1d3ba5fcb0b3583e2b493e77379834c74fd5a22d66d85d6540/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:836398932192dae4146c8f6f737d74baeac8b70ce14831a239bdb1ca882fc261", size = 487877 }, - { url = "https://files.pythonhosted.org/packages/ac/5b/df24cfc6424a12deb41503b64d42fbea6b8cb357ec62ca84a5a3476f654a/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:743185e7372b7bc7c389e1badcc606931a827112fbbd37f14c537320fca08620", size = 595176 }, - { url = "https://files.pythonhosted.org/packages/8f/b5/853b6757f7347de4e9b37e8cc3289283fb983cba1ab4d2d7144694871d9c/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:afaeff7696e0ad9f02cbb8f56365ff4686ab205fcf9c4c5b6fdfaaa16549dd04", size = 473577 }, - { url = "https://files.pythonhosted.org/packages/e1/f7/0a4467be0a56e80447c8529c9fce5b38eab4f513cb3d9bf82e7392a5696b/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3f7eb7da0eb23aa2ba036d4f616d46906013a68caf61b7fdbe42fc8b25132e77", size = 455425 }, - { url = "https://files.pythonhosted.org/packages/8e/e0/82583485ea00137ddf69bc84a2db88bd92ab4a6e3c405e5fb878ead8d0e7/watchfiles-1.1.1-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:831a62658609f0e5c64178211c942ace999517f5770fe9436be4c2faeba0c0ef", size = 628826 }, - { url = "https://files.pythonhosted.org/packages/28/9a/a785356fccf9fae84c0cc90570f11702ae9571036fb25932f1242c82191c/watchfiles-1.1.1-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:f9a2ae5c91cecc9edd47e041a930490c31c3afb1f5e6d71de3dc671bfaca02bf", size = 622208 }, - { url = "https://files.pythonhosted.org/packages/c3/f4/0872229324ef69b2c3edec35e84bd57a1289e7d3fe74588048ed8947a323/watchfiles-1.1.1-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:d1715143123baeeaeadec0528bb7441103979a1d5f6fd0e1f915383fea7ea6d5", size = 404315 }, - { url = "https://files.pythonhosted.org/packages/7b/22/16d5331eaed1cb107b873f6ae1b69e9ced582fcf0c59a50cd84f403b1c32/watchfiles-1.1.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:39574d6370c4579d7f5d0ad940ce5b20db0e4117444e39b6d8f99db5676c52fd", size = 390869 }, - { url = "https://files.pythonhosted.org/packages/b2/7e/5643bfff5acb6539b18483128fdc0ef2cccc94a5b8fbda130c823e8ed636/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7365b92c2e69ee952902e8f70f3ba6360d0d596d9299d55d7d386df84b6941fb", size = 449919 }, - { url = "https://files.pythonhosted.org/packages/51/2e/c410993ba5025a9f9357c376f48976ef0e1b1aefb73b97a5ae01a5972755/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bfff9740c69c0e4ed32416f013f3c45e2ae42ccedd1167ef2d805c000b6c71a5", size = 460845 }, - { url = "https://files.pythonhosted.org/packages/8e/a4/2df3b404469122e8680f0fcd06079317e48db58a2da2950fb45020947734/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b27cf2eb1dda37b2089e3907d8ea92922b673c0c427886d4edc6b94d8dfe5db3", size = 489027 }, - { url = "https://files.pythonhosted.org/packages/ea/84/4587ba5b1f267167ee715b7f66e6382cca6938e0a4b870adad93e44747e6/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:526e86aced14a65a5b0ec50827c745597c782ff46b571dbfe46192ab9e0b3c33", size = 595615 }, - { url = "https://files.pythonhosted.org/packages/6a/0f/c6988c91d06e93cd0bb3d4a808bcf32375ca1904609835c3031799e3ecae/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:04e78dd0b6352db95507fd8cb46f39d185cf8c74e4cf1e4fbad1d3df96faf510", size = 474836 }, - { url = "https://files.pythonhosted.org/packages/b4/36/ded8aebea91919485b7bbabbd14f5f359326cb5ec218cd67074d1e426d74/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5c85794a4cfa094714fb9c08d4a218375b2b95b8ed1666e8677c349906246c05", size = 455099 }, - { url = "https://files.pythonhosted.org/packages/98/e0/8c9bdba88af756a2fce230dd365fab2baf927ba42cd47521ee7498fd5211/watchfiles-1.1.1-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:74d5012b7630714b66be7b7b7a78855ef7ad58e8650c73afc4c076a1f480a8d6", size = 630626 }, - { url = "https://files.pythonhosted.org/packages/2a/84/a95db05354bf2d19e438520d92a8ca475e578c647f78f53197f5a2f17aaf/watchfiles-1.1.1-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:8fbe85cb3201c7d380d3d0b90e63d520f15d6afe217165d7f98c9c649654db81", size = 622519 }, - { url = "https://files.pythonhosted.org/packages/1d/ce/d8acdc8de545de995c339be67711e474c77d643555a9bb74a9334252bd55/watchfiles-1.1.1-cp314-cp314-win32.whl", hash = "sha256:3fa0b59c92278b5a7800d3ee7733da9d096d4aabcfabb9a928918bd276ef9b9b", size = 272078 }, - { url = "https://files.pythonhosted.org/packages/c4/c9/a74487f72d0451524be827e8edec251da0cc1fcf111646a511ae752e1a3d/watchfiles-1.1.1-cp314-cp314-win_amd64.whl", hash = "sha256:c2047d0b6cea13b3316bdbafbfa0c4228ae593d995030fda39089d36e64fc03a", size = 287664 }, - { url = "https://files.pythonhosted.org/packages/df/b8/8ac000702cdd496cdce998c6f4ee0ca1f15977bba51bdf07d872ebdfc34c/watchfiles-1.1.1-cp314-cp314-win_arm64.whl", hash = "sha256:842178b126593addc05acf6fce960d28bc5fae7afbaa2c6c1b3a7b9460e5be02", size = 277154 }, - { url = "https://files.pythonhosted.org/packages/47/a8/e3af2184707c29f0f14b1963c0aace6529f9d1b8582d5b99f31bbf42f59e/watchfiles-1.1.1-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:88863fbbc1a7312972f1c511f202eb30866370ebb8493aef2812b9ff28156a21", size = 403820 }, - { url = "https://files.pythonhosted.org/packages/c0/ec/e47e307c2f4bd75f9f9e8afbe3876679b18e1bcec449beca132a1c5ffb2d/watchfiles-1.1.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:55c7475190662e202c08c6c0f4d9e345a29367438cf8e8037f3155e10a88d5a5", size = 390510 }, - { url = "https://files.pythonhosted.org/packages/d5/a0/ad235642118090f66e7b2f18fd5c42082418404a79205cdfca50b6309c13/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3f53fa183d53a1d7a8852277c92b967ae99c2d4dcee2bfacff8868e6e30b15f7", size = 448408 }, - { url = "https://files.pythonhosted.org/packages/df/85/97fa10fd5ff3332ae17e7e40e20784e419e28521549780869f1413742e9d/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6aae418a8b323732fa89721d86f39ec8f092fc2af67f4217a2b07fd3e93c6101", size = 458968 }, - { url = "https://files.pythonhosted.org/packages/47/c2/9059c2e8966ea5ce678166617a7f75ecba6164375f3b288e50a40dc6d489/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f096076119da54a6080e8920cbdaac3dbee667eb91dcc5e5b78840b87415bd44", size = 488096 }, - { url = "https://files.pythonhosted.org/packages/94/44/d90a9ec8ac309bc26db808a13e7bfc0e4e78b6fc051078a554e132e80160/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:00485f441d183717038ed2e887a7c868154f216877653121068107b227a2f64c", size = 596040 }, - { url = "https://files.pythonhosted.org/packages/95/68/4e3479b20ca305cfc561db3ed207a8a1c745ee32bf24f2026a129d0ddb6e/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a55f3e9e493158d7bfdb60a1165035f1cf7d320914e7b7ea83fe22c6023b58fc", size = 473847 }, - { url = "https://files.pythonhosted.org/packages/4f/55/2af26693fd15165c4ff7857e38330e1b61ab8c37d15dc79118cdba115b7a/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8c91ed27800188c2ae96d16e3149f199d62f86c7af5f5f4d2c61a3ed8cd3666c", size = 455072 }, - { url = "https://files.pythonhosted.org/packages/66/1d/d0d200b10c9311ec25d2273f8aad8c3ef7cc7ea11808022501811208a750/watchfiles-1.1.1-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:311ff15a0bae3714ffb603e6ba6dbfba4065ab60865d15a6ec544133bdb21099", size = 629104 }, - { url = "https://files.pythonhosted.org/packages/e3/bd/fa9bb053192491b3867ba07d2343d9f2252e00811567d30ae8d0f78136fe/watchfiles-1.1.1-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:a916a2932da8f8ab582f242c065f5c81bed3462849ca79ee357dd9551b0e9b01", size = 622112 }, -] - -[[package]] -name = "websockets" -version = "15.0.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/21/e6/26d09fab466b7ca9c7737474c52be4f76a40301b08362eb2dbc19dcc16c1/websockets-15.0.1.tar.gz", hash = "sha256:82544de02076bafba038ce055ee6412d68da13ab47f0c60cab827346de828dee", size = 177016 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/51/6b/4545a0d843594f5d0771e86463606a3988b5a09ca5123136f8a76580dd63/websockets-15.0.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:3e90baa811a5d73f3ca0bcbf32064d663ed81318ab225ee4f427ad4e26e5aff3", size = 175437 }, - { url = "https://files.pythonhosted.org/packages/f4/71/809a0f5f6a06522af902e0f2ea2757f71ead94610010cf570ab5c98e99ed/websockets-15.0.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:592f1a9fe869c778694f0aa806ba0374e97648ab57936f092fd9d87f8bc03665", size = 173096 }, - { url = "https://files.pythonhosted.org/packages/3d/69/1a681dd6f02180916f116894181eab8b2e25b31e484c5d0eae637ec01f7c/websockets-15.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0701bc3cfcb9164d04a14b149fd74be7347a530ad3bbf15ab2c678a2cd3dd9a2", size = 173332 }, - { url = "https://files.pythonhosted.org/packages/a6/02/0073b3952f5bce97eafbb35757f8d0d54812b6174ed8dd952aa08429bcc3/websockets-15.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e8b56bdcdb4505c8078cb6c7157d9811a85790f2f2b3632c7d1462ab5783d215", size = 183152 }, - { url = "https://files.pythonhosted.org/packages/74/45/c205c8480eafd114b428284840da0b1be9ffd0e4f87338dc95dc6ff961a1/websockets-15.0.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0af68c55afbd5f07986df82831c7bff04846928ea8d1fd7f30052638788bc9b5", size = 182096 }, - { url = "https://files.pythonhosted.org/packages/14/8f/aa61f528fba38578ec553c145857a181384c72b98156f858ca5c8e82d9d3/websockets-15.0.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64dee438fed052b52e4f98f76c5790513235efaa1ef7f3f2192c392cd7c91b65", size = 182523 }, - { url = "https://files.pythonhosted.org/packages/ec/6d/0267396610add5bc0d0d3e77f546d4cd287200804fe02323797de77dbce9/websockets-15.0.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d5f6b181bb38171a8ad1d6aa58a67a6aa9d4b38d0f8c5f496b9e42561dfc62fe", size = 182790 }, - { url = "https://files.pythonhosted.org/packages/02/05/c68c5adbf679cf610ae2f74a9b871ae84564462955d991178f95a1ddb7dd/websockets-15.0.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5d54b09eba2bada6011aea5375542a157637b91029687eb4fdb2dab11059c1b4", size = 182165 }, - { url = "https://files.pythonhosted.org/packages/29/93/bb672df7b2f5faac89761cb5fa34f5cec45a4026c383a4b5761c6cea5c16/websockets-15.0.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3be571a8b5afed347da347bfcf27ba12b069d9d7f42cb8c7028b5e98bbb12597", size = 182160 }, - { url = "https://files.pythonhosted.org/packages/ff/83/de1f7709376dc3ca9b7eeb4b9a07b4526b14876b6d372a4dc62312bebee0/websockets-15.0.1-cp312-cp312-win32.whl", hash = "sha256:c338ffa0520bdb12fbc527265235639fb76e7bc7faafbb93f6ba80d9c06578a9", size = 176395 }, - { url = "https://files.pythonhosted.org/packages/7d/71/abf2ebc3bbfa40f391ce1428c7168fb20582d0ff57019b69ea20fa698043/websockets-15.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:fcd5cf9e305d7b8338754470cf69cf81f420459dbae8a3b40cee57417f4614a7", size = 176841 }, - { url = "https://files.pythonhosted.org/packages/cb/9f/51f0cf64471a9d2b4d0fc6c534f323b664e7095640c34562f5182e5a7195/websockets-15.0.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ee443ef070bb3b6ed74514f5efaa37a252af57c90eb33b956d35c8e9c10a1931", size = 175440 }, - { url = "https://files.pythonhosted.org/packages/8a/05/aa116ec9943c718905997412c5989f7ed671bc0188ee2ba89520e8765d7b/websockets-15.0.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5a939de6b7b4e18ca683218320fc67ea886038265fd1ed30173f5ce3f8e85675", size = 173098 }, - { url = "https://files.pythonhosted.org/packages/ff/0b/33cef55ff24f2d92924923c99926dcce78e7bd922d649467f0eda8368923/websockets-15.0.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:746ee8dba912cd6fc889a8147168991d50ed70447bf18bcda7039f7d2e3d9151", size = 173329 }, - { url = "https://files.pythonhosted.org/packages/31/1d/063b25dcc01faa8fada1469bdf769de3768b7044eac9d41f734fd7b6ad6d/websockets-15.0.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:595b6c3969023ecf9041b2936ac3827e4623bfa3ccf007575f04c5a6aa318c22", size = 183111 }, - { url = "https://files.pythonhosted.org/packages/93/53/9a87ee494a51bf63e4ec9241c1ccc4f7c2f45fff85d5bde2ff74fcb68b9e/websockets-15.0.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3c714d2fc58b5ca3e285461a4cc0c9a66bd0e24c5da9911e30158286c9b5be7f", size = 182054 }, - { url = "https://files.pythonhosted.org/packages/ff/b2/83a6ddf56cdcbad4e3d841fcc55d6ba7d19aeb89c50f24dd7e859ec0805f/websockets-15.0.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f3c1e2ab208db911594ae5b4f79addeb3501604a165019dd221c0bdcabe4db8", size = 182496 }, - { url = "https://files.pythonhosted.org/packages/98/41/e7038944ed0abf34c45aa4635ba28136f06052e08fc2168520bb8b25149f/websockets-15.0.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:229cf1d3ca6c1804400b0a9790dc66528e08a6a1feec0d5040e8b9eb14422375", size = 182829 }, - { url = "https://files.pythonhosted.org/packages/e0/17/de15b6158680c7623c6ef0db361da965ab25d813ae54fcfeae2e5b9ef910/websockets-15.0.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:756c56e867a90fb00177d530dca4b097dd753cde348448a1012ed6c5131f8b7d", size = 182217 }, - { url = "https://files.pythonhosted.org/packages/33/2b/1f168cb6041853eef0362fb9554c3824367c5560cbdaad89ac40f8c2edfc/websockets-15.0.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:558d023b3df0bffe50a04e710bc87742de35060580a293c2a984299ed83bc4e4", size = 182195 }, - { url = "https://files.pythonhosted.org/packages/86/eb/20b6cdf273913d0ad05a6a14aed4b9a85591c18a987a3d47f20fa13dcc47/websockets-15.0.1-cp313-cp313-win32.whl", hash = "sha256:ba9e56e8ceeeedb2e080147ba85ffcd5cd0711b89576b83784d8605a7df455fa", size = 176393 }, - { url = "https://files.pythonhosted.org/packages/1b/6c/c65773d6cab416a64d191d6ee8a8b1c68a09970ea6909d16965d26bfed1e/websockets-15.0.1-cp313-cp313-win_amd64.whl", hash = "sha256:e09473f095a819042ecb2ab9465aee615bd9c2028e4ef7d933600a8401c79561", size = 176837 }, - { url = "https://files.pythonhosted.org/packages/fa/a8/5b41e0da817d64113292ab1f8247140aac61cbf6cfd085d6a0fa77f4984f/websockets-15.0.1-py3-none-any.whl", hash = "sha256:f7a866fbc1e97b5c617ee4116daaa09b722101d4a3c170c787450ba409f9736f", size = 169743 }, -] diff --git a/devspace.yaml b/devspace.yaml index 1216656..4a145c3 100644 --- a/devspace.yaml +++ b/devspace.yaml @@ -3,8 +3,6 @@ name: mainloop # Variables from .env vars: - CLAUDE_CODE_OAUTH_TOKEN: - source: env GITHUB_TOKEN: source: env default: '' @@ -35,16 +33,6 @@ images: args: - --target=dev - agent-controller: - image: mainloop-agent-controller - tags: - - dev - dockerfile: claude-agent/Dockerfile - context: claude-agent - rebuildStrategy: ignoreContextChanges - docker: - useBuildKit: true - # Deployments using Kustomize deployments: mainloop: @@ -163,32 +151,6 @@ dev: value: http://localhost:8081 logs: {} - agent-controller: - labelSelector: - app: mainloop-agent-controller - namespace: mainloop - devImage: mainloop-agent-controller:dev - sync: - - path: ./claude-agent:/app - excludePaths: - - __pycache__/ - - '*.pyc' - - .venv/ - disableDownload: true - onUpload: - exec: - - name: rebuild-agent-image - command: |- - echo "[agent] Rebuilding image for Jobs..." - docker build -t mainloop-agent-controller:dev claude-agent/ && \ - kind load docker-image mainloop-agent-controller:dev --name mainloop-test && \ - echo "[agent] Done - new Jobs will use updated code" - local: true - onChange: ['*.py'] - command: - - /entrypoint.sh - logs: {} - # Hooks for Kind cluster management hooks: - name: ensure-kind-cluster @@ -214,7 +176,6 @@ hooks: echo "Loading images into Kind..." kind load docker-image mainloop-backend:dev --name mainloop-test kind load docker-image mainloop-frontend:dev --name mainloop-test - kind load docker-image mainloop-agent-controller:dev --name mainloop-test # Commands commands: diff --git a/docker-compose.test.yml b/docker-compose.test.yml index b1c83ca..acff875 100644 --- a/docker-compose.test.yml +++ b/docker-compose.test.yml @@ -69,10 +69,8 @@ services: - DB_NAME=mainloop - DB_USER=mainloop - DB_PASSWORD=mainloop - - CLAUDE_AGENT_URL=http://claude-agent-test:8001 - IS_TEST_ENV=true - USE_MOCK_GITHUB=${USE_MOCK_GITHUB:-true} - - CLAUDE_CODE_OAUTH_TOKEN=${CLAUDE_CODE_OAUTH_TOKEN:-} # No --reload needed - watchexec restarts container on file changes command: [uvicorn, mainloop.api:app, --host, 0.0.0.0, --port, '8000'] depends_on: diff --git a/docker-compose.yml b/docker-compose.yml index 7acc8ae..bc2b2cb 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -28,7 +28,6 @@ services: environment: - PYTHONUNBUFFERED=1 - GOOGLE_APPLICATION_CREDENTIALS=/secrets/gcp-credentials.json - - CLAUDE_AGENT_URL=http://claude-agent:8001 - DB_HOST=postgres - DB_PORT=5432 - DB_NAME=mainloop @@ -39,8 +38,6 @@ services: depends_on: postgres: condition: service_healthy - claude-agent: - condition: service_healthy develop: watch: - action: rebuild @@ -78,24 +75,5 @@ services: ignore: - node_modules - claude-agent: - build: - context: ./claude-agent - dockerfile: Dockerfile - container_name: mainloop-claude-agent - ports: - - 8001:8001 # Expose for debugging - env_file: - - .env - volumes: - - claude-workdir:/workspace - healthcheck: - test: [CMD, curl, -f, http://localhost:8001/health] - interval: 10s - timeout: 5s - retries: 3 - start_period: 30s - volumes: - claude-workdir: {} postgres-data: {} diff --git a/docs/architecture.md b/docs/architecture.md index f63d866..b37d3ef 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -1,91 +1,198 @@ -# Architecture - -## Overview - -Mainloop uses a main thread + sessions pattern where your continuous conversation spawns background sessions that appear inline as threaded replies. - -```text -┌─────────────────────────────────────────────────────────────┐ -│ User Devices │ -│ (phone, laptop, etc.) │ -└─────────────────────────┬───────────────────────────────────┘ - │ - ▼ -┌─────────────────────────────────────────────────────────────┐ -│ Cloudflare Access │ -│ (authentication + CDN) │ -└─────────────────────────┬───────────────────────────────────┘ - │ - ┌───────────────┴───────────────┐ - ▼ ▼ -┌──────────────────┐ ┌──────────────────┐ -│ Frontend │ │ Backend │ -│ (SvelteKit) │◄──────────►│ (FastAPI) │ -│ │ │ │ -│ - Chat + inline │ │ - DBOS workflows│ -│ sessions │ │ - Main thread │ -│ - Notifications │ │ - Session queue │ -└──────────────────┘ └────────┬─────────┘ - │ - ┌────────────────────┼────────────────────┐ - │ │ │ - ▼ ▼ ▼ - ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ - │ Session 1 │ │ Session 2 │ │ Session N │ - │ (Claude) │ │ (Opus) │ │ (varies) │ - │ │ │ │ │ │ - │ research │ │ code work │ │ any task │ - └──────────────┘ └──────────────┘ └──────────────┘ +# How Mainloop works + +Mainloop keeps project conversations, agent work, and workspace state together. The control +plane stores messages and delivery state; native Claude Code and Codex sessions run in isolated +Substrate workspaces. Mainloop uses attention as a compute budget: active workspaces run, while +inactive workspaces are snapshotted and suspended until needed. + +These diagrams describe implemented code paths; they do not claim a live cluster proof. Known +verification gaps are listed below. + +## System overview + +```mermaid +flowchart LR + person["Browser or phone"] --> app["SvelteKit app"] + app <--> api["FastAPI + DBOS"] + api --> data[("PostgreSQL
messages, deliveries, workspace state")] + api -->|turn and lifecycle control| router["Substrate router"] + person -->|authenticated preview URL| preview["Mainloop preview proxy"] + preview --> router + + subgraph workspace["One gVisor actor per workspace"] + runtime["App container
toolchain, native CLI, shim, project app"] + services["Sibling service containers
for example, Postgres"] + runtime --- services + end + router --> workspace + workspace -->|allowlisted provider requests| egress["Egress proxy"] + egress --> providers["External provider APIs"] ``` -## Components - -### Frontend (SvelteKit) - -- Mobile-first responsive UI with chat and sessions tabs -- **Main thread**: Continuous conversation with inline session blocks -- **Inline threading**: Session messages appear as Slack-style notifications in timeline -- **Session views**: Expand inline or zoom to fullscreen conversation -- **Notifications**: Toast alerts when sessions need attention - -### Backend (FastAPI + DBOS) - -- **Main Thread Workflow**: Per-user conversation that spawns sessions -- **Session Workflows**: Background work with their own conversations -- **PostgreSQL**: Durable workflow state and conversation history -- **SSE**: Real-time session updates and notifications - -### Sessions - -Sessions are the unified model for all background work: - -- **Simple sessions**: Research, analysis, or any conversation-based task -- **Dev sessions**: Code work with their own K8s namespace for isolation - -Each session has: - -- Its own conversation (separate from main thread) -- A color for visual distinction in timeline -- Status tracking (active, waiting, completed, etc.) -- Anchor to the main thread message that spawned it - -## Data Flow +Mainloop owns conversations, message delivery, workspace policy, and the mapping from native +sessions to workspaces. Substrate runs and snapshots the workspace actors. Agent CLIs keep their +native session identity, history, and tools inside the workspace. The preview proxy checks the +owner and allowed ports before sending traffic through the router. + +## Attention and parking + +Think of workspace compute like foveated rendering in VR: what needs attention is loaded and +running; the rest is parked. Here, parking means that Substrate snapshots and suspends an actor. +An `idle` workspace is still present in Mainloop; `idle` is the waiting period before suspension, +not a separate Substrate state. + +```mermaid +stateDiagram-v2 + [*] --> Running: workspace created + Running --> Idle: no turn or preview activity + Idle --> Running: new turn or preview activity + Idle --> Parked: configured idle timeout expires + Parked --> Waking: message, preview request, or resume action + Waking --> Running: actor restored and healthy + Waking --> Parked: actor confirmed still suspended + Waking --> Unknown: wake result is not confirmed +``` -1. User sends message via frontend -2. Backend adds to main thread conversation -3. Main thread (Claude) decides if session needed -4. If spawning: creates session anchored to user message -5. Session appears inline in main thread timeline -6. Session messages surface as thread notifications -7. User can respond inline or zoom into session -8. Completed sessions post summary back to main thread +The project `dev` manifest sets the idle timeout; its current default is 30 minutes. Mainloop +checks for expired workspaces about once a minute. Turns and preview requests count as activity. +Opening a workspace page does not wake its actor. Deliveries in `recorded`, `queued`, `sending`, +or `uncertain` states, and deliveries sent without completion, block parking. A new message, turn, +or allowed preview request asks the router to wake a parked actor. The intended interactive wake +is about a second. + +Measured locally on Kind in earlier Substrate actor runs: resume took about 0.4–0.8 seconds. +That is an observed actor-resume time, not an end-to-end latency promise. The current headless +native-CLI path and live wake-on-preview path have not been verified on a cluster. + +## Sending a message + +```mermaid +sequenceDiagram + actor Owner + participant UI as SvelteKit + participant Mainloop as FastAPI + DBOS + participant DB as PostgreSQL + participant Router as Substrate router + participant Shim as Workspace shim + participant CLI as Native agent CLI + + Owner->>UI: Send a message + UI->>Mainloop: Submit message + Mainloop->>DB: Record message and delivery under workspace lock + Mainloop->>Router: CONNECT to the workspace shim + Note over Router: Wake the actor if it is parked + Router->>Shim: POST /turn + Shim->>CLI: Run the native CLI for this session + CLI-->>Shim: Output, events, and completion + Shim-->>Mainloop: Journal and turn status + Mainloop->>DB: Save journal and delivery outcome + Mainloop-->>UI: Publish the update +``` -## Model Configuration +Recording the delivery before connecting gives Mainloop a stable delivery to reconcile if a +connection times out. Mainloop does not blindly send an uncertain turn again. + +## Development workspaces and previews + +A project manifest has a `dev` section for the app image or devcontainer, sibling services, +preview ports, and idle timeout. Mainloop creates a separate workspace actor for each branch. +Each actor has an app container with its toolchain, agent CLIs, shim, and app process; declared +services such as Postgres run alongside it in the same actor. Branches have separate app and +service state. + +```mermaid +flowchart LR + manifest["Project manifest
dev: image, services, ports, idle timeout"] + manifest --> branchA["Branch: feature-a"] + manifest --> branchB["Branch: fix-123"] + + subgraph actorA["Workspace actor A"] + appA["App container
toolchain + CLI + shim + app"] + dbA[("Postgres")] + appA --- dbA + end + subgraph actorB["Workspace actor B"] + appB["App container
toolchain + CLI + shim + app"] + dbB[("Postgres")] + appB --- dbB + end + branchA --> actorA + branchB --> actorB + + browser["Browser"] --> url["Preview URL
port--workspace.preview.domain"] + url --> proxy["Mainloop preview proxy
owner and port checks"] + proxy --> router["Substrate router
CONNECT"] + router --> appA + router --> appB +``` -| Component | Model | Purpose | -| ----------- | -------------- | -------------------------------- | -| Main thread | Sonnet | Coordination, spawning decisions | -| Sessions | Opus (default) | Complex tasks, code generation | -| Sessions | Sonnet/Haiku | Can be specified per-session | +The preview URL is `https://--.preview.`. The preview proxy routes the +requested port to that workspace through the router. It accepts ports declared in the manifest +or reported by the authenticated shim. A preview request is designed to wake a parked workspace, +then serve the app after it is ready. + +## Credentials + +Actors do not receive the real provider credentials. The credential broker keeps the real +Codex `auth.json` and Claude token in control-plane Secrets. In the actor, Codex gets a synthetic +placeholder `auth.json` and Claude gets a placeholder token. The egress credential provider +injects the broker-owned value into approved provider requests. + +```mermaid +flowchart LR + subgraph actor["Workspace actor"] + cli["Native agent CLI"] + placeholder["Placeholder auth.json
or Claude token"] + cli --- placeholder + end + subgraph control["Mainloop control plane"] + broker["Credential broker"] + secrets[("Kubernetes Secrets
real provider credentials")] + job["Bounded sign-in job"] + owner["Owner"] + broker --> secrets + owner --> job --> broker + end + egress["Egress proxy and credential provider"] + provider["Provider API"] + cli -->|approved request| egress + secrets -->|credential injection| egress + egress --> provider +``` -Sessions can use different models based on task complexity. +When a credential is missing or rejected, Mainloop raises an attention item instead of replaying +the failed turn. The owner can complete a provider sign-in flow from the workspace page. The +Kubernetes sign-in job and the external egress credential-provider contract are not live-verified. + +## Isolation and known gaps + +- Substrate actors use gVisor isolation; microVM isolation is deferred. The router admits actor + control traffic from the Mainloop control namespace, and each actor has a distinct shim token. + By default, shim token Secrets live in a namespace separate from provider credentials. +- Actor egress is restricted by host allowlists and passes through the egress proxy. +- Workspace lifecycle and credential paths have fake-backed coverage; their combined behavior + has not been verified end to end on a live cluster. +- The backend authenticates to the pinned Substrate API (`0f9635aed37bd5dde604a9bca1975421cd07181a`) + with a projected ServiceAccount token whose audience is `api.ate-system.svc`. That API verifies + trusted tokens with this audience but does not check caller authorization through its OpenFGA + model yet, so the token effectively grants full Substrate access. This is a known authorization + gap; the ClusterTrustBundle permission is limited to `list`. +- Mainloop pins a Substrate fork whose actor runtime runs containers as the image's `USER` in + its `WORKDIR`; the agent image runs as UID `10001` and refuses UID 0. The fork gives a fresh + durable volume to that user. Non-root startup and a durable volume surviving suspend and resume + were live checked on Kind with fork commit `0f9635ae` using the spike's proof scripts. +- The actor's observed `RLIMIT_NOFILE` is 1024. +- Restoring Postgres from an actor snapshot is unverified. +- Live wake-on-preview is unverified. + +## Glossary + +| Term | Meaning | +| ------------------- | --------------------------------------------------------------------------------------------------- | +| **Actor** | An isolated Substrate sandbox for one workspace; it can contain the app and sibling services. | +| **Atespace** | A Substrate namespace that groups actors and their templates. | +| **Template** | The configuration Substrate uses to create an actor, including its containers and startup settings. | +| **Golden snapshot** | A saved template starting state used to create a ready-to-run actor. | +| **Shim** | The small workspace process that accepts turns, starts native CLIs, and reports journals and ports. | +| **Router** | Substrate's control path for connecting Mainloop to an actor. | +| **Park** | Snapshot and suspend an idle actor so its compute is not running until needed. | diff --git a/docs/architecture/native-agent-claude.md b/docs/architecture/native-agent-claude.md index 0551cce..e1a1ca8 100644 --- a/docs/architecture/native-agent-claude.md +++ b/docs/architecture/native-agent-claude.md @@ -1,10 +1,8 @@ # Native Claude session adapter -Status: implemented as a sanitized fixture-backed normalizer only. This slice -does not start Claude, use a subscription, import the Claude Agent SDK, or wire -the adapter into a production call path. `ROADMAP.md` remains the intended -architecture; the existing `claude-agent/` worker and its SDK entrypoints are -unchanged. +Status: fixture-backed stream normalizer, used by the native Substrate session path. This note's +description of the former SDK worker is historical: that worker and its backend entrypoints were +removed by the 2026-09-24 Substrate cutover. This document does not claim live-cluster proof. ## Boundary @@ -93,9 +91,9 @@ Claude capability has been established. ## Existing SDK separation -The current `backend/src/mainloop/claude_agent.py`, -`backend/src/mainloop/services/claude_agent.py`, and `claude-agent/` service use -the existing Claude Agent SDK worker. This adapter does not call those modules, +The former `backend/src/mainloop/claude_agent.py`, +`backend/src/mainloop/services/claude_agent.py`, and worker service used the old SDK path; those +entrypoints were removed by the Substrate cutover. This adapter does not call those modules, does not parse their result wrapper as native evidence, and does not change their production behavior. Replacing those paths requires a later architecture decision backed by live native proof. diff --git a/docs/architecture/native-agent-codex.md b/docs/architecture/native-agent-codex.md index 1a56f87..a61ba4d 100644 --- a/docs/architecture/native-agent-codex.md +++ b/docs/architecture/native-agent-codex.md @@ -1,5 +1,9 @@ # Codex native-agent fixture boundary +Historical scope: this document describes the fixture parser boundary, not transport ownership. +The Substrate cutover removed the former SDK-based main chat path. The current workspace transport +is implemented separately and this fixture note is not live proof. + Status: implemented normalizer and sanitized fixture evidence only. This document does not claim a live Codex proof, production wiring, transport ownership, or subscription-backed capability. @@ -157,5 +161,5 @@ The declarations are separate from provider metadata on `NativeEvent`. The fixture tests therefore establish deterministic normalization and recovery inputs, not that Codex emits these records in every mode or that a native -session accepts a message. Existing production paths and the Claude Agent SDK +session accepts a message. Existing production paths and the former SDK-based main chat worker are unchanged. diff --git a/docs/architecture/native-agent-inventory.md b/docs/architecture/native-agent-inventory.md index 8cec693..b8a07c8 100644 --- a/docs/architecture/native-agent-inventory.md +++ b/docs/architecture/native-agent-inventory.md @@ -1,9 +1,9 @@ # Native-agent boundary inventory -Status: contract implementation and synthetic test cases only. No native-provider -proof, production wiring, database migration, or workspace lifecycle change is -included. `ROADMAP.md` describes the intended architecture; the existing specs -continue to describe user-visible behavior. +Historical inventory from before the 2026-09-24 Substrate cutover. Its rows describe the former +implementation and proposed replacement points; removed SDK and Job paths are retained here only +as migration history. Current behavior is documented in `docs/specs/` and implemented through the +native Substrate workspace transport. This inventory is not live-cluster proof. ## Existing implementation and replacement points diff --git a/docs/specs/chat.md b/docs/specs/chat.md index 486f5ae..a7b2b5e 100644 --- a/docs/specs/chat.md +++ b/docs/specs/chat.md @@ -1,73 +1,27 @@ # Chat -The main thread is a continuous conversation with Claude that persists across devices. +The home chat is the user's native Claude Code main session in a configured Substrate actor. The provider owns native session history and tools; Mainloop records logical messages and delivery state. -## Sending Messages +## Sending messages -- Input field with placeholder "Enter command..." -- EXEC button submits the message -- Message appears in conversation immediately -- Assistant response streams in below +- The input field submits one user message to the main session. +- Mainloop records the conversation message and delivery intent before contacting the actor. +- A prompt is sent once. If the transport outcome is unknown, Mainloop marks the delivery uncertain and does not replay it. +- The response is mirrored from the native journal into the conversation. While the page is open, it polls for new journal evidence and turn completion. +- A second message is rejected with `409` while a delivery or rotation is in flight. -## Conversation History +## Conversation history -- Messages persist across page reloads -- Context maintained in follow-up messages -- User messages and assistant responses displayed in sequence +- User and assistant messages persist across page reloads. +- The native session remains authoritative for provider history and context management; Mainloop mirrors observed messages and delivery receipts. +- Mainloop rotates the main native session after its configured token-growth or turn budget. Before rotation, it asks the current session to write durable state through the allowed Mainloop tools, then starts a new native session with a generated startup context. -## Spawning Sessions +## Delegating sessions -From the main thread, you can ask Claude to spawn sessions: +- The main session can create Claude Code or Codex child sessions through the `mainloop` tool. +- Child reports are stored against their topic and delivered to the main session as ledgered messages. Reports arriving during another turn are queued. +- The main session can inspect status and stored reports without sending a prompt to a child. -- Sessions appear as colored thread blocks in the timeline -- Session messages surface as thread notifications -- Click to expand inline or zoom to fullscreen view +## Identity and policy -## Native Agent Session Chat (implemented in the local kind slice) - -In a session bound to a native agent (see `sessions.md`), the chat tab shows the user's messages and the agent's -replies. Replies are read only from the agent's native journal (Claude transcript, Codex rollout), never from the -terminal, and are mirrored into the conversation once per completed turn. The chat refreshes every few seconds -while the page is open. - -## Native Main Thread (implemented in the local kind slice; flag `MAIN_THREAD_MODE=native`) - -With `MAIN_THREAD_MODE=native` the home chat talks to a native Claude Code session running under Herdr in its own -pod (`main-0`), instead of running a Claude Agent SDK query per message. The SDK path is unchanged with -`MAIN_THREAD_MODE=sdk` (the default). - -- **One main thread.** The conversation is the user's most recent main-thread conversation. A message is recorded, - then delivered once through the delivery ledger; the reply is mirrored from the native journal, so the page polls - until the turn completes. While a turn (or a rotation) is in flight a second message is rejected (`409`). -- **Identity strip.** Above the chat: agent, model (from the journal), policy, native session id, Herdr pane, pod, - generation, window number, turns in the window, last context size and its baseline, and native compaction count. -- **Short window by rotation.** The native session is disposable. When the context grew by 20,000 tokens over the - window's first-turn baseline (or after 12 turns), Mainloop asks the agent to write anything durable through the - CLI (one ledgered turn), stops it, and starts a fresh native session whose start-up context is generated from - Postgres: standing context, topic index, checkpoint, open pending intent and the last 6 visible messages. The - new session's transcript contains none of the earlier conversation. Native auto-compaction is left at its - default; no compaction was observed below the rotation budget in the measured sessions (the default threshold is - assumed, not verified). -- **Dispatcher only.** The agent's only tool is Bash restricted to `mainloop ...`; it has no repository. It records - facts with `mainloop note|decide|pending`, files work with `mainloop delegate --topic ... --kind claude|codex`, - and answers "what is the child doing" from `mainloop status|read`, which read Postgres and never message the - child. On request it ends a running child with `mainloop cancel ` and tidies the user's list with - `mainloop clear` (finished children only; records are kept). -- **Topics.** A topic is a durable record (name, status line, notes, decisions, pending intent, child reports), not - a session. The topic index (names, status, pending counts) is shown under the identity strip. -- **Child reports.** A delegated child appears in the session list marked `↳` with its topic. Its - `mainloop report` (or, as a fallback, the last reply of a turn that ended without one) is recorded on the topic - and delivered to the main thread as a message. A report that arrives while the main thread is busy is queued - and delivered when it is idle. -- **Server-side policy.** At most 3 concurrent children per parent (6 in total), depth limit 2, and only the main - thread may delegate in this release; refusals are shown to the agent as `[concurrency]`, `[role]`, `[depth]`. - -- **Security limits.** The per-binding token scopes what the `mainloop` CLI may do; it is not a security boundary. - The rest of the backend API is unauthenticated and reachable from the workspace pods, and agents that share a pod - can read each other's token files, so a hostile agent could bypass the policy. Child reports are relayed to the - main thread as untrusted data (the main thread is told not to obey them). A prompt whose turn never completes - becomes `uncertain` (agent gone or after 30 minutes) and never blocks the session; a rotation closes the old - window's open deliveries the same way. - -Not implemented: topic supervisors, per-child turn budgets, approvals/attention for children, a UI for correcting -a topic assignment, and recovery of a queued or `recorded` delivery after a backend restart. +The identity strip shows the native agent, model, approval policy, native session id, configured workspace actor and readiness, delivery generation, rotation counters, journal cursor, and delivery states. Mainloop's per-binding token scopes its tool commands but is not a security boundary; backend API authorization and isolation remain separate concerns. diff --git a/docs/specs/credentials.md b/docs/specs/credentials.md new file mode 100644 index 0000000..4d19027 --- /dev/null +++ b/docs/specs/credentials.md @@ -0,0 +1,46 @@ +# Agent credentials and sign-in + +Mainloop owns one credential set per account and provider. Real Codex `auth.json` and Claude +tokens are stored in pre-created Kubernetes Secrets. The backend may seed those Secrets from +configured file paths (`SUBSTRATE_CODEX_AUTH_PATH` and `SUBSTRATE_CLAUDE_TOKEN_PATH`); file +contents are never exposed through the API or logs. Seeding applies only to an existing Secret +whose data map is empty, the uninitialized state, and subsequent reads leave the seeded value +alone. A missing Secret is a deployment error; a non-empty rejected or expired credential is +reported as such and is never silently replaced from a file. The owner must complete sign-in to +replace rejected or expired data. The egress credential provider consumes the Secret's +`injection-value` key. + +## Actor boundary + +Actors do not receive real provider credentials. Codex actors get a synthetic `auth.json` with +inert JWTs, an empty refresh token, a recent `last_refresh`, and the account ID. Claude actors +get a synthetic CLI token. The egress provider injects the current real token on approved +provider requests. The backend checks the Codex access JWT expiry and disables injection when +it is within five minutes of expiry. It does not implement a refresh-token exchange because the +request payload and rotation behavior have not been verified. + +## Attention and recovery + +If a configured credential is missing or expiring, Mainloop marks a delivery as not sent and +creates a deduplicated session attention item such as “Codex needs sign-in.” A failed native +turn whose shim status identifies an HTTP 401 or provider authentication failure also creates +that attention item. The original turn is not replayed automatically. + +From the workspace page, the owner can start a provider sign-in job. The control-side Job runs +`codex login --device-auth` or `claude setup-token`, displays a filtered HTTPS device challenge +when the CLI provides one, and sends the result to a short-lived authenticated callback. The +broker validates the result and replaces the provider Secret data. Real credentials are kept +out of job logs, challenge responses, and browser responses. + +## Implementation limits and evidence + +Broker and runner behavior is covered by fake-backed tests. The Kubernetes Job path is not live +verified. Its image must contain the matching native CLI binaries and +`/usr/local/bin/mainloop-reauth`, and must be selected with `SUBSTRATE_REAUTH_JOB_IMAGE`. The +credential egress provider remains a separate Substrate deployment contract; this spec does not +claim that publishing the Secret key alone deploys or configures that provider. No real +credentials are included in tests or fixtures. + +The native CLIs have not been live tested with the new Claude placeholder token. Re-auth job +state and callback authorization are held in backend memory; restarting the backend during a +sign-in attempt requires the owner to start that attempt again. diff --git a/docs/specs/sessions.md b/docs/specs/sessions.md index 09e9550..aa0343c 100644 --- a/docs/specs/sessions.md +++ b/docs/specs/sessions.md @@ -1,111 +1,45 @@ # Sessions -Sessions are background AI work spawned from the main thread. Each session has its own conversation and runs independently. +Sessions are native Claude Code or Codex work started from the home thread or the `/agents` page. Mainloop keeps the product session, workspace lifecycle, native binding, and message delivery as separate records. -## Session List +## Session list Desktop shows sessions in a sidebar. Mobile shows sessions in a tab. -When no sessions exist: +When no sessions exist, the list explains that sessions appear when work is delegated or started. Each session shows its title and status. Workspace health and controls appear separately from session status. -- Shows empty state with "No sessions yet" message -- Shows hint: "Sessions appear when Claude spawns background work" +| Status | Meaning | +| --------------- | ------------------------------------------------- | +| pending | Created but not yet active | +| active | A native turn is in flight | +| waiting_on_user | The agent is idle and can receive another message | +| completed | Finished successfully | +| failed | An error occurred | +| cancelled | Stopped by the user | -When sessions exist: +Cancelled and failed are final. Agent activity does not change those statuses. A child that has reported is completed; if the user sends another message, it becomes active until that turn finishes. -- Each session shows title and status badge -- Active count shown in header (e.g., "2 active") -- Clicking a session opens its detail view +## Creating and messaging sessions -## Status Badges - -| Status | Badge | Meaning | -| --------------- | ----------- | ------------------------ | -| pending | PENDING | Queued, not started | -| active | ACTIVE | Currently running | -| waiting_on_user | NEEDS INPUT | Blocked on user response | -| completed | DONE | Finished successfully | -| failed | FAILED | Error occurred | -| cancelled | CANCELLED | Stopped by the user | - -Failed sessions show error message below the badge. - -Cancelled and failed are final: an agent's later activity never changes them. For a native agent session, -"active" is an open turn and "NEEDS INPUT" is an idle agent waiting for the next message. A delegated child that -has reported is DONE (it shows its report as the summary); messaging it again makes it active until the reply, then -DONE again. +- `/agents` offers Claude Code and Codex. `POST /sessions` accepts `agent_kind`; when omitted, it defaults to Claude Code. +- Native CLI execution lives in the Substrate actor selected by the configured provider binding. Mainloop's credential broker owns real provider credentials in control-side Secrets; actors receive synthetic placeholder files only. Mainloop does not create a Claude SDK worker for each session. +- Each user message is recorded with a delivery state before it is sent. Delivery states include `recorded`, `sending`, `delivered`, `completed`, `queued`, `failed`, and `uncertain`. +- An uncertain delivery is never replayed automatically. A message is rejected with `409` while another turn is in flight or while the workspace is suspending or suspended. +- Session conversations mirror messages and turn evidence from the native journal. ## Cancelling and clearing -Implemented; covered by unit tests with fakes (status rules, the agent verbs' policy), not yet exercised against a live cluster. - -- **Cancel** (session view, live sessions only) ends the session and stops its agent. If Mainloop cannot confirm the - agent stopped it says so; the session is still cancelled and the stop is not retried blindly. A cancelled session - no longer accepts messages. -- **Clear** removes finished sessions (done, failed, cancelled) from the list: "Clear" on a finished session's view, - or "clear N" in the list header for all of them. Cleared sessions are kept for audit, never deleted. A live - session cannot be cleared; cancel it first. The main thread's own conversation is never listed or cleared. -- The main thread can do both for its children: `mainloop cancel ` and `mainloop clear []`. Only the main - thread may; a child agent is refused. - -## Session Detail View - -Clicking a session navigates to `/sessions/{id}`: - -- Shows title as h1 heading -- Shows description if present -- Shows the session's chat directly (there is no Logs tab) -- Live sessions show Cancel; finished ones show Clear (see "Cancelling and clearing") -- Shows a one-line identity summary (agent, model, live or idle, topic) that expands to the full identity strip -- Follows the URL: opening another session from the list switches to it -- The session open in the main pane is highlighted in the list -- Active sessions show Cancel button -- Completed sessions show Summary section -- Failed sessions show Error section -- Back button returns to home -- Non-existent session ID shows "Session not found" with link to home -- When the backend is unreachable the page says so and retries when it returns, instead of "Session not found" - -## Notifications - -When a session needs attention: - -- Toast notification appears with title and preview -- Clicking notification navigates to that session's detail view - -## Native Agent Sessions (implemented in the local kind slice; not production) - -`/agents` ("new agent session" in the header, "+ agent" in the session list) starts a session bound to a real -agent instead of the session worker: +- Cancel ends the session and asks the actor to stop the native turn. If Mainloop cannot confirm the stop, it reports that result and does not repeat the stop blindly. +- A cancelled session no longer accepts messages. +- Clear archives finished sessions for audit. Live sessions must be cancelled first. +- The main thread can cancel or clear its child sessions through the `mainloop` tools. -- Choose **Claude Code** or **Codex**, an optional title and a first message. The session is created with - `agent_kind` (`POST /sessions`); no Job or namespace is created. -- The agent runs under Herdr in the workspace pod in bypass-permissions mode. The approval policy is recorded on - the binding and shown in the UI. -- The session detail view shows an identity strip: agent kind, model (read from the native journal), approval - policy, native session id, Herdr pane, workspace pod (short UID, ready or not), whether the agent process is - live, the ownership generation, the journal file and cursor, and the state of each delivery. -- Delivery states: `recorded`, `sending`, `delivered`, `completed`, `failed` (nothing sent), `uncertain` - ("delivery unknown"). A prompt is sent once. If the outcome is unknown the UI says so and waits for the user; - it never resends automatically. While a turn is in flight a second message is rejected (`409`). -- Replacing the pod keeps the conversation: the agent is shown as "not running (resumes on next message)"; the next - message restarts it with the native resume flag against the same native session id (generation increases) and - then delivers the message. +## Session detail -Measured with real agents (Claude Code 2.1.278, codex-cli 0.155.1) on the local kind cluster only; see -`docs/spikes/k8s-herdr-agents.md`. Known gaps: the header status badge is loaded once, message text is rendered -as markup (angle brackets in messages disappear), and there is no way to mark an `uncertain` delivery resolved. +The session view shows the conversation, session status, and a native identity strip with the agent kind, model, approval policy, native session id, configured workspace actor, readiness, generation, journal cursor, and delivery states. Workspace health and lifecycle controls are shown separately. -## Delegated child sessions (implemented in the local kind slice; flag `MAIN_THREAD_MODE=native`) +Opening a session follows its URL. Missing sessions show a not-found state. If the backend is unavailable, the page retries instead of treating the session as missing. -A session started by the native main thread (`mainloop delegate`) is a child: it has a parent (the main thread), -a topic, and runs as a native Claude Code or Codex agent in its own scratch directory under Herdr in `workspace-0`. +## Evidence boundary -- The session list marks children with `↳` and `#`; the identity strip shows role, parent and topic. -- The child receives one task brief (a ledgered delivery with source `brief`); Mainloop never sends it the - parent's transcript. -- The child ends with `mainloop report --summary` (size-capped). The report is recorded on the topic as evidence - and delivered to the main thread. If a turn ends without a report, its last reply is reported with a - "fallback" label. -- You can still message a child directly from its session view; that is an ordinary ledgered delivery. -- The main thread's own binding is not listed as a session; it is the home conversation. +Native journal parsing, delivery handling, and Substrate transport tests use sanitized fixtures and fake routers. The earlier Kind session-resume proof is retained as historical evidence in `docs/spikes/k8s-herdr-agents.md`; it does not prove the current Substrate runtime. diff --git a/docs/specs/workspaces.md b/docs/specs/workspaces.md new file mode 100644 index 0000000..a82e231 --- /dev/null +++ b/docs/specs/workspaces.md @@ -0,0 +1,113 @@ +# Workspaces + +Workspaces are runtime resources attached to sessions. A project branch workspace has its own +Substrate actor and lifecycle. Workspace lifecycle is separate from the session's task status, +native-agent activity, message delivery, user attention, and publication state. + +Workspace actors currently use Substrate's gVisor sandbox; microVM isolation is deferred. The +Mainloop agent image runs as UID `10001`, and the pinned Substrate fork honors that image user. +Non-root actor startup, a headless Claude turn, and a durable workspace volume surviving +suspend and resume were live checked on Kind with fork commit `0f9635ae`, using the spike's proof +scripts rather than the Mainloop API. + +## Lifecycle + +Mainloop records desired state (`running` or `suspended`), observed state, conditions, the last +observed transition, an operation ID, the last known snapshot reference, and the last activity +time. Mainloop owns desired state; Substrate is the source of actor observations. + +| Observed state | User label | Meaning | +| ---------------------------------------- | ---------------------------- | -------------------------------------------------------------------------- | +| `running` | RUNNING | Substrate reports the actor running. | +| `suspending` | SUSPENDING | Substrate is suspending the actor. | +| `suspended` with a snapshot reference | PARKED | Substrate reports suspension and Mainloop has a snapshot reference. | +| `suspended` without a snapshot reference | SUSPENDED · SNAPSHOT UNKNOWN | Actor suspension is observed, but parking is not confirmed. | +| `resuming` | RESUMING | Substrate is restoring the actor. | +| `failed` | FAILED | Substrate reports a crashed/deleting actor, or the bound actor is missing. | +| `unknown` | UNKNOWN | A transport or unrecognized actor state prevents a reliable conclusion. | + +The UI shows workspace state wherever sessions are listed and links from the session detail to +`/workspaces/{id}`. The workspace page shows the manifest, conditions, transition time, snapshot +reference, idle timeout, and last activity, with suspend, resume, refresh, and delete controls. +Session badges and session status are not changed by workspace operations. + +The page also lists allowed preview ports with **Open preview** links and shows Codex and Claude +credential status with a sign-in action. Sign-in challenges are displayed on this page; the +session attention item links the owner here when an agent needs authentication. + +Opening the workspace page does not wake a parked actor to discover live ports; it lists ports +from the manifest until the workspace is running. A preview request itself may wake the actor. + +## API + +- `GET /workspaces` lists the current user's workspace lifecycle records. +- `GET /workspaces/{id}` returns one workspace lifecycle and manifest. +- `POST /workspaces` accepts a project ID, branch, and strict dev manifest, then provisions one + actor from its declared actor template or the configured default template. It also creates a + native-agent binding that routes the workspace session to that actor. An optional top-level + `agent_kind` (`claude` or `codex`, default `claude`) selects the binding's native agent. +- `POST /workspaces/{id}/suspend` records the desired state and requests suspension. +- `POST /workspaces/{id}/resume` records the desired state and requests resumption. +- `POST /workspaces/{id}/refresh` reads Substrate status without changing desired state. +- `POST /workspaces/{id}/touch?reason=preview` records activity and wakes a suspended workspace. + The preview proxy can call the same `touch_workspace(workspace_id, reason)` service API. +- `DELETE /workspaces/{id}` deletes the actor and its shim token Secret. Open deliveries return + `409`; an unconfirmed Substrate deletion keeps the durable workspace binding for reconciliation. +- The control plane generates one shim bearer token per actor, stores it in the configured shim + Secret namespace, and installs it through the actor shim's one-time bootstrap endpoint. +- `GET /workspaces/{id}/ports` lists declared or shim-reported preview ports and their URLs. +- `GET /workspaces/{id}/credentials` reports provider availability and expiry metadata only. +- `POST /workspaces/{id}/credentials/{provider}/reauth` starts a bounded control-side sign-in Job. +- `GET /workspaces/{id}/credentials/reauth/{job_id}` returns job state and a filtered device challenge. +- Lifecycle changes are published through the existing event stream as `workspace:updated`. + +Preview hosts have the form `--.preview.`. Mainloop checks workspace +ownership and permits only ports declared by the manifest or reported by the authenticated +actor shim. The preview listener accepts identity only from the Cloudflare Access email header +when `SUBSTRATE_PREVIEW_TRUSTED_INGRESS=true`; that ingress must remove any client-supplied copy +before setting its authenticated value. It never treats `X-User-ID` as identity. Without trusted +ingress, requests receive `401` (WebSocket close `4401`). The Kind overlay alone enables +`SUBSTRATE_PREVIEW_LOCAL_DEV_MODE=true`, which uses the fixed local development owner. + +The proxy strips browser cookies, authorization, and identity headers before using the router's +CONNECT stream, including WebSocket upgrades. It retries only a router CONNECT failure before +forwarding the request to the actor. After forwarding, a disconnect has an unknown outcome and +the proxy does not replay the request. HTTP streams read at most 64 KiB per block and use a +bounded socket read timeout. Active HTTP streams and WebSocket connections refresh workspace +activity every 20 seconds until they close, so idle suspension waits for active previews to end. + +Suspend is refused while the native delivery ledger contains a recorded, queued, sending, +delivered-but-incomplete, or uncertain delivery. The API reports `409` with the reason. A +transport timeout is stored as `unknown`; the UI asks the owner to refresh status, and Mainloop +does not replay the operation without inspecting the actor first. The durable workspace +generation is advanced with a compare-and-swap before a lifecycle control call. + +## Declarative manifest + +Each workspace exposes repository URL and branch, allowed agent kinds (`claude` and `codex`), +skill and MCP references, an egress host allowlist, a resource class, and an optional `dev` +section. `dev` requires exactly one of `image` or `devcontainer_ref`; it can declare an actor +template, sibling services (`name`, `image`, `env`, and numeric ports), HTTP preview ports +(`name`, `number`, `protocol`), and an idle timeout from 1 to 1440 minutes. Unknown fields, +duplicate service or port names, duplicate ports, and invalid timeouts are rejected by the +shared strict model. + +The project page creates a workspace per branch and lists each workspace independently. Turn, +delivery, and preview activity use the durable last-activity timestamp. Mainloop's existing +reconcile loop checks idle workspaces once a minute and suspends expired workspaces through the +fenced lifecycle operation; open deliveries still block suspension. Services and image values +are declared by the project manifest and must match its configured actor template. + +## Scope and evidence + +`POST /workspaces` provisions an actor from its declared template. Other lifecycle, preview, and +credential endpoints operate on an existing Substrate workspace binding. Runtime behavior is +covered by fake-backed tests; this specification does not claim a live cluster integration proof. + +The sample under `examples/devenv-sample/` documents the intended Node plus Postgres project +manifest shape. + +The wildcard preview proxy and WebSocket tunnel are not live cluster verified. Kind uses the +preview Service and a local port-forward; the browser-visible `*.preview.localhost` hostname +depends on that tunnel. The credential Job also requires a configured CLI image as described in +`credentials.md`. diff --git a/docs/spikes/k8s-herdr-agents.md b/docs/spikes/k8s-herdr-agents.md index 4f17cf7..c184296 100644 --- a/docs/spikes/k8s-herdr-agents.md +++ b/docs/spikes/k8s-herdr-agents.md @@ -1,6 +1,10 @@ # Spike: Herdr-owned arbitrary agents in a Kubernetes workspace -Status: local spike, not a product feature. Implementation lives in `spikes/k8s-herdr-agents/`. +> Cutover note (2026-09-24): Herdr was removed; Substrate is now the only workspace runtime. + +Historical local spike, not a current product feature. Its implementation under +`spikes/k8s-herdr-agents/` was removed during the 2026-09-24 Substrate cutover; this document keeps +the original proof evidence and limitations. ## What it shows diff --git a/docs/spikes/native-main-thread-context.md b/docs/spikes/native-main-thread-context.md index a977d9e..d091a45 100644 --- a/docs/spikes/native-main-thread-context.md +++ b/docs/spikes/native-main-thread-context.md @@ -1,9 +1,14 @@ # Native main thread and cross-session context (plan r7) +> Cutover note (2026-09-24): Herdr was removed; Substrate is now the only workspace runtime. + Status labels: **Implemented** = built and exercised on the local kind cluster; **Measured** = observed with real agents (Claude Code 2.1.278, codex-cli 0.155.1); **Proposed** = design intent not yet built. Nothing here is production-tested. Fixtures and fakes cover the default tests; live evidence is outside the repository. +Historical note: this plan predates the 2026-09-24 cutover. The mode flag, former workspace transport, and SDK +fallback described below have been removed. Current chat always uses the native Substrate session path. + ## What was built (Implemented) - `MAIN_THREAD_MODE=native`: `POST /chat` records the user message and delivers it, through the r6 delivery ledger, diff --git a/docs/spikes/substrate-preview-quickstart.md b/docs/spikes/substrate-preview-quickstart.md new file mode 100644 index 0000000..fbf2f4c --- /dev/null +++ b/docs/spikes/substrate-preview-quickstart.md @@ -0,0 +1,63 @@ +# Mainloop Substrate preview + +> Cutover note (2026-09-24): Herdr was removed; Substrate is now the only workspace runtime. + +This local Kind preview runs Mainloop's backend, frontend, and PostgreSQL in `mainloop-control`. The backend uses the headless Claude and Codex actors already provisioned in the preview cluster. This overlay does not create actors or include shim-token values. + +## Prerequisites + +- The `kind-substrate-preview` cluster, `localhost:5001` registry, Substrate router ingress policy, and headless actors are ready. The cluster lane creates the two referenced `mainloop-shim-*` Secrets in `mainloop-control`, each with a `token` key; the deploy script does not create them. +- Docker, `kubectl`, `kubectl-ate`, `curl`, and `openssl` are installed. Docker can reach the local registry. +- The preview kubeconfig is available at `/tmp/substrate-preview-kubeconfig`. + +Set the kubeconfig path once in the shell: + +```bash +export SUBSTRATE_PREVIEW_KUBECONFIG=/tmp/substrate-preview-kubeconfig +``` + +## Build and deploy + +```bash +scripts/substrate-preview.sh build +scripts/substrate-preview.sh deploy +scripts/substrate-preview.sh status +``` + +`build` pushes the backend, frontend, and mirrored PostgreSQL images to `localhost:5001`, checks each pushed manifest by digest, and records those digests in `$XDG_STATE_HOME/mainloop/substrate-preview/image-digests` (or `$HOME/.local/state/mainloop/substrate-preview/image-digests` when `XDG_STATE_HOME` is unset). Run `build` before `deploy`; deploy fails if that state file is missing or invalid. `deploy` renders a temporary copy of the overlay with the recorded digests, then applies it to context `kind-substrate-preview`; the tracked overlay remains tag-based. On first deploy, it creates `mainloop-db-app` with a random password if that Secret is absent. The password is not printed, and subsequent deploys keep the existing Secret. + +## Open Mainloop + +Run this in a terminal and leave it running; Ctrl+C stops both port-forwards: + +```bash +scripts/substrate-preview.sh open +``` + +Open the printed frontend URL. The backend API docs are at `http://127.0.0.1:8000/docs`. + +## Watch actors and logs + +The installed `kubectl-ate get actor` command does not provide a watch flag. Poll both atespaces every two seconds to see actors suspend and resume: + +```bash +watch -n 2 'kubectl-ate --kubeconfig "$SUBSTRATE_PREVIEW_KUBECONFIG" --context kind-substrate-preview get actor --all-atespaces' +``` + +In another terminal, follow backend logs (or choose `frontend` or `postgres`): + +```bash +scripts/substrate-preview.sh logs +scripts/substrate-preview.sh logs frontend +``` + +`scripts/substrate-preview.sh status` prints Mainloop pods, Substrate actors, and WorkerPools. + +## Tear down + +This deletes the `mainloop-control` namespace, its local PostgreSQL data, and shim-token Secrets there. It leaves Substrate actors and cluster-level services alone. + +```bash +kubectl --kubeconfig "$SUBSTRATE_PREVIEW_KUBECONFIG" --context kind-substrate-preview \ + delete -k k8s/apps/mainloop/overlays/substrate-preview +``` diff --git a/docs/spikes/substrate-workspace-adapter.md b/docs/spikes/substrate-workspace-adapter.md new file mode 100644 index 0000000..21cb621 --- /dev/null +++ b/docs/spikes/substrate-workspace-adapter.md @@ -0,0 +1,590 @@ +# Spike: Substrate as Mainloop's Kubernetes workspace runtime + +> Cutover note (2026-09-24): Herdr was removed; Substrate is now the only workspace runtime. + +Status: local spike, not a product feature. Adapter code lives in +`backend/src/mainloop/runtime/substrate.py` and `workspace_adapter.py`; the actor manifest +lives in `spikes/substrate-workspace-adapter/`. The earlier live proof used a Herdr-backed +actor; the target described by this closeout has no Herdr server or terminal manager in the +actor and invokes native CLIs headlessly once per turn. See +`docs/spikes/k8s-herdr-agents.md` for the historical native-session/Herdr spike. + +Historical scope: the Phase 5 closeout records evidence and design as of 2026-09-23. The +2026-09-24 cutover made Substrate the only runtime and removed the `WORKSPACE_RUNTIME` switch and +its former default. Lane A run 5's durable restore and run 6's Claude turn measurements are dated +addenda in the gate table; other results remain evidence for the versions and actors actually +measured at their run times. + +## Current status — Phase 5 closeout (2026-09-23) + +The Round 3 and Phase 4 runs measured Substrate actors, Cilium-enforced router ingress, +per-actor shim tokens, Envoy hostname egress, and native sessions for both Claude Code and +Codex in the earlier Herdr-backed actor. Both agents completed a native turn and recalled a +nonce in the same session after suspend/resume. Measured suspend/resume times were 393/408 ms +for Claude and 573/789 ms for Codex. Those results do not prove the headless per-turn actor +model. Its rebuilt image is **not yet live-proved**. The Codex file marker was not confirmed in +the recorded run; its follow-up result, Claude recall after worker loss, and Claude history +after revert remain pending in the cluster lane. + +The earlier Codex API HTTP 503 came from Envoy selecting unreachable IPv6 upstream addresses +when its DNS caches used `ALL`; `V4_PREFERRED` corrected that path. A separate ChatGPT SAN +failure was traced by reviewer-provided evidence to cross-SNI upstream TLS session reuse in +Envoy 1.39.1. After applying both settings, a credential-free probe returned the expected API +401 and ChatGPT 200 with no TLS verification failures, then the native Codex turn and recall +completed. Native-session support behind the Phase 3 boundary is proved for both agents in the +earlier actor design. The requested headless actor design remains unproved, the broader +continuity gate is partial, and production adoption is deferred pending live proof of its +rebuilt image and the requirements below. + +## What it shows + +[Substrate](https://github.com/agent-substrate/substrate) can provide the per-session isolated +compute Mainloop's roadmap calls for ("Workspace platform"), while Mainloop stays the durable +owner of session-to-actor mapping, delivery, and audit state. The target actor has no Herdr +server or terminal manager; Mainloop delivers each turn to a headless native CLI process. The +actor holds files between turns, with no attachable TUI; callers watch progress through streamed +events. Lane A run 5 measured the target image running as UID 10001 and restoring `/work/repo` +after both WorkerPool pods were deleted. Lane A run 6 then measured a Claude native turn and +same-session recall across suspend/resume on the headless actor. Claude continuity after worker +loss and native Codex behavior on this actor remain unproved. Existing live lifecycle and +native-session results below came from the earlier Herdr-backed actor and are historical evidence +for Substrate and the Phase 3 boundary. + +The headless shim exposes authenticated `POST /turn` and `GET /turn/:id` endpoints. Prompts are +sent to the native CLI on stdin; the shim stores bounded JSONL events per turn and reports the +native session/thread id and final message. It permits one in-flight turn per agent and returns +409 instead of queueing. `POST /run` starts a shell command with a bounded timeout and actor-local +output file; `GET /run/:id` reports its status and bounded output. `/healthz` and `/readyz` check +only the shim and workspace. Lane A run 5 exercised `/run`, `/healthz`, and durable workspace +restore; run 6 exercised Claude `/turn`, same-session recall, concurrent-turn rejection, and +credential-free health on the headless actor. + +## Pre-cutover Substrate configuration (2026-09-23) + +At the time of this spike, `WORKSPACE_RUNTIME=substrate` selected the native-session transport and +the former runtime was the default. The cutover removed this switch; this paragraph is retained as +historical configuration context only. +`SUBSTRATE_ROUTER_ADDRESS` configures the HTTP CONNECT listener. `SUBSTRATE_ACTOR_BINDINGS` is a +JSON object keyed by `claude` and `codex`; each entry supplies `atespace`, `actor`, and +`shim_token_secret_name`. `SUBSTRATE_SHIM_SECRET_NAMESPACE` selects the Secret namespace and +defaults to `mainloop-control`. The backend reads the Secret's `token` key there and keeps the +value out of logs and config. Actor names and Secret names stay in deployment config; Mainloop +attaches only to the named, pre-created actor. A 401 refreshes the cached token and retries +read/status operations once; a rejected `POST /turn` clears the cache and is never replayed. + +The adapter routes turns and status through the authenticated shim, verifies the selected CLI +credential is installed before starting a session, then mirrors replies and completion from native +session journals. The shim's bounded `GET /journal` endpoint pages Claude transcripts and Codex +rollout files. This is fixture-backed transport behavior; it does not add live-proof claims for the +headless image or its current actors. + +## Earlier real-versus-stand-in inventory (before Round 3) + +| Layer | Status | +| --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| kind cluster `substrate-preview`, pinned Substrate `cdac9baef8...` (ate-system + agentgateway dataplane) | Real | +| `mainloop-workspace` WorkerPool + ActorTemplate, actor create/get/resume/suspend/revert/delete | Real, driven through `backend/src/mainloop/runtime/substrate.py`'s actual code (not a separate probe script's own CLI calls) | +| `preview-gate` WorkerPool + ActorTemplate: real Herdr server, real Vite dev server, real NGINX header-proxy, real browser (`agent-browser`), real WebSocket HMR | Real | +| `dev-service-gate` WorkerPool + ActorTemplate: real `psql`, real external `postgres:16-alpine` StatefulSet, real `EgressPolicy` (CIDR rule, created via a small gRPC tool since `kubectl-ate` has no CLI verb for it) | Real | +| Herdr + `agentctl` inside the actor images | Real (same image contents as `spikes/k8s-herdr-agents`), without the real Claude/Codex CLIs | +| File edits and shell commands run inside actors (a generic `herdr pane run` shim, not a native agent's own Bash tool) | Stand-in -- see "Why not a real agent" below | +| `live-agent-gate` WorkerPool + ActorTemplate: real Claude/Codex CLIs, credential-free boot | Golden snapshot READY and actor RUNNING proved live. A later credential-free preview actor hit the pinned gVisor restore error described below. No provider session ran. | +| Claude/Codex agent processes, credentials | Not run; the 2026-09-23 live lane stopped before credential work (see "Limits") | +| `workspace_bindings` durable mapping (Postgres) | Fixture/unit-tested only; not exercised against a live backend + database in this run | + +## Preview-gate edit path and Round 3 credential boundary + +Substrate's pinned commit has no generic secret-injection mechanism equivalent to a Kubernetes +Secret volume/env mount. `ActorTemplate` container env values are literal only (no +`envFrom`/`valueFrom`, and the template is immutable, so baking a token in would also mean +storing it permanently in a control-plane object -- unacceptable under this task's "credentials +by path, never by value" rule). `SystemInfo` volumes are limited to actor metadata and an +allowlisted CA bundle. `CredentialProvider` is an egress-gateway plugin, keyed by actor SPIFFE +identity, that injects a credential into an outbound request; it does not hand a token to the +CLI's local environment or filesystem. + +The pinned commit also has experimental static-header injection from a Kubernetes Secret URI +into decrypted outbound requests. HTTPS hostname rules require the Envoy sdsmint overlay and +`--experimental-egress-credential-injection`; the plain Envoy overlay has no MITM egress, and +agentgateway does not implement this injection path. The Envoy 1.39.1 router crash was from the +earlier `cdac9ba` install; it does not describe the `0f9635ae` preview, which ran Envoy with +sdsmint and the actor-bound credential provider. Claude's credential was injected on the upstream +leg and stayed out of actor snapshots. Codex instead received `auth.json` through the authenticated shim because +Codex refreshes that file locally; its actor snapshots therefore contain that credential. Moving +Codex credentials out of snapshots remains a production requirement. Neither credential value +was logged or copied into a golden snapshot, and the old unauthenticated relay was not used. + +The Round 3 egress run also found that `dns_lookup_family: ALL` selected unreachable IPv6 +addresses on the IPv4-only Kind node. Setting the five egress DNS caches to `V4_PREFERRED` +allowed the credential-free API and ChatGPT probes to reach upstreams. A separate +reviewer-provided diagnosis tied the earlier ChatGPT SAN failures to cross-SNI TLS session +resumption; setting `max_session_keys: 0` on the four approved upstream TLS clusters preserved +SAN verification and the later probe passed. These are Envoy-specific settings; they do not make +agentgateway hostname enforcement safe. + +The preview/HMR edit itself still uses a generic exec shim +(`spikes/substrate-workspace-adapter/image/exec-shim.js`) that pastes text into a real Herdr shell +pane via `herdr pane run` -- a real shell executing a real command, but not a native agent's own +Bash tool. + +## Substrate source + +Mainloop targets Substrate fork [`oldsj/substrate`](https://github.com/oldsj/substrate), +branch `patched-next`, pinned at +`0f9635aed37bd5dde604a9bca1975421cd07181a`. The fork carries no Mainloop-specific code. +It is rebased on upstream `14c0c136bc3fda4d8e67de3851c087a28a2e754b` with the fork patches +that run actor containers as the image user in its working directory, preserve image and +non-root layer ownership, and assign fresh durable volumes to the first writable non-root user. +When `backend/scripts/gate5_setup.py` resolves a `ko://` WorkerPool image from source, it +requires this exact commit; the live proof scripts use a digest-pinned WorkerPool image instead. +The evidence below records the commit each result was measured on; results before the fork were +measured on upstream `cdac9ba`, the historical Lane A rows are labelled `ce265c1d`, and the new +Lane A rows are labelled with fork `patched-next` commit `0f9635aed37bd5dde604a9bca1975421cd07181a`. + +The adapter changes for `patched-next` read the actor template UID from +`status.externalSnapshot.actorTemplateUid`, use ActorTemplate fields `wakeupProbe` and +`snapshotConfig` in place of `readyz` and `snapshotsConfig`, and include the current +`metadata.uid` and `metadata.version` as egress-policy update preconditions. The API has no +egress-policy delete operation; the Claude proof revokes credential injection by updating the +policy to an empty rule set. + +Run these from the pinned Substrate checkout. Credential injection requires the Envoy dataplane; +`ate-setup` rejects the injection flag with agentgateway. The `0f9635ae` preview used Envoy with +sdsmint and both credential-provider overrides: + +```sh +cd "$SUBSTRATE_SRC" +export VERSION=0f9635ae +export KO_DOCKER_REPO=localhost:5001 +KUBECONFIG_PATH=/tmp/substrate-preview-kubeconfig + +go run ./cmd/ate-setup --kind --kubeconfig "$KUBECONFIG_PATH" --context kind-substrate-preview \ + --atenet-dataplane envoy --experimental-use-sdsmint --experimental-egress-credential-injection \ + --credential-provider-name ate-secret://kubernetes.io \ + --credential-provider-address credprovider.mainloop-control.svc:50051 \ + deploy ate-system +go run ./cmd/ate-setup --kind --kubeconfig "$KUBECONFIG_PATH" --context kind-substrate-preview \ + --atenet-dataplane envoy --experimental-use-sdsmint --experimental-egress-credential-injection \ + --credential-provider-name ate-secret://kubernetes.io \ + --credential-provider-address credprovider.mainloop-control.svc:50051 \ + deploy atenet +``` + +At `0f9635ae`, the defaults are `ate-secret://k8s.io` and +`k8s-credential-provider.ate-system.svc:50051`. Redeploying without the overrides breaks the +Claude provider preflight with HTTP 500: `credential URI names a provider this gateway does not +serve`. + +## Run it + +There is no single demo script yet (unlike `spikes/k8s-herdr-agents/demo.sh`); the commands used +are recorded in the task's proof note. In outline: + +```bash +# SUBSTRATE_SRC is a checkout of oldsj/substrate at the pinned commit above. +KIND_CLUSTER_NAME=substrate-preview KUBECONFIG=/tmp/substrate-preview-kubeconfig \ + "$SUBSTRATE_SRC"/hack/create-kind-cluster.sh +# Deploy ate-system and atenet with the `ate-setup` commands above. +# build kubectl-ate, build+push an actor image, apply one of: +# k8s/actor-template.yaml.tmpl -- mainloop-workspace: headless native-agent shim +# k8s/preview-gate-template.yaml.tmpl -- preview-gate: real Vite dev server + exec shim +# + k8s/preview-proxy.yaml.tmpl -- the NGINX ate-target-actor header-proxy in front +# k8s/dev-service-gate-template.yaml.tmpl -- dev-service-gate: real psql + exec shim +# + k8s/postgres-target.yaml -- the external postgres:16-alpine StatefulSet +# + `kubectl ate create egress-policy` -- the actor's EgressPolicy +# (WorkerPool via `ko resolve | kubectl apply`, ActorTemplate via `kubectl ate create actor-template -f -`) +``` + +## Observed behaviour + +- Historical for the earlier `cdac9ba` install: the default Envoy-based `atenet-router` + crash-looped, and the `agentgateway` dataplane fixed startup. The `0f9635ae` preview used + Envoy with sdsmint for credential injection. +- A freshly created actor starts `SUSPENDED`, not running -- `create_actor` never implicitly + starts an actor. An explicit `resume_actor` is required, and it returned `RUNNING` directly + (no further polling needed) in every observed case. +- Force-deleting an actor's worker pod (`kubectl delete pod ... --grace-period=0 --force`, the + same technique as the prior proof's "abrupt worker loss" trial) drove the actor to `CRASHED` + within a few seconds, correctly observed as `WorkspaceBinding.observed_state="unavailable"` + through `substrate.py`'s real `ActorState` -> `observed_state` mapping. +- `revert_actor` on a `CRASHED` actor returned it to `SUSPENDED` from its last completed (here: + golden) snapshot; a subsequent `resume_actor` brought it back to `RUNNING`. Nothing in the + adapter reverts automatically -- `workspace_adapter.revert_workspace` requires + `acknowledge_loss=True`. +- Actor logs for a resume showed gVisor's `runsc ... restore -image-path ... restore-state` + path (`"Actor restoring"` / `"Actor restored"`), not a fresh container boot -- consistent with + the golden snapshot's process state (including the running `herdr` server) being restored + rather than the entrypoint re-running. This is supporting evidence for gate 5 (native-session + continuity) but not a full proof: no real agent session was resumed and asked to recall a + pre-suspend nonce in this run. +- Building this adapter against the real CLI found one bug fixed in the same commit: + `create_actor` was missing `-o json` and crashed parsing `kubectl ate`'s default table output. + +### Preview/HMR gate (gate 3): proved live, three real bugs isolated and fixed + +Through the actual intended route (real browser -> NGINX header-proxy -> `atenet-router` +(agentgateway) -> a real Vite dev server in a real actor), with a real WebSocket HMR socket open +the whole time: a real shell write (via the exec-shim's `herdr pane run`, not a purpose-built +`/__edit` endpoint) to `main.js` produced a genuine in-place HMR update -- confirmed by a +`window.__hmrMarker` value set before the edit surviving after it (a full reload would have reset +it) and by the console logging `[vite] hot updated: /main.js`. This held across an explicit +suspend/resume cycle too: content and the marker's own page state survived, and Vite's client +logged `server connection lost. Polling for restart...` during the suspend and reconnected +cleanly on resume, with a further post-resume edit still hot-updating correctly. + +Getting there required isolating and fixing three independent, real bugs -- exactly what the +prior Kind preview proof asked for ("isolate ... rather than re-measuring as one blob"): + +1. **NGINX's `proxy_pass` defaults to HTTP/1.0 upstream**, which silently breaks `Connection: +Upgrade`. Symptom: `503 upstream call failed: SendRequest: connection closed before message +completed` from `atenet-router`, which looked like a router bug until isolated by testing the + same header-routed request directly against the router (works) versus through NGINX (fails). + The Jupyter demo's own `nginx.conf` (the pattern this proxy was copied from) has the same gap. + Fix: add `proxy_http_version 1.1;`. +2. **A hardcoded `hmr.clientPort` pointed the browser's WebSocket at the actor's internal port + (80), not the port the browser actually reached the proxy on.** Symptom: `[vite] failed to +connect to websocket (Error: WebSocket closed without opened.)` in the real browser, while a + raw `curl` WebSocket upgrade against the same actor succeeded (isolating it to the _browser's_ + target URL, not the routing path). Fix: do not set `hmr.clientPort`; let Vite infer it from + `window.location`, which is correct for same-origin proxying. +3. **A plain shell-redirect truncate-in-place write (`cmd > file`) was never observed by Vite's + file watcher on this gVisor-sandboxed filesystem, with or without `usePolling`; an atomic + rename-replace write (`sed -i`, or any editor/tool that writes-then-renames, which is how most + real editors and Node's own atomic-write helpers behave) was picked up every time.** This was + isolated by holding the watcher config fixed and varying only the write method. The initial + hypothesis (inotify does not work under gVisor) was wrong and is corrected here rather than + left standing: the default inotify-based watch picked up `sed -i` edits fine, with or without + polling enabled. `usePolling` is kept in the fixture's `vite.config.js` as defense in depth, + but it was not the actual fix. + +None of these three are Substrate bugs in the sense of "broken by Substrate" -- (1) is a gap in +the demo NGINX pattern this repo's own docs show, (2) is a Vite config default that does not +suit a proxied deployment, and (3) is a filesystem-semantics fact worth knowing about (most real +editors already write this way, so it may not affect a real native-agent's edits, which is +exactly why gate 5's live proof matters and was not reached in this run). + +### Dev-service gate (gate 4): proved live, including real policy enforcement + +A real `postgres:16-alpine` StatefulSet (same image/auth shape as +`k8s/apps/mainloop/overlays/test/postgres-statefulset.yaml`) in its own namespace, reached from a +`dev-service-gate` actor (real `psql`, driven through the same generic exec shim) under an +`EgressPolicy` scoped to exactly the Postgres Service's `/32` ClusterIP. + +`kubectl-ate` has **no CLI verb for egress policies** -- confirmed by the pinned checkout's own +`demos/egress/README.md`: `"test-egress.sh creates and resumes the Actor but cannot create its +EgressPolicy (no CLI verb yet)"`. Its own e2e suite calls the gRPC API directly +(`internal/e2e/egresspolicy.go`). This spike originally did the same with a small gRPC tool. +Upstream has since added `kubectl ate get` and `create egress-policy`, and the fork adds +`update`, so `gate5_setup.py` now writes the policy manifest and applies it with those verbs. + +**Result**: DNS resolution (bypasses the policy enforcement point entirely -- port 53 is always +allowed), a real `SELECT` query over the actual Postgres wire protocol, and reconnection after an +explicit suspend/resume cycle (a second query, `SELECT 43`, succeeded cleanly post-resume, no +policy re-creation needed -- the policy is attached to the actor, not the connection) all worked +on the first try. Authorization is real, not merely passive: a request to a _different_ Service's +ClusterIP (not covered by the `/32` rule) was cleanly rejected -- +`HTTP 403 actor egress policy denied destination` from the egress gateway itself, not a silent +timeout or a security-group-shaped ambiguity. This is a materially better outcome than the prior +Kind preview proof's own external-backend trial (`403 -> 503`, "reconnection after wake was +therefore not proved") -- the difference was using a **CIDR rule** (works for any TCP protocol +per `docs/network-egress.md`'s "CIDR/all policy: dial now" passthrough path) instead of a +**hostname rule** (HTTP/TLS-SNI-specific, and Postgres is neither), which the prior proof's HTTP +`fetch`-based trial did not have reason to distinguish. + +## Historical limits from the earlier Phase 2 checkpoint + +- **Native-session gate, live at that checkpoint**: no Claude or Codex session ran then. The + owner authorized the work; a tool policy rejected particular actions after the old relay + returned HTTP 403. The relay was removed. The first Phase 3 preview restore error was later + diagnosed as a dead app from the old image and missing readiness probe, then corrected with a + new image and template. Those earlier limits were superseded by the Round 3 and Phase 4 results + above. +- Preview/HMR under the router policy and actor-to-actor access were unverified at the earlier + checkpoint. In the current finish run, preview HMR passed, and an unrelated actor's CONNECT + to the shim was rejected at the actor egress gateway before reaching the router. The + per-actor shim token passed live checks, including suspend/resume persistence. +- Phase 2's counter/marker persistence and worker-loss revert were not proved in that original + run. Later Phase 4 checks proved Claude's file marker across suspend/resume; the three pending + Phase 4 checks are listed in the current results below. +- **`workspace_bindings` orchestration functions** (`ensure_workspace`, `resume_workspace`, ...) + were not exercised against a live Postgres + running backend; only their extracted pure logic + (`plan_ensure`, `_binding_from_row`, `is_crashed`) is unit tested, and the transport layer + they call (`SubstrateControl`) is proved live as described above. + +### Native-session gate addendum: harness repair and Phase 2 partial live proof + +The original live attempt (outside this doc's own commits) created the live-agent-gate +infrastructure and hit a real failure during golden-snapshot creation, reviewed +in `.tasknotes/gate5-review-and-recovery-plan-2026-09-22.md`: the golden actor's entrypoint +fetched a credential from `cred-server` unconditionally at boot, that fetch was denied (`403`), +and the golden actor exited before its snapshot was captured. A later restore returned +`runsc exit 128`; the original evidence did not establish that the denied fetch caused that +restore error. The harness script driving that attempt (never committed; reviewed from a scratch copy) also applied an +unresolved `ko://` WorkerPool image, never registered the atespace at the control-plane API +(a Kubernetes Namespace of the same name is not an atespace), checked for an existing +ActorTemplate with the atespace embedded in the name rather than the CLI's required `-a` flag, +and printed success from a log line reached before the state it implied was actually confirmed. + +Phase 1 (recovery plan step 2) repairs the harness findings and removes the boot-time fetch: + +- `entrypoint.sh` no longer fetches a credential or needs network access to reach a running + state. There is no credential-fetch helper or relay path in the image. Credential delivery + remains a separate, gated step and is never performed during golden-actor warmup. +- The product `k8s/actor-template.yaml.tmpl` is also the Gate 5 manifest. The harness versions + its ActorTemplate name (`live-agent-gate-${TEMPLATE_VERSION}`) so a failed golden snapshot is + never reused; the template starts without credentials or external network access. +- `backend/src/mainloop/runtime/substrate.py` gained `ensure_atespace`/`get_actor_template`/ + `create_actor_template`/`get_eligible_workers`, plus bounded, exception-raising waits for + golden snapshots, eligible workers, actor state, and the live actor health route. Rerun + identity reconciliation distinguishes absent, unowned, matching, and diverged actors before + the harness creates or resumes one. +- `backend/scripts/gate5_setup.py` registers the atespace, resolves the WorkerPool image with + `ko resolve` from the verified pinned checkout, waits for an eligible worker and a golden + snapshot, binds reruns to persisted cluster/template/actor identity, then confirms health + through the actor route before applying egress policy. +- The egress step fails closed: exactly one of `--egress-deny-all`, `--egress-cidr`, + `--egress-hostname` or `--egress-allow-all` must be explicit. + +Phase 2 used pinned Substrate `cdac9baef81dd319b46086d695266e6161e9e592`, a fresh +`kind-substrate-preview` cluster, agentgateway, and image +`localhost:5001/live-agent-gate@sha256:a5ffadbede22382732067873c0239fa67a757f3bbde38b838942a5bfa20fbeda`. +ActorTemplate `live-agent-gate-v1` (UID `b16cf365-0856-4623-87a9-479112767d46`) reached a READY +golden snapshot; actor `claude-gate5` (UID `cbb4d70c-4ba9-4ce2-af08-20f866a11866`) reached +RUNNING. The harness health probe timed out because it forwarded the router's HTTP port 80 while +the documented non-default-port CONNECT listener is 8081. A direct `/healthz` through 8081 +returned 200. Suspend produced snapshot +`gs://ate-snapshots/live-agent-gate/atespaces/live-agent-gate/actors/cbb4d70c-4ba9-4ce2-af08-20f866a11866/snapshots/fadab73f-5f8a-4346-8cfb-af67c85c893d`; resume returned the same actor UID to RUNNING, the health route returned 200, and logs showed gVisor's `restore -image-path` path. The marker/counter and process PID were not measured. + +The first setup pass exposed one additional pinned-CLI result shape: a valid zero-match worker +query serializes as `{}`. The adapter now treats only an empty object as zero workers and still +rejects non-empty objects missing `workers`. The affected correction run passed 30 tests: 9 +worker-discovery tests and 21 contract tests. The live Phase 2 run reached a READY golden and a +RUNNING actor, returned `/healthz` 200 through the CONNECT listener on port 8081, and suspended +then resumed the same actor to RUNNING with `/healthz` 200. Its unchanged rerun did not create a +duplicate actor, but waited for spare worker capacity before checking the persisted actor UID and +failed because the single worker was already occupied. The marker/counter and process PID were +not measured in that run. + +Before any provider credential work, a separate tokenless Pod in `default` POSTed a harmless +command through `atenet-router:8081` to `actor-upstream:8090/run` with the actor-routing header; +the shim returned `200 OK`. The pinned router documentation says ingress treats request headers +as unauthenticated input, while ingress authorization is future roadmap work. This is not +closable with an existing Substrate actor-ingress primitive in this configuration. Under the +earlier plan this meant fallback C; the 2026-09-23 owner decision supersedes that fallback with a +Mainloop-owned NetworkPolicy, per-actor shim token, and closed-channel credential delivery. No +provider credential Secret was created or read. Phase 4 was not attempted in that run. + +After deny-all egress was applied, operator `/run` calls did not complete; a pane read confirmed +the baseline counter/marker command had not run. Therefore the suspend/resume result proves the +actor and Herdr health path restored, but does not establish counter/marker persistence. The +force-delete-worker/revert portion of the lane was not attempted without those markers. + +The reviewing session reports the full runtime suite passed 189/189 outside its restricted +sandbox before the Phase 2 empty-result correction. The previous Phase 2 run's operator calls +after deny-all egress did not complete. In the Phase 3 run, an in-cluster control namespace POST +to `/run` returned 200 after deny-all egress was attached, so the ingress response path worked +for that trusted request. The focused setup, Substrate, and contract suites now pass 85/85. +The credential-free proof is partial live evidence; native-session continuity remains partial, +and full counter/marker and worker-loss checks are unproved. + +### Phase 2b — rerun and router corrections + +The harness now forwards to the router CONNECT listener on Service port 8081, checks live actor +identity before waiting for worker capacity, and handles the pinned CLI's `{}` zero-worker result. +The focused 63-test setup/Substrate suite passed before the Phase 3 code; the latest setup, +Substrate, and contract run passed 85/85. Phase 2b was committed locally as `3ca780a`. +The in-sandbox `unittest discover` process IDs 2293648 and 2293673 were still alive but invisible +to the sandbox check; the reviewing session killed them on 2026-09-23. The full suite result +189/189 is attributed to that reviewing session. + +### Phase 3–4 finish run — router boundary, preview correction, and egress stop + +One fresh `kind-substrate-preview` cluster ran Kind v0.33.0, node image v1.37.0, enforcing +kindnet `v20260820`, and pinned Substrate +`cdac9baef81dd319b46086d695266e6161e9e592` with the agentgateway dataplane. Phase 3a passed: +the client reached an HTTP probe before policy, was blocked by deny-all ingress, then reached it +after a namespace-scoped allow. + +The tracked `k8s/router-ingress-policy.yaml` selects router Pods by `app=atenet-router`. It +allows `mainloop-control` on router control ports, the preview proxy on HTTP only, and +`otel-system` on stats port 15020. NetworkPolicy ports target Pod ports; Service port 80 maps +to router container port 8080. A POST to `/run` through port 8081 returned 200 from +`mainloop-control` and timed out from an unrelated Pod in `default`. + +The corrected `preview-gate-v2` template used the rebuilt image from +`spikes/substrate-workspace-adapter/image/`, an explicit working directory, and `readyz`. The +live Vite page passed HMR through the NGINX preview proxy: editing `main.js` changed the page +heading in place, preserved the browser marker and time origin, and produced a successful HMR +WebSocket upgrade. An unrelated preview actor's CONNECT attempt to +`live-agent-gate/claude-gate5:8090` returned 405 at the actor egress gateway before reaching the +router. The request was rejected, though that result is an egress-gateway denial rather than a +router NetworkPolicy denial. + +The first immutable preview template remains abandoned. Its worker logged `npm error ENOENT` +for `/package.json` before golden checkpoint. `savedMFOwners=[_pause:/]` records the surviving +pause container after the application exited; the template lacked `readyz`, so the dead app was +snapshotted. This is a harness/image/readiness error, not a gVisor restore blocker. Record it as +a candidate upstream issue because Substrate checkpointed an exited container without a clear +error. The corrected template name is `preview-gate-v2`. + +On `live-agent-gate/claude-gate5` (actor UID +`6125c129-7312-453b-aea5-a2fae6745c62`), the live shim-token test passed: missing and wrong +tokens returned 401, the correct token authorized `/run`, a second token install returned 409, +and the token continued to authorize requests after suspend/resume with the same actor UID. A +deny-all control check initially returned `/healthz` 200 and authenticated `/run` 200 with the +command marker observed. After a later restore, `/healthz` intermittently timed out at the shim's +serial Herdr status/pane-read check, while direct `herdr status server` and authenticated +`/read` succeeded. The WorkerPool remained Ready; the failure was located at the shim health +handler, not the router or NetworkPolicy. + +Provider discovery without credentials exited early (Claude rc 1, Codex rc 2) and emitted no +hostnames. Since those were not CLI-measured destinations, the initial bounded check used +provider/API/auth host candidates, informed by [Claude Code's network requirements](https://code.claude.com/docs/en/corporate-proxy), +the [Codex ChatGPT sign-in flow](https://developers.openai.com/codex/auth), and the Codex file's +`chatgpt` auth mode: +`api.anthropic.com`, `platform.claude.com`, `api.openai.com`, `auth.openai.com`, and +`chatgpt.com`. The live Substrate API confirmed exactly one hostname rule with those entries; +its policy cache is 10 seconds. HTTPS to those hosts reached their public services, but an +unlisted HTTPS request to `example.com` also returned 200. This fails Phase 3d's required +unlisted-host 403 and shows hostname-only egress is not enforced on this agentgateway path. +The actor was restored to a zero-rule deny-all EgressPolicy immediately afterward. No provider +Secret was created or read, no credential was delivered, and no Claude or Codex session was +started. Phase 4 and all snapshot-revert tests were not attempted. + +The restore diagnosis is clarified in the Phase 3 stop-condition section of the finish plan. +The Codex sandbox could not see the stalled `unittest discover` processes 2293648 and 2293673; +the reviewing session killed both on 2026-09-23. The full runtime result 189/189 is attributed +to that reviewing session, not to a sandbox process killed by this agent. + +## Historical CapabilityResult (before Round 3) + +| Capability | State | Scope | Evidence and limit | +| ---------------------------- | ------- | ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | +| `workspace_adapter_contract` | partial | fixture | `SubstrateControl` is exercised live; `workspace_bindings` orchestration is fixture-tested, not run against a live backend and Postgres. | +| `substrate_actor_lifecycle` | partial | live | READY golden creation and actor RUNNING proved live; full marker and worker-loss recovery remain unproved. | +| `preview_hmr` | proved | live | Real Vite HMR edit and WebSocket upgrade succeeded through the preview proxy. | +| `dev_service_postgres` | proved | live | Real Postgres query, narrow allow rule, denied destination, and reconnect after wake. | +| `phase3a_networkpolicy` | proved | live | Kindnet blocked and then allowed the in-cluster HTTP probe according to NetworkPolicy. | +| `router_ingress_boundary` | proved | live | `default` was blocked; `mainloop-control` was admitted; unrelated actor CONNECT to the shim was rejected at actor egress; preview traffic passed. | +| `shim_token_auth` | proved | live | 401/409 behavior, successful bearer use, and token persistence across suspend/resume passed on the final actor. | +| `provider_egress` | failed | live | Exact hostname policy was confirmed, but HTTPS to unlisted `example.com` returned 200 through agentgateway. Actor returned to deny-all. | +| `credential_delivery` | unknown | unverified | No provider Secret was created or read; credentials were not delivered. | +| `native_session_continuity` | partial | unverified | Phase 3d failed before credential delivery; no native session was run. | +| `failure_recovery` | partial | live | Earlier CRASHED/revert mechanics were proved; worker-loss recovery and the backend-restart `recorded` case remain unproved live. | + +## Historical recommendation (superseded by the Round 3/Phase 4 result) + +The corrected preview image resolves the earlier `npm ENOENT`/dead-checkpoint harness error; +that result does not justify deferring native-session adoption. The current agentgateway run +found a separate security blocker: a hostname-only EgressPolicy allowed HTTPS to an unlisted +host. Do not deliver provider credentials or promote native sessions to production until +hostname enforcement is verified on a supported dataplane or another host-aware egress boundary +is added. Production also needs a GitOps-managed router NetworkPolicy on an enforcing CNI, +shim-token issuance by the real Mainloop backend, and snapshot-bucket access controls. The +credential-bearing snapshot trade-off remains open because no credential entered an actor. + +## Historical cleanup note (superseded by the Phase 4 owner-review hold) + +Phase 0 removed the approved relay resources and both named Secrets, then deleted the +owner-confirmed failed-trial cluster. Per the owner's 2026-09-23 instruction, cleanup of the +fresh `kind-substrate-preview` cluster and its registry is stopped; both remain up for review. +The actor's EgressPolicy is zero-rule deny-all. `mainloop-test` remains outside scope. No +provider Secret or credential-bearing snapshot was created. Current Kind/Docker inventory and +disk headroom are recorded in the task proof note. The owner handles rotation of credentials +previously served by the removed relay. + +## Current CapabilityResult — gates 1–6 and Phase 4 + +| Gate / capability | State | Scope | Evidence and limit | +| ------------------------------------------------------------------ | ------- | ---------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Gate 1 — native contract | proved | fixture | Record transitions, ownership fencing, and reconciliation are exercised with `ContractStore` fakes; this is not a database-backed restart proof. | +| Gate 2 — workspace binding adapter | partial | fixture | Planning and row-mapping logic use fakes; `workspace_bindings` orchestration was not run against live Postgres and a running backend. | +| Gate 2 — Substrate control adapter | proved | live | Actor lifecycle operations used the real `kubectl ate` adapter and the Cilium preview cluster. | +| Gate 3 — preview/HMR | proved | live | Real Vite edits updated the open browser session over WebSocket, including across suspend/resume. | +| Gate 4 — dev-service access | proved | live | Real PostgreSQL query, narrow CIDR policy, denied destination, and reconnect after wake passed. | +| Gate 5 — native sessions in prior actor design | proved | live | Claude and Codex each completed a native turn and recalled a nonce after suspend/resume in the same Herdr-backed actor. Suspend/resume measured 393/408 ms for Claude and 573/789 ms for Codex. | +| Gate 5 — headless per-turn CLI actor | partial | live | Lane A runs 5–7 measured `/run`, `/healthz`, and durable restore; runs 6 and 9 proved the Claude turn and same-session recall path on this actor. Native Codex behavior and Claude continuity after worker loss remain unproved. | +| Gate 5 — headless Claude turn and same-session recall | proved | live | Measured on `kind-substrate-preview` in Lane A run 6, fork/Substrate atelet `ce265c1d`, actor image `live-agent-gate@sha256:eb819c5ae18829a18972fd7be9ec44e0f3dbef08e554f8e0dfc1d6240f962f5d`: UID 10001; provider preflight HTTP 200; concurrent `/turn` HTTP 409; native turn and same-session recall completed across suspend/resume; suspend 414 ms; resume 480 ms; health 200; zero credential leaks in provider and WorkerPool logs. | +| Gate 5 — durable `/work/repo` restore after worker loss | proved | live | Measured on `kind-substrate-preview` in Lane A run 5, fork `ce265c1d`, WorkerPool image digest prefix `e19e6617`: UID 10001; `octocat/Hello-World` at `7fd1a60b`; 48 files; snapshot scope `FULL`; suspend 418 ms; both original workers deleted; resume 2,589 ms including the free-worker retry wait; health 200; exact path/UID/GID/mode/SHA-256 manifest match. Lane resources were cleaned up after evidence capture. | +| Gate 5 — headless Claude turn and same-session recall (0f9635ae) | proved | live | `CLAUDE_TURN_PROOF=PASS` on `kind-substrate-preview`, Lane A run 10, fork `patched-next` commit `0f9635aed37bd5dde604a9bca1975421cd07181a`: UID 10001; provider preflight HTTP 200; concurrent `/turn` HTTP 409; same-session recall after suspend/resume; marker hash matched. Provider and WorkerPool log collection both succeeded (a failed fetch now fails the check), and both report zero credential-prefix leaks. `suspend_ms=415`, `resume_ms=483` are single-run wall-clock measurements, not benchmarks. Evidence: `lane-a-claude-proof-0f9635ae-run10-2026-09-25.log`. | +| Gate 5 — durable `/work/repo` restore after worker loss (0f9635ae) | proved | live | `DURABLE_RESTORE_PROOF=PASS` on `kind-substrate-preview`, Lane A run 7, fork `patched-next` commit `0f9635aed37bd5dde604a9bca1975421cd07181a`: `suspend_ms=415`, `resume_ms=481`, `worker_loss=yes`, `files=48`; replacement-worker restore succeeded with an exact manifest match. Timings are single-run wall-clock measurements, not benchmarks. Evidence: `lane-a-durable-proof-0f9635ae-run7-2026-09-25.log`. | +| Gate 5 — complete continuity | partial | live | Lane A runs 6 and 9 proved Claude same-session recall across suspend/resume; runs 5 and 7 proved durable workspace restore after worker loss. Native Claude recall after worker loss, history after revert, and the earlier Codex marker follow-up remain pending. | +| Gate 6 — actor failure recovery | partial | live | CRASHED-to-revert-to-resume and replacement-worker restore were measured; native recall after worker loss and history after revert are not established. | +| Phase 4 — Claude native session (prior actor) | proved | live | In the earlier Herdr-backed actor, Claude Code completed a turn and same-session nonce recall after suspend/resume; the post-worker-loss and post-revert history checks remain pending. | +| Phase 4 — Codex native session (prior actor) | partial | live | In the earlier Herdr-backed actor, Codex CLI completed a turn and same-session nonce recall after suspend/resume; file-marker continuity remains pending. | +| `backend_restart_delivery_reconciliation` | proved | fixture | A new fake-backed test reloads a persisted message and `recorded` attempt into a fresh `ContractStore`; retry is blocked until `not_delivered` evidence, and the same payload reference remains pending. It does not exercise Postgres, a transport, or the production delivery loop. | +| `router_ingress_boundary` and `shim_token_auth` | proved | live | The unrelated namespace was denied, control-namespace access succeeded, and missing/wrong/correct token plus one-time install and suspend/resume checks passed. | +| `provider_hostname_egress` | proved | live | Envoy/Cilium actor policies denied unlisted hosts; the earlier agentgateway HTTPS path allowed an unlisted host and is not a supported hostname boundary. | +| `credential_delivery` | proved | live | Claude was injected on the Envoy upstream leg. Codex `auth.json` was installed through the shim and remains in the actor snapshot; production must move Codex credentials to egress injection. | +| `cilium_kube_proxy_replacement` | partial | live | KPR=true CoreDNS Pod-IP queries worked, but kube-dns Service-IP queries timed out. The cause was not isolated; the run continued with KPR=false. | +| `snapshot_bucket_access_control` | unknown | unverified | Snapshot-bucket read access was not established. | +| `stuck_state_timeouts` | unknown | unverified | Production timeouts and surfaced recovery for stuck states remain to be implemented and measured. | + +## Current recommendation and production requirements + +**Defer production adoption of the headless per-turn actor design.** The earlier Herdr-backed +actor proved that Substrate and the Phase 3 boundary can host native Claude and Codex sessions; +both completed a turn and same-session nonce recall across suspend/resume. Lane A runs 5 and 7 +measured the target image running as UID 10001 and restoring durable workspace contents after +worker loss; runs 6 and 9 measured Claude turn completion and same-session recall across +suspend/resume on the headless actor. Production readiness remains partial because native +Claude/Codex continuity after worker loss or revert, durable backend integration, and +snapshot access controls are not proved. + +Production requirements: + +- The backend's projected ServiceAccount token for Substrate API commit + `0f9635aed37bd5dde604a9bca1975421cd07181a` has audience `api.ate-system.svc`. The API accepts + any valid cluster ServiceAccount token with that audience and does not yet enforce per-caller + authorization through the OpenFGA model. The backend token therefore grants effectively full + Substrate access. This is a known authorization gap; enforce caller-level authorization before + production adoption. +- Isolate worker selection by tenant. Substrate does not scope selection by atespace or + namespace; require unique per-tenant WorkerPool names/selectors and enforce uniqueness with + admission policy. +- Manage the router `NetworkPolicy` through GitOps and use a CNI that enforces it. +- Issue and rotate per-actor shim tokens from the real Mainloop backend. +- Enforce snapshot-bucket access controls. The Codex snapshot currently contains `auth.json`. +- Move Codex credentials from the actor snapshot to a supported egress-injection flow. +- Use Envoy sdsmint for experimental credential injection; agentgateway does not implement it. +- Set `dns_lookup_family: V4_PREFERRED` on the egress DNS caches and `max_session_keys: 0` on + the relevant upstream TLS clusters; retain hostname and SAN verification. +- Resolve KPR=true actor DNS: CoreDNS Pod-IP lookup worked, but kube-dns Service-IP lookup + timed out and the exact cause remains unknown. +- Add bounded timeouts and visible recovery for actors and deliveries that remain stuck. + +## Current cleanup and review hold + +The Phase 4 owner-review brief requires both actors to remain SUSPENDED for review and their +credential Secrets to be deleted. The dedicated preview cluster and run-built images remain +available for that review; `mainloop-test` remains untouched. Temporary delivery Jobs, +ConfigMaps, shim-token Secrets, the Claude provider, and the Claude/Codex credential Secrets are +removed. The actor snapshots are intentionally retained for owner inspection, so the Codex +snapshot still contains `auth.json`. The proof note records the final inventory and identifies +this review hold as the reason the older finish-plan teardown was not applied. + +The Round 3 helper sources are under `spikes/substrate-workspace-adapter/tools/`; the captured +sources match the pinned Substrate checkout. The Codex file-write route now requires an installed +shim token even on a tokenless golden and validates a JSON object. That source hardening is +fixture-tested, but the retained actor image digest predates the change; rebuild before using +that route in another run. + +## Design only: owner-audited operator execution + +The shim's `POST /run` endpoint can execute a command and retains bounded output and timeout +status. It is not exposed to clients in this slice. A future operator path should authorize the +workspace owner in Mainloop, keep the per-actor shim token server-side, and persist an audit +record before dispatch with the operator, workspace, command string, working directory, +timeout, and a result reference. The owner action should be explicitly scoped and bounded; the +shim's bearer token alone is not Mainloop owner authorization. This is a design proposal only: +there is no operator-exec API, UI, audit record, or runtime change here. + +## Follow-up for the transport lane: serialize delivery with suspend + +Suspend reservation takes a `FOR UPDATE` lock on the workspace binding and checks the delivery +ledger in the same transaction, then checks the ledger again immediately before calling +Substrate. The delivery-recording path does not take that row lock yet, so a delivery can still +race after the last check and before Substrate applies suspension, including across backend +replicas. The transport lane should make delivery recording take the same lock (or an agreed +workspace advisory lock) and prove the handoff with a concurrency test. Until then, the second +check narrows this race but does not eliminate it. diff --git a/examples/devenv-sample/Dockerfile b/examples/devenv-sample/Dockerfile new file mode 100644 index 0000000..5dfab9f --- /dev/null +++ b/examples/devenv-sample/Dockerfile @@ -0,0 +1,14 @@ +FROM node:22-bookworm-slim +WORKDIR /workspace +COPY package.json ./ +RUN npm install --omit=dev +COPY server.mjs ./ +RUN groupadd --system app \ + && useradd --system --gid app --create-home --home-dir /home/app app \ + && chown -R app:app /workspace +USER app:app +ENV PORT=3000 PGHOST=postgres PGPORT=5432 PGUSER=postgres PGPASSWORD=dev-only PGDATABASE=workspace +EXPOSE 3000 +HEALTHCHECK --interval=30s --timeout=3s --start-period=15s --retries=3 \ + CMD ["node", "-e", "fetch('http://127.0.0.1:3000/').then((response) => process.exit(response.ok ? 0 : 1)).catch(() => process.exit(1))"] +CMD ["npm", "start"] diff --git a/examples/devenv-sample/README.md b/examples/devenv-sample/README.md new file mode 100644 index 0000000..52ab43d --- /dev/null +++ b/examples/devenv-sample/README.md @@ -0,0 +1,15 @@ +# Mainloop dev environment sample + +This small fixture demonstrates the shape of a branch workspace: one Node app, one sibling +Postgres service, an HTTP preview port, a WebSocket echo endpoint, and a persistent counter. + +The app serves one page at `/`, echoes WebSocket messages at `/ws`, and reads or increments a +Postgres row through `/api/counter`. `mainloop.yaml` declares the app image, service, preview +port, actor template, and 30-minute idle timeout. Build the image from this directory with +`docker build -t mainloop-devenv-sample:latest .` in an environment with Docker available. + +The image declares a real non-root `app` user and checks the app's `/` HTTP route with a +container health check. Mainloop's pinned Substrate fork runs actors as the image's `USER` in +its `WORKDIR`. This sample does not claim to validate non-root actor execution or live +multi-container connectivity. Sample credentials are fixture-only; use secret-backed values +for real projects. diff --git a/examples/devenv-sample/mainloop.yaml b/examples/devenv-sample/mainloop.yaml new file mode 100644 index 0000000..22734a4 --- /dev/null +++ b/examples/devenv-sample/mainloop.yaml @@ -0,0 +1,21 @@ +--- +repo_url: https://github.com/example/devenv-sample +branch: main +agent_kinds: [claude, codex] +resource_class: default +dev: + image: mainloop-devenv-sample:latest + actor_template: devenv-sample + services: + - name: postgres + image: postgres:16-alpine + env: + POSTGRES_DB: workspace + POSTGRES_USER: postgres + POSTGRES_PASSWORD: dev-only + ports: [5432] + ports: + - name: app + number: 3000 + protocol: http + idle_timeout_minutes: 30 diff --git a/examples/devenv-sample/package.json b/examples/devenv-sample/package.json new file mode 100644 index 0000000..f71d33a --- /dev/null +++ b/examples/devenv-sample/package.json @@ -0,0 +1,13 @@ +{ + "name": "mainloop-devenv-sample", + "version": "1.0.0", + "private": true, + "type": "module", + "scripts": { + "start": "node server.mjs" + }, + "dependencies": { + "pg": "^8.13.1", + "ws": "^8.18.0" + } +} diff --git a/examples/devenv-sample/server.mjs b/examples/devenv-sample/server.mjs new file mode 100644 index 0000000..ea957e7 --- /dev/null +++ b/examples/devenv-sample/server.mjs @@ -0,0 +1,62 @@ +import { createServer } from 'node:http'; +import { Pool } from 'pg'; +import { WebSocketServer } from 'ws'; + +const port = Number(process.env.PORT ?? 3000); +const pool = new Pool(); +const server = createServer(async (request, response) => { + if (request.method === 'GET' && request.url === '/') { + response.writeHead(200, { 'content-type': 'text/html; charset=utf-8' }); + response.end(` + +Mainloop dev workspace +

Branch workspace

Postgres counter: …

+

WebSocket: connecting…

+`); + return; + } + + if (request.url === '/api/counter' && request.method === 'GET') { + const result = await pool.query('SELECT value FROM sample_counter WHERE id=1'); + response.writeHead(200, { 'content-type': 'application/json' }); + response.end(JSON.stringify({ value: result.rows[0]?.value ?? 0 })); + return; + } + + if (request.url === '/api/counter' && request.method === 'POST') { + const result = await pool.query( + 'INSERT INTO sample_counter (id,value) VALUES (1,1) ON CONFLICT (id) DO UPDATE SET value=sample_counter.value+1 RETURNING value' + ); + response.writeHead(200, { 'content-type': 'application/json' }); + response.end(JSON.stringify({ value: result.rows[0].value })); + return; + } + + response.writeHead(404); + response.end('Not found'); +}); + +const sockets = new WebSocketServer({ noServer: true }); +server.on('upgrade', (request, socket, head) => { + if (request.url !== '/ws') return socket.destroy(); + sockets.handleUpgrade(request, socket, head, (websocket) => { + websocket.on('message', (message) => websocket.send(message.toString())); + }); +}); + +await pool.query(`CREATE TABLE IF NOT EXISTS sample_counter ( + id integer PRIMARY KEY, + value integer NOT NULL DEFAULT 0 +)`); +await pool.query('INSERT INTO sample_counter (id,value) VALUES (1,0) ON CONFLICT (id) DO NOTHING'); +server.listen(port, '0.0.0.0', () => console.log(`sample app listening on ${port}`)); diff --git a/frontend/src/lib/api.ts b/frontend/src/lib/api.ts index f8be1e9..a9d970c 100644 --- a/frontend/src/lib/api.ts +++ b/frontend/src/lib/api.ts @@ -199,6 +199,98 @@ export interface NativeDelivery { detail: string | null; } +export type WorkspaceDesiredState = 'running' | 'suspended'; +export type WorkspaceObservedState = + | 'running' + | 'suspending' + | 'suspended' + | 'resuming' + | 'failed' + | 'unknown'; + +export interface WorkspaceManifest { + repo_url: string | null; + branch: string; + agent_kinds: ('claude' | 'codex')[]; + skills: string[]; + mcp_servers: string[]; + egress_allowlist: string[]; + resource_class: string; + dev: WorkspaceDev | null; +} + +export interface WorkspacePort { + name: string; + number: number; + protocol: 'http'; +} + +export interface WorkspaceService { + name: string; + image: string; + env: Record; + ports: number[]; +} + +export interface WorkspaceDev { + image: string | null; + devcontainer_ref: string | null; + actor_template: string | null; + services: WorkspaceService[]; + ports: WorkspacePort[]; + idle_timeout_minutes: number; +} + +export interface WorkspaceCondition { + type: string; + status: 'True' | 'False' | 'Unknown'; + reason: string; + message: string; + last_transition_time: string; +} + +export interface WorkspaceTransition { + from_state: WorkspaceObservedState | null; + to_state: WorkspaceObservedState; + reason: string; + occurred_at: string; +} + +export interface WorkspaceLifecycle { + workspace_id: string; + session_id: string; + desired_state: WorkspaceDesiredState; + observed_state: WorkspaceObservedState; + manifest: WorkspaceManifest; + conditions: WorkspaceCondition[]; + last_transition: WorkspaceTransition | null; + operation_id: string | null; + snapshot_ref: string | null; + last_activity_at: string | null; + ownership_generation: number; + updated_at: string; +} + +export interface WorkspacePreviewPort { + port: number; + name: string; + url: string; +} + +export interface WorkspaceCredentialStatus { + provider: 'codex' | 'claude'; + available: boolean; + needs_signin: boolean; + expires_at: string | null; +} + +export interface CredentialReauthStatus { + id: string; + provider: 'codex' | 'claude'; + state: 'running' | 'completed' | 'failed'; + challenge: { url: string; code: string } | null; +} + export interface TopicLine { name: string; status_line: string; @@ -206,7 +298,7 @@ export interface TopicLine { } export interface MainThreadInfo { - mode: 'sdk' | 'native'; + mode: 'native'; session_id: string | null; conversation_id: string | null; native: NativeSessionInfo | null; @@ -245,11 +337,7 @@ export interface NativeSessionInfo { native_session_id: string | null; model: string | null; approval_policy: string; - herdr_pane_id: string | null; - herdr_terminal_id: string | null; - herdr_workspace_id: string | null; - workspace_pod: string | null; - workspace_pod_uid: string | null; + workspace_name: string | null; workspace_ready: boolean; agent_live: boolean | null; generation: number; @@ -403,6 +491,25 @@ export const api = { if (!response.ok) throw new Error('Failed to refresh project'); }, + async createWorkspace( + projectId: string, + branch: string, + dev: WorkspaceDev + ): Promise { + const response = await apiFetch(`${API_URL}/workspaces`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ project_id: projectId, branch, dev }) + }); + if (!response.ok) throw new Error(await errorDetail(response, 'Failed to create workspace')); + return response.json(); + }, + + async deleteWorkspace(workspaceId: string): Promise { + const response = await apiFetch(`${API_URL}/workspaces/${workspaceId}`, { method: 'DELETE' }); + if (!response.ok) throw new Error(await errorDetail(response, 'Failed to delete workspace')); + }, + /** * Get the SSE endpoint URL for the global event stream. */ @@ -411,6 +518,82 @@ export const api = { }, // Session endpoints + async listWorkspaces(): Promise { + const response = await apiFetch(`${API_URL}/workspaces`); + if (!response.ok) throw new Error('Failed to list workspaces'); + return response.json(); + }, + + async getWorkspace(workspaceId: string): Promise { + const response = await apiFetch(`${API_URL}/workspaces/${workspaceId}`); + if (!response.ok) throw new Error(await errorDetail(response, 'Failed to get workspace')); + return response.json(); + }, + + async suspendWorkspace(workspaceId: string): Promise { + const response = await apiFetch(`${API_URL}/workspaces/${workspaceId}/suspend`, { + method: 'POST' + }); + if (!response.ok) throw new Error(await errorDetail(response, 'Failed to suspend workspace')); + return response.json(); + }, + + async resumeWorkspace(workspaceId: string): Promise { + const response = await apiFetch(`${API_URL}/workspaces/${workspaceId}/resume`, { + method: 'POST' + }); + if (!response.ok) throw new Error(await errorDetail(response, 'Failed to resume workspace')); + return response.json(); + }, + + async refreshWorkspace(workspaceId: string): Promise { + const response = await apiFetch(`${API_URL}/workspaces/${workspaceId}/refresh`, { + method: 'POST' + }); + if (!response.ok) throw new Error(await errorDetail(response, 'Failed to refresh workspace')); + return response.json(); + }, + + async listWorkspacePreviewPorts(workspaceId: string): Promise { + const response = await apiFetch(`${API_URL}/workspaces/${workspaceId}/ports`); + if (!response.ok) throw new Error(await errorDetail(response, 'Failed to list preview ports')); + const result = await response.json(); + if (!Array.isArray(result?.ports)) throw new Error('Invalid preview port response'); + return result.ports; + }, + + async getWorkspaceCredentials(workspaceId: string): Promise { + const response = await apiFetch(`${API_URL}/workspaces/${workspaceId}/credentials`); + if (!response.ok) + throw new Error(await errorDetail(response, 'Failed to get credential status')); + const result = await response.json(); + if (!Array.isArray(result?.credentials)) throw new Error('Invalid credential status response'); + return result.credentials; + }, + + async startWorkspaceCredentialReauth( + workspaceId: string, + provider: 'codex' | 'claude' + ): Promise { + const response = await apiFetch( + `${API_URL}/workspaces/${workspaceId}/credentials/${provider}/reauth`, + { method: 'POST' } + ); + if (!response.ok) throw new Error(await errorDetail(response, 'Failed to start sign-in')); + return response.json(); + }, + + async getWorkspaceCredentialReauth( + workspaceId: string, + jobId: string + ): Promise { + const response = await apiFetch( + `${API_URL}/workspaces/${workspaceId}/credentials/reauth/${encodeURIComponent(jobId)}` + ); + if (!response.ok) throw new Error(await errorDetail(response, 'Failed to check sign-in')); + return response.json(); + }, + async listSessions(options?: { status?: string }): Promise { const params = new URLSearchParams(); if (options?.status) params.set('status', options.status); diff --git a/frontend/src/lib/components/Chat.svelte b/frontend/src/lib/components/Chat.svelte index 222d5b4..9604e45 100644 --- a/frontend/src/lib/components/Chat.svelte +++ b/frontend/src/lib/components/Chat.svelte @@ -12,8 +12,7 @@ import ConversationView from './ConversationView.svelte'; import MainThreadHeader from './MainThreadHeader.svelte'; - // Native main thread (MAIN_THREAD_MODE=native): a Claude session under Herdr whose window - // Mainloop rotates. The reply is mirrored from the native journal, so we poll for it. + // Mainloop mirrors the native Substrate session journal, so we poll for its reply. let mainThread = $state(null); let sendError = $state(null); diff --git a/frontend/src/lib/components/NativeIdentityStrip.svelte b/frontend/src/lib/components/NativeIdentityStrip.svelte index 0b1f9f5..385db24 100644 --- a/frontend/src/lib/components/NativeIdentityStrip.svelte +++ b/frontend/src/lib/components/NativeIdentityStrip.svelte @@ -80,13 +80,8 @@ >{info.native_session_id ?? 'pending'} - herdr pane {info.herdr_pane_id ?? '-'} - ({info.agent_name}) - pod {info.workspace_pod} - {info.workspace_pod_uid ? info.workspace_pod_uid.slice(0, 8) : '-'} + workspace actor {info.workspace_name} {info.workspace_ready ? 'ready' : 'not ready'} import type { Session } from '$lib/api'; import { navigationContext } from '$lib/stores/navigationContext'; + import { workspaces } from '$lib/stores/workspaces'; + import WorkspaceLifecycleBadge from './WorkspaceLifecycleBadge.svelte'; let { session, @@ -32,6 +34,9 @@ ); const needsAttention = $derived(session.status === 'waiting_on_user'); + const workspace = $derived( + $workspaces.workspaces.find((item) => item.session_id === session.id) + ); function handleClick() { if (onSelect) { @@ -81,6 +86,9 @@ > {statusLabels[session.status] || session.status.toUpperCase()} + {#if workspace} + + {/if} diff --git a/frontend/src/lib/components/SessionListItem.svelte b/frontend/src/lib/components/SessionListItem.svelte index 2c1d45a..f64f5a4 100644 --- a/frontend/src/lib/components/SessionListItem.svelte +++ b/frontend/src/lib/components/SessionListItem.svelte @@ -1,6 +1,8 @@ + + + WS {label} + diff --git a/frontend/src/lib/sse.ts b/frontend/src/lib/sse.ts index 5b5147e..faf148c 100644 --- a/frontend/src/lib/sse.ts +++ b/frontend/src/lib/sse.ts @@ -13,6 +13,7 @@ export type SSEEventType = | 'session:updated' | 'session:needs_input' | 'session:message' + | 'workspace:updated' | 'heartbeat' | 'log' | 'status' @@ -88,6 +89,7 @@ export class SSEClient { 'session:updated', 'session:needs_input', 'session:message', + 'workspace:updated', 'heartbeat', 'log', 'status', diff --git a/frontend/src/lib/stores/workspaces.ts b/frontend/src/lib/stores/workspaces.ts new file mode 100644 index 0000000..e4710f3 --- /dev/null +++ b/frontend/src/lib/stores/workspaces.ts @@ -0,0 +1,61 @@ +import { writable } from 'svelte/store'; +import { api, type WorkspaceLifecycle } from '$lib/api'; + +interface WorkspacesState { + workspaces: WorkspaceLifecycle[]; + loading: boolean; + error: string | null; +} + +function createWorkspacesStore() { + const { subscribe, set, update } = writable({ + workspaces: [], + loading: false, + error: null + }); + + return { + subscribe, + + async fetchWorkspaces() { + update((state) => ({ ...state, loading: true, error: null })); + try { + const workspaces = await api.listWorkspaces(); + update((state) => ({ ...state, workspaces, loading: false })); + } catch (error) { + update((state) => ({ + ...state, + loading: false, + error: error instanceof Error ? error.message : 'Failed to fetch workspaces' + })); + } + }, + + upsert(workspace: WorkspaceLifecycle) { + update((state) => { + const index = state.workspaces.findIndex( + (item) => item.workspace_id === workspace.workspace_id + ); + const workspaces = [...state.workspaces]; + if (index === -1) workspaces.push(workspace); + else workspaces[index] = workspace; + return { ...state, workspaces }; + }); + }, + + get(workspaceId: string): WorkspaceLifecycle | undefined { + let result: WorkspaceLifecycle | undefined; + const unsubscribe = subscribe((state) => { + result = state.workspaces.find((item) => item.workspace_id === workspaceId); + }); + unsubscribe(); + return result; + }, + + reset() { + set({ workspaces: [], loading: false, error: null }); + } + }; +} + +export const workspaces = createWorkspacesStore(); diff --git a/frontend/src/routes/+layout.svelte b/frontend/src/routes/+layout.svelte index 6330c4b..82b0618 100644 --- a/frontend/src/routes/+layout.svelte +++ b/frontend/src/routes/+layout.svelte @@ -1,9 +1,11 @@ {$currentProject?.project.full_name || 'Project'} - mainloop -
+
{#if detail} {@const { project, open_prs, recent_commits, sessions: projectSessions } = detail} -
+
@@ -76,20 +122,20 @@ {/if}
-

{project.full_name}

+

{project.full_name}

{#if project.description} -

{project.description}

+

{project.description}

{/if}
View on GitHub → - + {project.open_pr_count} open {project.open_pr_count === 1 ? 'PR' : 'PRs'}
@@ -101,7 +147,7 @@
-

Open Pull Requests

+

Open Pull Requests

{#if open_prs.length > 0}
{#each open_prs as pr (pr.number)} @@ -109,20 +155,20 @@ href={pr.url} target="_blank" rel="noopener noreferrer" - class="block border border-term-border bg-term-bg p-3 hover:border-term-accent" + class="border-term-border bg-term-bg hover:border-term-accent block border p-3" >
- #{pr.number} + #{pr.number} {#if pr.is_mainloop} - + mainloop {/if}
-

{pr.title}

-

+

{pr.title}

+

by {pr.author} · {formatDate(pr.created_at)}

@@ -131,13 +177,75 @@ {/each}
{:else} -

No open pull requests

+

No open pull requests

+ {/if} +
+ +
+
+

+ Branch workspaces +

+
{ + event.preventDefault(); + void createWorkspace(project); + }} + > + + +
+
+ {#if workspaceError} + + {/if} + {#if projectWorkspaces.length} + + {:else} +

No branch workspaces yet.

{/if}
-

Recent Commits

+

Recent Commits

{#if recent_commits.length > 0}
{#each recent_commits as commit (commit.sha)} @@ -145,10 +253,10 @@ href={commit.url} target="_blank" rel="noopener noreferrer" - class="block border border-term-border bg-term-bg p-3 hover:border-term-accent" + class="border-term-border bg-term-bg hover:border-term-accent block border p-3" > -

{commit.message.split('\n')[0]}

-
+

{commit.message.split('\n')[0]}

+
{commit.author} · {formatDate(commit.date)} @@ -159,24 +267,26 @@ {/each}
{:else} -

No recent commits

+

No recent commits

{/if}
-

Sessions

+

Sessions

{#if projectSessions.length > 0}
{:else}
-

Loading project...

+

Loading project...

{/if}
diff --git a/frontend/src/routes/sessions/[id]/+page.svelte b/frontend/src/routes/sessions/[id]/+page.svelte index 32355d2..fd7cc3c 100644 --- a/frontend/src/routes/sessions/[id]/+page.svelte +++ b/frontend/src/routes/sessions/[id]/+page.svelte @@ -8,6 +8,8 @@ import { statusLabel } from '$lib/sessionStatus'; import SessionChat from '$lib/components/SessionChat.svelte'; import NativeIdentityStrip from '$lib/components/NativeIdentityStrip.svelte'; + import WorkspaceLifecycleBadge from '$lib/components/WorkspaceLifecycleBadge.svelte'; + import { workspaces } from '$lib/stores/workspaces'; let sessionId = $derived($page.params.id); let loaded = $state(null); @@ -18,6 +20,9 @@ // The sessions store is kept current by SSE; the page's own fetch only gets it started. const live = $derived($sessions.sessions.find((s) => s.id === sessionId)); const session = $derived(loaded ? { ...loaded, ...live } : null); + const workspace = $derived( + $workspaces.workspaces.find((item) => item.session_id === session?.id) + ); // SvelteKit reuses this component when only [id] changes, so load per id rather than on mount. // Clearing `loaded` first unmounts the chat and identity strip, which read their id once. @@ -181,6 +186,19 @@ + {#if workspace} + + {/if} +
diff --git a/frontend/src/routes/workspaces/[id]/+page.svelte b/frontend/src/routes/workspaces/[id]/+page.svelte new file mode 100644 index 0000000..e30bf22 --- /dev/null +++ b/frontend/src/routes/workspaces/[id]/+page.svelte @@ -0,0 +1,526 @@ + + + + {workspace ? `Workspace ${workspace.workspace_id}` : 'Workspace'} - mainloop + + +{#if pageError} +
+
+

{pageError}

+ {#if unreachable} +

+ The page will retry when the backend reconnects. +

+ {/if} + Back to sessions +
+
+{:else if loading && !workspace} +
+
+
+
+
+
+
+{:else if workspace} +
+
+
+
+ + Back to session + +

Workspace

+

+ {workspace.workspace_id} +

+
+ +
+ +
+
+
+

Lifecycle

+

+ Desired {workspace.desired_state} + · + Observed {workspace.observed_state} +

+
+
+ + + + +
+
+ + {#if actionError} + + {:else if failedCondition} + + {:else if operationCondition?.status === 'Unknown'} +

+ {operationCondition.message} +

+ {/if} + + {#if workspace.snapshot_ref} +

+ Last observed snapshot: {workspace.snapshot_ref} +

+ {/if} + {#if workspace.last_transition} +

+ Last transition: {workspace.last_transition.from_state ?? 'new'} → + {workspace.last_transition.to_state} · {workspace.last_transition.reason} · + {formatTime(workspace.last_transition.occurred_at)} +

+ {/if} +
+ +
+

Conditions

+ {#if workspace.conditions.length} +
    + {#each workspace.conditions as condition (condition.type)} +
  • + {condition.type} + {condition.status} +
    +

    {condition.message}

    +

    + {condition.reason} · {formatTime(condition.last_transition_time)} +

    +
    +
  • + {/each} +
+ {:else} +

No lifecycle conditions have been reported.

+ {/if} +
+ +
+

Agent sign-in

+

+ Sign-in credentials stay in the Mainloop control plane; workspaces receive synthetic local auth files. +

+ {#if credentialsError} +

{credentialsError}

+ {:else} +
    + {#each ['codex', 'claude'] as provider} + {@const status = credentialStatuses.find((item) => item.provider === provider)} + {@const job = reauthJobs[provider]} +
  • +
    +

    {provider}

    +

    + {status?.available ? 'Signed in' : 'Needs sign-in'} + {#if status?.expires_at} + · expires {formatTime(status.expires_at)} + {/if} +

    + {#if job?.challenge} +

    + Open sign-in page + Code: {job.challenge.code} +

    + {:else if job?.state === 'running'} +

    Waiting for sign-in instructions…

    + {/if} + {#if reauthErrors[provider]} + + {/if} + {#if job?.state === 'completed'} +

    Sign-in completed.

    + {/if} +
    + +
  • + {/each} +
+ {/if} +
+ +
+
+

Workspace manifest

+

+ Project settings applied to this branch workspace. +

+
+
+
+
Repository
+
+ {#if workspace.manifest.repo_url} + + {workspace.manifest.repo_url} + + {:else} + Not declared + {/if} +
+
+
+
Branch
+
{workspace.manifest.branch}
+
+
+
Agent kinds
+
{formatList(workspace.manifest.agent_kinds)}
+
+
+
Resource class
+
{workspace.manifest.resource_class}
+
+
+
Idle timeout
+
+ {workspace.manifest.dev?.idle_timeout_minutes ?? 'Not configured'} minutes +
+
+
+
Last activity
+
+ {workspace.last_activity_at ? formatTime(workspace.last_activity_at) : 'Not recorded'} +
+
+ {#if workspace.manifest.dev} +
+
Development image
+
+ {workspace.manifest.dev.image ?? workspace.manifest.dev.devcontainer_ref} +
+
+
+
Actor template
+
+ {workspace.manifest.dev.actor_template ?? 'Default project template'} +
+
+
+
Services
+
+ {workspace.manifest.dev.services.length + ? workspace.manifest.dev.services + .map((service) => `${service.name} (${service.image})`) + .join(', ') + : 'None declared'} +
+
+
+
Preview ports
+
+ {workspace.manifest.dev.ports.length + ? workspace.manifest.dev.ports + .map((port) => `${port.name}: ${port.number}/${port.protocol}`) + .join(', ') + : 'None declared'} +
+
+ {/if} +
+
Skills (references)
+
{formatList(workspace.manifest.skills)}
+
+
+
MCP servers (references)
+
{formatList(workspace.manifest.mcp_servers)}
+
+
+
Egress host allowlist
+
+ {formatList(workspace.manifest.egress_allowlist)} +
+
+
+
+ +
+

Previews

+

+ Open a declared or currently listening HTTP port in a new tab. +

+ {#if previewPortsError} +

{previewPortsError}

+ {:else if previewPorts.length} +
    + {#each previewPorts as previewPort (previewPort.port)} +
  • + + {previewPort.name} + :{previewPort.port} + + + Open preview + +
  • + {/each} +
+ {:else} +

No declared or listening HTTP ports are available.

+ {/if} +
+
+
+{/if} diff --git a/k8s/apps/mainloop/base/deployment-agent-controller.yaml b/k8s/apps/mainloop/base/deployment-agent-controller.yaml deleted file mode 100644 index b6c46e5..0000000 --- a/k8s/apps/mainloop/base/deployment-agent-controller.yaml +++ /dev/null @@ -1,79 +0,0 @@ -apiVersion: apps/v1 -kind: Deployment -metadata: - name: mainloop-agent-controller - namespace: mainloop -spec: - replicas: 1 - selector: - matchLabels: - app: mainloop-agent-controller - template: - metadata: - labels: - app: mainloop-agent-controller - spec: - securityContext: - runAsNonRoot: true - runAsUser: 1000 - runAsGroup: 1000 - fsGroup: 1000 - seccompProfile: - type: RuntimeDefault - imagePullSecrets: - - name: ghcr-secret - containers: - - name: agent-controller - image: ghcr.io/oldsj/mainloop-agent-controller:latest - ports: - - containerPort: 8001 - securityContext: - runAsNonRoot: true - runAsUser: 1000 - runAsGroup: 1000 - allowPrivilegeEscalation: false - capabilities: - drop: - - ALL - seccompProfile: - type: RuntimeDefault - env: - - name: CLAUDE_CODE_OAUTH_TOKEN - valueFrom: - secretKeyRef: - name: claude-credentials - key: oauth-token - - name: GH_TOKEN - valueFrom: - secretKeyRef: - name: mainloop-secrets - key: github-token - volumeMounts: - - name: workspace - mountPath: /workspace - resources: - requests: - memory: 512Mi - cpu: 500m - limits: - memory: 1Gi - cpu: 1000m - livenessProbe: - httpGet: - path: /health - port: 8001 - initialDelaySeconds: 30 - periodSeconds: 10 - timeoutSeconds: 5 - failureThreshold: 3 - readinessProbe: - httpGet: - path: /health - port: 8001 - initialDelaySeconds: 10 - periodSeconds: 5 - timeoutSeconds: 3 - failureThreshold: 3 - volumes: - - name: workspace - emptyDir: {} diff --git a/k8s/apps/mainloop/base/deployment-backend.yaml b/k8s/apps/mainloop/base/deployment-backend.yaml index 141d65e..0e9f8fd 100644 --- a/k8s/apps/mainloop/base/deployment-backend.yaml +++ b/k8s/apps/mainloop/base/deployment-backend.yaml @@ -47,11 +47,6 @@ spec: secretKeyRef: name: mainloop-secrets key: db-password - - name: CLAUDE_CODE_OAUTH_TOKEN - valueFrom: - secretKeyRef: - name: mainloop-secrets - key: claude-secret-token - name: GITHUB_TOKEN valueFrom: secretKeyRef: diff --git a/k8s/apps/mainloop/base/kustomization.yaml b/k8s/apps/mainloop/base/kustomization.yaml index c73fc1e..403b9a5 100644 --- a/k8s/apps/mainloop/base/kustomization.yaml +++ b/k8s/apps/mainloop/base/kustomization.yaml @@ -6,11 +6,8 @@ namespace: mainloop resources: - namespace.yaml - rbac-backend.yaml - - networkpolicy.yaml - deployment-backend.yaml - - deployment-agent-controller.yaml - deployment-frontend.yaml - service-backend.yaml - service-frontend.yaml - - service-agent-controller.yaml - configmap.yaml diff --git a/k8s/apps/mainloop/base/networkpolicy-task-namespace.yaml b/k8s/apps/mainloop/base/networkpolicy-task-namespace.yaml deleted file mode 100644 index 2ae3613..0000000 --- a/k8s/apps/mainloop/base/networkpolicy-task-namespace.yaml +++ /dev/null @@ -1,60 +0,0 @@ -# Network policies for task namespaces (worker agents) -# These should be applied when creating a new task namespace -# Workers get minimal network access: DNS + internet only, no cluster internal ---- -# Default deny-all for task namespace -apiVersion: networking.k8s.io/v1 -kind: NetworkPolicy -metadata: - name: default-deny-all - # namespace: - set dynamically when creating namespace -spec: - podSelector: {} - policyTypes: - - Ingress - - Egress ---- -# Allow DNS queries (required for internet access) -apiVersion: networking.k8s.io/v1 -kind: NetworkPolicy -metadata: - name: allow-dns - # namespace: - set dynamically when creating namespace -spec: - podSelector: {} - policyTypes: - - Egress - egress: - - to: - - namespaceSelector: - matchLabels: - kubernetes.io/metadata.name: kube-system - ports: - - protocol: UDP - port: 53 ---- -# Allow egress to internet ONLY (block all cluster internal) -apiVersion: networking.k8s.io/v1 -kind: NetworkPolicy -metadata: - name: allow-internet-only - # namespace: - set dynamically when creating namespace -spec: - podSelector: {} - policyTypes: - - Egress - egress: - # Allow to public internet only (block RFC1918 private networks) - - to: - - ipBlock: - cidr: 0.0.0.0/0 - except: - # Block all private/internal networks - - 10.0.0.0/8 # Private class A - - 172.16.0.0/12 # Private class B - - 192.168.0.0/16 # Private class C - - 169.254.0.0/16 # Link-local - - 127.0.0.0/8 # Loopback - - fc00::/7 # IPv6 private - - fe80::/10 # IPv6 link-local - - ::1/128 # IPv6 loopback diff --git a/k8s/apps/mainloop/base/networkpolicy.yaml b/k8s/apps/mainloop/base/networkpolicy.yaml deleted file mode 100644 index 657813e..0000000 --- a/k8s/apps/mainloop/base/networkpolicy.yaml +++ /dev/null @@ -1,32 +0,0 @@ ---- -# Restrict agent-controller to internet-only access (no internal cluster access) -apiVersion: networking.k8s.io/v1 -kind: NetworkPolicy -metadata: - name: restrict-agent-controller - namespace: mainloop -spec: - podSelector: - matchLabels: - app: mainloop-agent-controller - policyTypes: - - Egress - egress: - # Allow DNS - - to: - - namespaceSelector: - matchLabels: - kubernetes.io/metadata.name: kube-system - ports: - - protocol: UDP - port: 53 - - protocol: TCP - port: 53 - # Allow internet only (block private IP ranges) - - to: - - ipBlock: - cidr: 0.0.0.0/0 - except: - - 10.0.0.0/8 # Private class A - - 172.16.0.0/12 # Private class B - - 192.168.0.0/16 # Private class C diff --git a/k8s/apps/mainloop/base/rbac-backend.yaml b/k8s/apps/mainloop/base/rbac-backend.yaml index 36611cc..e7dff99 100644 --- a/k8s/apps/mainloop/base/rbac-backend.yaml +++ b/k8s/apps/mainloop/base/rbac-backend.yaml @@ -1,4 +1,3 @@ ---- apiVersion: v1 kind: ServiceAccount metadata: @@ -8,118 +7,21 @@ metadata: apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: - name: mainloop-backend-cluster-role + name: mainloop-backend-secret-reader rules: - # Namespace management - create/delete task namespaces - - apiGroups: [''] - resources: [namespaces] - verbs: [create, delete, get, list, watch] - - # Secret management - read secrets from mainloop, create in task namespaces - apiGroups: [''] resources: [secrets] - verbs: [create, get, list, delete] - - # ServiceAccount management - create worker service accounts in task namespaces - - apiGroups: [''] - resources: [serviceaccounts] - verbs: [create, get, delete] - - # RoleBinding management - bind worker role in task namespaces - - apiGroups: [rbac.authorization.k8s.io] - resources: [rolebindings] - verbs: [create, get, delete] - - # ClusterRole binding - allow binding the worker-role to service accounts - # This is required for RBAC escalation prevention - we must explicitly allow binding this role - - apiGroups: [rbac.authorization.k8s.io] - resources: [clusterroles] - resourceNames: [mainloop-worker-role] - verbs: [bind] - - # Job management - create/monitor/delete worker jobs - - apiGroups: [batch] - resources: [jobs] - verbs: [create, get, list, watch, delete] - - # Pod management - for job monitoring - - apiGroups: [''] - resources: [pods, pods/log] - verbs: [get, list, watch] - - # NetworkPolicy management - isolate task namespaces - - apiGroups: [networking.k8s.io] - resources: [networkpolicies] - verbs: [create, get, delete] + verbs: [get] --- apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRoleBinding metadata: - name: mainloop-backend-cluster-role-binding + name: mainloop-backend-secret-reader subjects: - kind: ServiceAccount name: mainloop-backend namespace: mainloop roleRef: kind: ClusterRole - name: mainloop-backend-cluster-role + name: mainloop-backend-secret-reader apiGroup: rbac.authorization.k8s.io ---- -# ClusterRole for worker jobs - grants permissions needed for Claude Code agents -# This is bound to the worker ServiceAccount via RoleBinding in each task namespace -apiVersion: rbac.authorization.k8s.io/v1 -kind: ClusterRole -metadata: - name: mainloop-worker-role -rules: - # Core resources - pods, services, configmaps, etc. - - apiGroups: [''] - resources: - - pods - - services - - configmaps - - persistentvolumeclaims - - serviceaccounts - - endpoints - verbs: [create, get, list, watch, update, patch, delete] - - # Pod logs - read only - - apiGroups: [''] - resources: - - pods/log - verbs: [get, list, watch] - - # Secrets - full access for app credentials - - apiGroups: [''] - resources: - - secrets - verbs: [create, get, list, watch, update, patch, delete] - - # Apps - deployments, replicasets, statefulsets, daemonsets - - apiGroups: [apps] - resources: - - deployments - - replicasets - - statefulsets - - daemonsets - verbs: [create, get, list, watch, update, patch, delete] - - # Batch - jobs, cronjobs - - apiGroups: [batch] - resources: - - jobs - - cronjobs - verbs: [create, get, list, watch, update, patch, delete] - - # Networking - ingresses, networkpolicies - - apiGroups: [networking.k8s.io] - resources: - - ingresses - - networkpolicies - verbs: [create, get, list, watch, update, patch, delete] - - # Events - for debugging - - apiGroups: [''] - resources: - - events - verbs: [get, list, watch] diff --git a/k8s/apps/mainloop/base/service-agent-controller.yaml b/k8s/apps/mainloop/base/service-agent-controller.yaml deleted file mode 100644 index 12ad891..0000000 --- a/k8s/apps/mainloop/base/service-agent-controller.yaml +++ /dev/null @@ -1,11 +0,0 @@ -apiVersion: v1 -kind: Service -metadata: - name: mainloop-agent-controller - namespace: mainloop -spec: - selector: - app: mainloop-agent-controller - ports: - - port: 8001 - targetPort: 8001 diff --git a/k8s/apps/mainloop/overlays/dev/agent-controller-patch.yaml b/k8s/apps/mainloop/overlays/dev/agent-controller-patch.yaml deleted file mode 100644 index ef6e867..0000000 --- a/k8s/apps/mainloop/overlays/dev/agent-controller-patch.yaml +++ /dev/null @@ -1,15 +0,0 @@ -# Patch agent-controller deployment for local testing -apiVersion: apps/v1 -kind: Deployment -metadata: - name: mainloop-agent-controller - namespace: mainloop -spec: - template: - spec: - # Remove GHCR image pull secrets (using local images) - imagePullSecrets: [] - containers: - - name: agent-controller - # Use locally loaded image, never try to pull - imagePullPolicy: Never diff --git a/k8s/apps/mainloop/overlays/dev/configmap-patch.yaml b/k8s/apps/mainloop/overlays/dev/configmap-patch.yaml index 293a858..2a324c1 100644 --- a/k8s/apps/mainloop/overlays/dev/configmap-patch.yaml +++ b/k8s/apps/mainloop/overlays/dev/configmap-patch.yaml @@ -10,8 +10,3 @@ data: DB_PORT: '5432' DB_NAME: mainloop FRONTEND_DOMAIN: localhost:3000 - # Backend internal URL for K8s Job callbacks - BACKEND_INTERNAL_URL: http://mainloop-backend.mainloop.svc.cluster.local:8000 - # Use local worker image for dev (loaded into Kind) - WORKER_IMAGE: mainloop-agent-controller:dev - WORKER_IMAGE_PULL_POLICY: Never diff --git a/k8s/apps/mainloop/overlays/dev/kustomization.yaml b/k8s/apps/mainloop/overlays/dev/kustomization.yaml index 02315ce..9a5d741 100644 --- a/k8s/apps/mainloop/overlays/dev/kustomization.yaml +++ b/k8s/apps/mainloop/overlays/dev/kustomization.yaml @@ -24,11 +24,6 @@ patches: kind: Deployment name: mainloop-frontend - - path: agent-controller-patch.yaml - target: - kind: Deployment - name: mainloop-agent-controller - # Override images to use local dev-tagged images (loaded into Kind, no registry) images: - name: ghcr.io/oldsj/mainloop-backend @@ -37,6 +32,3 @@ images: - name: ghcr.io/oldsj/mainloop-frontend newName: mainloop-frontend newTag: dev - - name: ghcr.io/oldsj/mainloop-agent-controller - newName: mainloop-agent-controller - newTag: dev diff --git a/k8s/apps/mainloop/overlays/prod/kustomization.yaml b/k8s/apps/mainloop/overlays/prod/kustomization.yaml index e2f4c80..da47838 100644 --- a/k8s/apps/mainloop/overlays/prod/kustomization.yaml +++ b/k8s/apps/mainloop/overlays/prod/kustomization.yaml @@ -15,4 +15,3 @@ patches: target: kind: Cluster name: mainloop-db - - path: shutdown-patch.yaml diff --git a/k8s/apps/mainloop/overlays/prod/personal-config-patch.yaml.example b/k8s/apps/mainloop/overlays/prod/personal-config-patch.yaml.example index 81f1158..31edbe0 100644 --- a/k8s/apps/mainloop/overlays/prod/personal-config-patch.yaml.example +++ b/k8s/apps/mainloop/overlays/prod/personal-config-patch.yaml.example @@ -52,14 +52,3 @@ spec: - name: FRONTEND_DOMAIN value: "mainloop.example.com" # Your frontend domain (for CORS) --- -apiVersion: apps/v1 -kind: Deployment -metadata: - name: mainloop-agent-controller - namespace: mainloop -spec: - template: - spec: - containers: - - name: agent-controller - image: ghcr.io/yourusername/mainloop-agent-controller:latest # Your GHCR username diff --git a/k8s/apps/mainloop/overlays/prod/shutdown-patch.yaml b/k8s/apps/mainloop/overlays/prod/shutdown-patch.yaml deleted file mode 100644 index ed15000..0000000 --- a/k8s/apps/mainloop/overlays/prod/shutdown-patch.yaml +++ /dev/null @@ -1,39 +0,0 @@ -apiVersion: apps/v1 -kind: Deployment -metadata: - name: mainloop-backend - namespace: mainloop -spec: - replicas: 0 ---- -apiVersion: apps/v1 -kind: Deployment -metadata: - name: mainloop-frontend - namespace: mainloop -spec: - replicas: 0 ---- -apiVersion: apps/v1 -kind: Deployment -metadata: - name: mainloop-agent-controller - namespace: mainloop -spec: - replicas: 0 ---- -apiVersion: postgresql.cnpg.io/v1 -kind: Cluster -metadata: - name: mainloop-db - namespace: mainloop - annotations: - cnpg.io/hibernation: "on" ---- -apiVersion: postgresql.cnpg.io/v1 -kind: Pooler -metadata: - name: mainloop-db-pooler - namespace: mainloop -spec: - instances: 0 diff --git a/k8s/apps/mainloop/overlays/spike-herdr/kustomization.yaml b/k8s/apps/mainloop/overlays/spike-herdr/kustomization.yaml deleted file mode 100644 index ed49c61..0000000 --- a/k8s/apps/mainloop/overlays/spike-herdr/kustomization.yaml +++ /dev/null @@ -1,43 +0,0 @@ -# Local-kind overlay for the native-agent slice: the `test` overlay (backend, frontend, Postgres) -# without the Claude Agent SDK controller, which this slice does not use. The workspace pod -# (Herdr + real claude/codex), its exec Role and the credential Secrets live in -# spikes/k8s-herdr-agents (applied by build-real-agents.sh); Secret values are created by path -# and never appear in manifests. -apiVersion: kustomize.config.k8s.io/v1beta1 -kind: Kustomization - -resources: - - ../test - -patches: - - target: - kind: Deployment - name: mainloop-agent-controller - patch: |- - - op: replace - path: /spec/replicas - value: 0 - - # Native main thread (plan r7): Claude under Herdr in pod main-0 replaces the SDK chat path. - # The SDK path stays available behind MAIN_THREAD_MODE=sdk. Model/effort/rotation use defaults - # (sonnet, medium, 20k tokens above baseline or 12 turns). - - target: - kind: Deployment - name: mainloop-backend - patch: |- - - op: add - path: /spec/template/spec/containers/0/env/- - value: { name: MAIN_THREAD_MODE, value: native } - -# The test overlay remaps ghcr.io/yourusername/*, but the base uses ghcr.io/oldsj/*; remap those -# to the locally built, kind-loaded images (imagePullPolicy is Never). -images: - - name: ghcr.io/oldsj/mainloop-backend - newName: mainloop-backend - newTag: test - - name: ghcr.io/oldsj/mainloop-frontend - newName: mainloop-frontend - newTag: test - - name: ghcr.io/oldsj/mainloop-agent-controller - newName: mainloop-agent-controller - newTag: test diff --git a/k8s/apps/mainloop/overlays/substrate-preview/backend-rbac.yaml b/k8s/apps/mainloop/overlays/substrate-preview/backend-rbac.yaml new file mode 100644 index 0000000..c0442a4 --- /dev/null +++ b/k8s/apps/mainloop/overlays/substrate-preview/backend-rbac.yaml @@ -0,0 +1,64 @@ +apiVersion: v1 +kind: ServiceAccount +metadata: + name: mainloop-backend + namespace: mainloop-control +automountServiceAccountToken: true +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: mainloop-backend-runtime + namespace: mainloop-control +rules: + - apiGroups: [''] + resources: [secrets] + resourceNames: + - mainloop-credential-owner-codex + - mainloop-credential-owner-claude + verbs: [get, patch] + - apiGroups: [batch] + resources: [jobs] + verbs: [create, get] + - apiGroups: [''] + resources: [pods] + verbs: [get, list] + - apiGroups: [''] + resources: [pods/log] + verbs: [get] +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: mainloop-backend-runtime + namespace: mainloop-control +subjects: + - kind: ServiceAccount + name: mainloop-backend + namespace: mainloop-control +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: mainloop-backend-runtime +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: mainloop-substrate-trust-bundles +rules: + - apiGroups: [certificates.k8s.io] + resources: [clustertrustbundles] + verbs: [list] +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: mainloop-substrate-trust-bundles +subjects: + - kind: ServiceAccount + name: mainloop-backend + namespace: mainloop-control +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: mainloop-substrate-trust-bundles diff --git a/k8s/apps/mainloop/overlays/substrate-preview/backend.yaml b/k8s/apps/mainloop/overlays/substrate-preview/backend.yaml new file mode 100644 index 0000000..b3a7751 --- /dev/null +++ b/k8s/apps/mainloop/overlays/substrate-preview/backend.yaml @@ -0,0 +1,81 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: mainloop-backend + namespace: mainloop-control +spec: + replicas: 1 + selector: + matchLabels: + app: mainloop-backend + template: + metadata: + labels: + app: mainloop-backend + spec: + serviceAccountName: mainloop-backend + containers: + - name: backend + image: localhost:5001/mainloop-backend:substrate-preview + imagePullPolicy: Always + ports: + - name: http + containerPort: 8000 + securityContext: + runAsNonRoot: true + runAsUser: 1000 + runAsGroup: 1000 + allowPrivilegeEscalation: false + capabilities: + drop: [ALL] + seccompProfile: + type: RuntimeDefault + envFrom: + - configMapRef: + name: mainloop-config + env: + - name: DB_USER + valueFrom: + secretKeyRef: + name: mainloop-db-app + key: username + - name: DB_PASSWORD + valueFrom: + secretKeyRef: + name: mainloop-db-app + key: password + volumeMounts: + - name: substrate-api-token + mountPath: /var/run/secrets/tokens/substrate-api + readOnly: true + resources: + requests: + memory: 1Gi + cpu: 250m + limits: + memory: 2Gi + cpu: 1000m + livenessProbe: + httpGet: + path: /health + port: http + initialDelaySeconds: 90 + periodSeconds: 10 + timeoutSeconds: 5 + failureThreshold: 3 + readinessProbe: + httpGet: + path: /health + port: http + initialDelaySeconds: 30 + periodSeconds: 5 + timeoutSeconds: 3 + failureThreshold: 3 + volumes: + - name: substrate-api-token + projected: + sources: + - serviceAccountToken: + audience: api.ate-system.svc + expirationSeconds: 3600 + path: token diff --git a/k8s/apps/mainloop/overlays/substrate-preview/configmap.yaml b/k8s/apps/mainloop/overlays/substrate-preview/configmap.yaml new file mode 100644 index 0000000..947d706 --- /dev/null +++ b/k8s/apps/mainloop/overlays/substrate-preview/configmap.yaml @@ -0,0 +1,19 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: mainloop-config + namespace: mainloop-control +data: + HOST: 0.0.0.0 + PORT: '8000' + DB_HOST: mainloop-db-pooler + DB_PORT: '5432' + DB_NAME: mainloop + FRONTEND_DOMAIN: localhost:3000 + # Kind-only fixed identity for local port-forwarded previews; do not copy to shared overlays. + SUBSTRATE_PREVIEW_LOCAL_DEV_MODE: 'true' + SUBSTRATE_ENDPOINT: api.ate-system.svc:443 + SUBSTRATE_TOKEN_FILE: /var/run/secrets/tokens/substrate-api/token + SUBSTRATE_ROUTER_ADDRESS: http://atenet-router.ate-system.svc.cluster.local:8081 + SUBSTRATE_SHIM_SECRET_NAMESPACE: mainloop-shim-secrets + SUBSTRATE_ACTOR_BINDINGS: '{"claude":{"atespace":"live-agent-gate","actor":"headless-claude-reproof","shim_token_secret_name":"mainloop-shim-live-agent-gate-headless-claude-reproof"},"codex":{"atespace":"native-codex","actor":"headless-codex-reproof","shim_token_secret_name":"mainloop-shim-native-codex-headless-codex-reproof"}}' diff --git a/k8s/apps/mainloop/overlays/substrate-preview/credential-secrets.yaml b/k8s/apps/mainloop/overlays/substrate-preview/credential-secrets.yaml new file mode 100644 index 0000000..af91eb3 --- /dev/null +++ b/k8s/apps/mainloop/overlays/substrate-preview/credential-secrets.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Secret +metadata: + name: mainloop-credential-owner-codex + namespace: mainloop-control +type: Opaque +data: {} +--- +apiVersion: v1 +kind: Secret +metadata: + name: mainloop-credential-owner-claude + namespace: mainloop-control +type: Opaque +data: {} diff --git a/k8s/apps/mainloop/overlays/substrate-preview/database.yaml b/k8s/apps/mainloop/overlays/substrate-preview/database.yaml new file mode 100644 index 0000000..fda2932 --- /dev/null +++ b/k8s/apps/mainloop/overlays/substrate-preview/database.yaml @@ -0,0 +1,73 @@ +apiVersion: v1 +kind: Service +metadata: + name: postgres + namespace: mainloop-control +spec: + clusterIP: None + selector: + app: postgres + ports: + - name: postgres + port: 5432 + targetPort: 5432 +--- +apiVersion: apps/v1 +kind: StatefulSet +metadata: + name: postgres + namespace: mainloop-control +spec: + serviceName: postgres + replicas: 1 + selector: + matchLabels: + app: postgres + template: + metadata: + labels: + app: postgres + spec: + containers: + - name: postgres + image: localhost:5001/postgres:16-alpine + imagePullPolicy: Always + ports: + - name: postgres + containerPort: 5432 + env: + - name: POSTGRES_USER + valueFrom: + secretKeyRef: + name: mainloop-db-app + key: username + - name: POSTGRES_PASSWORD + valueFrom: + secretKeyRef: + name: mainloop-db-app + key: password + - name: POSTGRES_DB + value: mainloop + volumeMounts: + - name: postgres-data + mountPath: /var/lib/postgresql/data + resources: + requests: + memory: 256Mi + cpu: 100m + limits: + memory: 512Mi + cpu: 500m + readinessProbe: + exec: + command: [pg_isready, -U, mainloop] + initialDelaySeconds: 5 + periodSeconds: 5 + volumeClaimTemplates: + - metadata: + name: postgres-data + spec: + accessModes: [ReadWriteOnce] + resources: + requests: + storage: 1Gi diff --git a/k8s/apps/mainloop/overlays/substrate-preview/frontend.yaml b/k8s/apps/mainloop/overlays/substrate-preview/frontend.yaml new file mode 100644 index 0000000..5031c06 --- /dev/null +++ b/k8s/apps/mainloop/overlays/substrate-preview/frontend.yaml @@ -0,0 +1,64 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: mainloop-frontend + namespace: mainloop-control +spec: + replicas: 1 + selector: + matchLabels: + app: mainloop-frontend + template: + metadata: + labels: + app: mainloop-frontend + spec: + securityContext: + runAsNonRoot: true + runAsUser: 1001 + runAsGroup: 1001 + fsGroup: 1001 + seccompProfile: + type: RuntimeDefault + containers: + - name: frontend + image: localhost:5001/mainloop-frontend:substrate-preview + imagePullPolicy: Always + ports: + - name: http + containerPort: 3000 + securityContext: + runAsNonRoot: true + runAsUser: 1001 + runAsGroup: 1001 + allowPrivilegeEscalation: false + capabilities: + drop: [ALL] + seccompProfile: + type: RuntimeDefault + env: + - name: ORIGIN + value: http://localhost:3000 + resources: + requests: + memory: 128Mi + cpu: 100m + limits: + memory: 256Mi + cpu: 250m + livenessProbe: + httpGet: + path: / + port: http + initialDelaySeconds: 30 + periodSeconds: 10 + timeoutSeconds: 5 + failureThreshold: 3 + readinessProbe: + httpGet: + path: / + port: http + initialDelaySeconds: 10 + periodSeconds: 5 + timeoutSeconds: 3 + failureThreshold: 3 diff --git a/k8s/apps/mainloop/overlays/substrate-preview/kustomization.yaml b/k8s/apps/mainloop/overlays/substrate-preview/kustomization.yaml new file mode 100644 index 0000000..4e5feb8 --- /dev/null +++ b/k8s/apps/mainloop/overlays/substrate-preview/kustomization.yaml @@ -0,0 +1,13 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +resources: + - namespace.yaml + - shim-secret-rbac.yaml + - configmap.yaml + - database.yaml + - backend-rbac.yaml + - credential-secrets.yaml + - backend.yaml + - frontend.yaml + - services.yaml diff --git a/k8s/apps/mainloop/overlays/substrate-preview/namespace.yaml b/k8s/apps/mainloop/overlays/substrate-preview/namespace.yaml new file mode 100644 index 0000000..e5069c7 --- /dev/null +++ b/k8s/apps/mainloop/overlays/substrate-preview/namespace.yaml @@ -0,0 +1,13 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: mainloop-control + labels: + mainloop.dev/role: control +--- +apiVersion: v1 +kind: Namespace +metadata: + name: mainloop-shim-secrets + labels: + mainloop.dev/role: shim-secrets diff --git a/k8s/apps/mainloop/overlays/substrate-preview/services.yaml b/k8s/apps/mainloop/overlays/substrate-preview/services.yaml new file mode 100644 index 0000000..d1cf0b1 --- /dev/null +++ b/k8s/apps/mainloop/overlays/substrate-preview/services.yaml @@ -0,0 +1,51 @@ +apiVersion: v1 +kind: Service +metadata: + name: mainloop-db-pooler + namespace: mainloop-control +spec: + selector: + app: postgres + ports: + - name: postgres + port: 5432 + targetPort: 5432 +--- +apiVersion: v1 +kind: Service +metadata: + name: mainloop-backend + namespace: mainloop-control +spec: + selector: + app: mainloop-backend + ports: + - name: http + port: 8000 + targetPort: 8000 +--- +apiVersion: v1 +kind: Service +metadata: + name: mainloop-preview + namespace: mainloop-control +spec: + selector: + app: mainloop-backend + ports: + - name: preview + port: 8001 + targetPort: 8000 +--- +apiVersion: v1 +kind: Service +metadata: + name: mainloop-frontend + namespace: mainloop-control +spec: + selector: + app: mainloop-frontend + ports: + - name: http + port: 3000 + targetPort: 3000 diff --git a/k8s/apps/mainloop/overlays/substrate-preview/shim-secret-rbac.yaml b/k8s/apps/mainloop/overlays/substrate-preview/shim-secret-rbac.yaml new file mode 100644 index 0000000..d46be36 --- /dev/null +++ b/k8s/apps/mainloop/overlays/substrate-preview/shim-secret-rbac.yaml @@ -0,0 +1,23 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: mainloop-manage-shim-secrets + namespace: mainloop-shim-secrets +rules: + - apiGroups: [''] + resources: [secrets] + verbs: [create, get, delete] +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: mainloop-manage-shim-secrets + namespace: mainloop-shim-secrets +subjects: + - kind: ServiceAccount + name: mainloop-backend + namespace: mainloop-control +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: mainloop-manage-shim-secrets diff --git a/k8s/apps/mainloop/overlays/test/agent-controller-patch.yaml b/k8s/apps/mainloop/overlays/test/agent-controller-patch.yaml deleted file mode 100644 index ef6e867..0000000 --- a/k8s/apps/mainloop/overlays/test/agent-controller-patch.yaml +++ /dev/null @@ -1,15 +0,0 @@ -# Patch agent-controller deployment for local testing -apiVersion: apps/v1 -kind: Deployment -metadata: - name: mainloop-agent-controller - namespace: mainloop -spec: - template: - spec: - # Remove GHCR image pull secrets (using local images) - imagePullSecrets: [] - containers: - - name: agent-controller - # Use locally loaded image, never try to pull - imagePullPolicy: Never diff --git a/k8s/apps/mainloop/overlays/test/configmap-patch.yaml b/k8s/apps/mainloop/overlays/test/configmap-patch.yaml index b016a33..2a324c1 100644 --- a/k8s/apps/mainloop/overlays/test/configmap-patch.yaml +++ b/k8s/apps/mainloop/overlays/test/configmap-patch.yaml @@ -10,8 +10,3 @@ data: DB_PORT: '5432' DB_NAME: mainloop FRONTEND_DOMAIN: localhost:3000 - # Backend internal URL for K8s Job callbacks - BACKEND_INTERNAL_URL: http://mainloop-backend.mainloop.svc.cluster.local:8000 - # Use local worker image for test (loaded into Kind) - WORKER_IMAGE: mainloop-agent-controller:test - WORKER_IMAGE_PULL_POLICY: Never diff --git a/k8s/apps/mainloop/overlays/test/kustomization.yaml b/k8s/apps/mainloop/overlays/test/kustomization.yaml index c9b93c4..1b13f1b 100644 --- a/k8s/apps/mainloop/overlays/test/kustomization.yaml +++ b/k8s/apps/mainloop/overlays/test/kustomization.yaml @@ -27,12 +27,6 @@ patches: kind: Deployment name: mainloop-frontend - # Patch agent-controller deployment for test environment - - path: agent-controller-patch.yaml - target: - kind: Deployment - name: mainloop-agent-controller - # Disable HTTPRoutes that don't exist in test (prevent errors) # Note: These resources don't exist in base anymore, no patching needed @@ -44,6 +38,3 @@ images: - name: ghcr.io/oldsj/mainloop-frontend newName: mainloop-frontend newTag: test - - name: ghcr.io/oldsj/mainloop-agent-controller - newName: mainloop-agent-controller - newTag: test diff --git a/models/README.md b/models/README.md index 111a4dd..5478a93 100644 --- a/models/README.md +++ b/models/README.md @@ -5,15 +5,12 @@ Shared Pydantic models for the mainloop project. This package contains all shared data models used across: - Backend API -- Claude agent integration - BigQuery schemas ## Models - `Conversation`: Conversation metadata and state - `Message`: Individual messages in a conversation -- `AgentTask`: Claude agent task definitions -- `AgentResponse`: Claude agent responses ## Usage diff --git a/models/src/models/__init__.py b/models/src/models/__init__.py index 80eb824..c816a13 100644 --- a/models/src/models/__init__.py +++ b/models/src/models/__init__.py @@ -1,6 +1,5 @@ """Shared Pydantic models for mainloop.""" -from models.agent import AgentResponse, AgentTask from models.conversation import Conversation, Message from models.native_agent import ( AttentionItem, @@ -38,13 +37,24 @@ QueueItemType, WorkflowEvent, ) +from models.workspace import ( + WorkspaceAgentKind, + WorkspaceCondition, + WorkspaceConditionStatus, + WorkspaceDesiredState, + WorkspaceDev, + WorkspaceLifecycle, + WorkspaceManifest, + WorkspaceObservedState, + WorkspacePort, + WorkspaceService, + WorkspaceTransition, +) __all__ = [ # Existing "Conversation", "Message", - "AgentTask", - "AgentResponse", # Session models "Session", "SessionCreate", @@ -80,4 +90,15 @@ "ProviderExtension", "ReconciliationEvidence", "WorkspaceBinding", + "WorkspaceAgentKind", + "WorkspaceCondition", + "WorkspaceConditionStatus", + "WorkspaceDesiredState", + "WorkspaceDev", + "WorkspaceLifecycle", + "WorkspaceManifest", + "WorkspaceObservedState", + "WorkspacePort", + "WorkspaceService", + "WorkspaceTransition", ] diff --git a/models/src/models/agent.py b/models/src/models/agent.py deleted file mode 100644 index 9d86921..0000000 --- a/models/src/models/agent.py +++ /dev/null @@ -1,31 +0,0 @@ -"""Claude agent task and response models.""" - -from datetime import datetime -from typing import Any - -from pydantic import BaseModel, Field - - -class AgentTask(BaseModel): - """A task for the Claude agent.""" - - id: str = Field(..., description="Unique task ID") - conversation_id: str = Field(..., description="Parent conversation ID") - prompt: str = Field(..., description="Task prompt for Claude") - context: dict[str, Any] | None = Field(None, description="Additional context") - created_at: datetime = Field( - default_factory=datetime.now, description="Creation timestamp" - ) - - -class AgentResponse(BaseModel): - """Response from the Claude agent.""" - - task_id: str = Field(..., description="Parent task ID") - content: str = Field(..., description="Response content") - tool_uses: list[dict[str, Any]] | None = Field( - None, description="Tool uses during execution" - ) - created_at: datetime = Field( - default_factory=datetime.now, description="Creation timestamp" - ) diff --git a/models/src/models/native_agent.py b/models/src/models/native_agent.py index 588839c..a6faacb 100644 --- a/models/src/models/native_agent.py +++ b/models/src/models/native_agent.py @@ -63,8 +63,6 @@ class NativeBinding(ContractModel): provider: Identifier runtime_type: Identifier native_session_id: Identifier - herdr_session_id: Identifier - herdr_agent_id: Identifier creation_mode: Literal["created", "attached", "discovered"] ownership_generation: Generation observed: ProviderExtension | None = None diff --git a/models/src/models/session.py b/models/src/models/session.py index 8c54157..8dd26cf 100644 --- a/models/src/models/session.py +++ b/models/src/models/session.py @@ -149,9 +149,9 @@ class SessionCreate(BaseModel): None, description="Main thread message ID to anchor this session to" ) - # Optional: run a real native agent under Herdr in the workspace pod + # Optional native runtime selection. Omitted sessions use Claude Code by default. agent_kind: Literal["claude", "codex"] | None = Field( - None, description="Native agent kind; omit for the existing session worker" + None, description="Native agent kind; defaults to Claude Code" ) @@ -183,7 +183,7 @@ class NativeDeliveryInfo(BaseModel): class NativeSessionInfo(BaseModel): - """Identity strip for a session bound to a native agent under Herdr.""" + """Identity strip for a session bound to a native agent in Substrate.""" session_id: str kind: Literal["claude", "codex"] @@ -194,11 +194,7 @@ class NativeSessionInfo(BaseModel): native_session_id: str | None = None model: str | None = None approval_policy: str - herdr_pane_id: str | None = None - herdr_terminal_id: str | None = None - herdr_workspace_id: str | None = None - workspace_pod: str | None = None - workspace_pod_uid: str | None = None + workspace_name: str | None = None workspace_ready: bool = False agent_live: bool | None = None generation: int = 1 diff --git a/models/src/models/workspace.py b/models/src/models/workspace.py new file mode 100644 index 0000000..62cd182 --- /dev/null +++ b/models/src/models/workspace.py @@ -0,0 +1,264 @@ +"""Declarative workspace intent and durable lifecycle observations.""" + +import ipaddress +import re +from enum import StrEnum +from typing import Annotated, Literal +from urllib.parse import urlsplit + +from pydantic import AwareDatetime, ConfigDict, Field, StrictStr, field_validator + +from models.native_agent import ContractModel + +WorkspaceIdentifier = Annotated[StrictStr, Field(min_length=1)] + + +class WorkspaceContractModel(ContractModel): + model_config = ConfigDict( + extra="forbid", frozen=True, strict=True, revalidate_instances="always" + ) + + +def _is_host_or_ip(host: str) -> bool: + try: + ipaddress.ip_address(host) + return True + except ValueError: + labels = host.lower().split(".") + return len(host) <= 253 and all( + label + and len(label) <= 63 + and re.fullmatch(r"[a-z0-9](?:[a-z0-9-]*[a-z0-9])?", label) + for label in labels + ) + + +class WorkspaceAgentKind(StrEnum): + CLAUDE = "claude" + CODEX = "codex" + + +class WorkspaceDesiredState(StrEnum): + RUNNING = "running" + SUSPENDED = "suspended" + + +class WorkspaceObservedState(StrEnum): + RUNNING = "running" + SUSPENDING = "suspending" + SUSPENDED = "suspended" + RESUMING = "resuming" + FAILED = "failed" + UNKNOWN = "unknown" + + +class WorkspacePort(WorkspaceContractModel): + name: Annotated[StrictStr, Field(min_length=1, pattern=r"^[a-z][a-z0-9-]{0,31}$")] + number: Annotated[int, Field(ge=1, le=65535, strict=True)] + protocol: Literal["http"] = "http" + + +class WorkspaceService(WorkspaceContractModel): + name: Annotated[StrictStr, Field(min_length=1, pattern=r"^[a-z][a-z0-9-]{0,31}$")] + image: Annotated[StrictStr, Field(min_length=1)] + env: dict[StrictStr, StrictStr] = Field(default_factory=dict) + ports: tuple[Annotated[int, Field(ge=1, le=65535, strict=True)], ...] = () + + @field_validator("ports", mode="before") + @classmethod + def parse_json_ports(cls, values): + return tuple(values) if isinstance(values, list) else values + + @field_validator("ports") + @classmethod + def unique_ports(cls, values: tuple[int, ...]) -> tuple[int, ...]: + if len(values) != len(set(values)): + raise ValueError("service ports must be unique") + return values + + +class WorkspaceDev(WorkspaceContractModel): + image: Annotated[StrictStr, Field(min_length=1)] | None = None + devcontainer_ref: Annotated[StrictStr, Field(min_length=1)] | None = None + actor_template: ( + Annotated[StrictStr, Field(pattern=r"^[a-z0-9](?:[-a-z0-9]{0,61}[a-z0-9])?$")] + | None + ) = None + services: tuple[WorkspaceService, ...] = () + ports: tuple[WorkspacePort, ...] = () + idle_timeout_minutes: Annotated[int, Field(ge=1, le=1440, strict=True)] = 30 + + @field_validator("services", "ports", mode="before") + @classmethod + def parse_json_collections(cls, values): + return tuple(values) if isinstance(values, list) else values + + @field_validator("image", "devcontainer_ref") + @classmethod + def validate_nonblank(cls, value: str | None) -> str | None: + if value is not None and not value.strip(): + raise ValueError("image and devcontainer_ref must not be blank") + return value + + @field_validator("services") + @classmethod + def unique_service_names( + cls, values: tuple[WorkspaceService, ...] + ) -> tuple[WorkspaceService, ...]: + names = [service.name for service in values] + if len(names) != len(set(names)): + raise ValueError("service names must be unique") + return values + + @field_validator("ports") + @classmethod + def unique_port_names_and_numbers( + cls, values: tuple[WorkspacePort, ...] + ) -> tuple[WorkspacePort, ...]: + names = [port.name for port in values] + numbers = [port.number for port in values] + if len(names) != len(set(names)) or len(numbers) != len(set(numbers)): + raise ValueError("workspace port names and numbers must be unique") + return values + + +class WorkspaceManifest(WorkspaceContractModel): + """Declarative workspace policy and dev environment settings.""" + + repo_url: StrictStr | None = None + branch: Annotated[StrictStr, Field(min_length=1)] + agent_kinds: tuple[WorkspaceAgentKind, ...] = () + skills: tuple[Annotated[StrictStr, Field(min_length=1)], ...] = () + mcp_servers: tuple[Annotated[StrictStr, Field(min_length=1)], ...] = () + egress_allowlist: tuple[Annotated[StrictStr, Field(min_length=1)], ...] = () + resource_class: Annotated[StrictStr, Field(pattern=r"^[a-z][a-z0-9-]{0,31}$")] + dev: WorkspaceDev | None = None + + @field_validator("dev") + @classmethod + def require_image_source(cls, value: WorkspaceDev | None) -> WorkspaceDev | None: + if value is not None and (value.image is None) == ( + value.devcontainer_ref is None + ): + raise ValueError("dev requires exactly one of image or devcontainer_ref") + return value + + @field_validator("repo_url") + @classmethod + def validate_repo_url(cls, value: str | None) -> str | None: + if value is None: + return None + if re.fullmatch(r"[^@\s]+@[^:\s]+:.+", value): + host = value.split("@", 1)[1].split(":", 1)[0] + if _is_host_or_ip(host) and value.split(":", 1)[1]: + return value + raise ValueError("repo_url must identify a valid host and repository path") + try: + parsed = urlsplit(value) + hostname = parsed.hostname + except ValueError as exc: + raise ValueError("repo_url must be a valid URL") from exc + if ( + parsed.scheme not in {"https", "ssh", "git"} + or not hostname + or not _is_host_or_ip(hostname) + or not parsed.path.strip("/") + or parsed.query + or parsed.fragment + or (parsed.scheme == "https" and parsed.username) + or parsed.password + ): + raise ValueError( + "repo_url must be an HTTPS, SSH, or Git URL with a host and repository path" + ) + return value + + @field_validator("branch") + @classmethod + def validate_branch(cls, value: str) -> str: + invalid = set(" ~^:?*[\\") + components = value.split("/") + if ( + any(char in invalid or ord(char) < 32 for char in value) + or value.startswith("-") + or value.startswith("/") + or value.endswith(("/", ".", ".lock")) + or ".." in value + or "@{" in value + or value == "@" + or "//" in value + or any( + component in ("", ".", "..") + or component.startswith(".") + or component.endswith((".", ".lock")) + for component in components + ) + ): + raise ValueError("branch is not a valid Git branch name") + return value + + @field_validator("skills", "mcp_servers", "egress_allowlist") + @classmethod + def validate_references(cls, values: tuple[str, ...]) -> tuple[str, ...]: + if len(values) != len(set(values)): + raise ValueError("manifest references must be unique") + if any( + any(char.isspace() or ord(char) < 32 for char in value) for value in values + ): + raise ValueError("manifest references must not contain whitespace") + return values + + @field_validator("egress_allowlist") + @classmethod + def validate_egress_hosts(cls, values: tuple[str, ...]) -> tuple[str, ...]: + for host in values: + if host != host.lower() or host.endswith(".") or "*" in host: + raise ValueError( + "egress entries must be exact lowercase hostnames or IP addresses" + ) + try: + ipaddress.ip_address(host) + continue + except ValueError: + pass + if not _is_host_or_ip(host): + raise ValueError( + "egress entries must be exact lowercase hostnames or IP addresses" + ) + return values + + +class WorkspaceConditionStatus(StrEnum): + TRUE = "True" + FALSE = "False" + UNKNOWN = "Unknown" + + +class WorkspaceCondition(WorkspaceContractModel): + type: Annotated[StrictStr, Field(min_length=1)] + status: WorkspaceConditionStatus + reason: Annotated[StrictStr, Field(min_length=1)] + message: StrictStr + last_transition_time: AwareDatetime + + +class WorkspaceTransition(WorkspaceContractModel): + from_state: WorkspaceObservedState | None = None + to_state: WorkspaceObservedState + reason: Annotated[StrictStr, Field(min_length=1)] + occurred_at: AwareDatetime + + +class WorkspaceLifecycle(WorkspaceContractModel): + workspace_id: WorkspaceIdentifier + session_id: WorkspaceIdentifier + desired_state: WorkspaceDesiredState + observed_state: WorkspaceObservedState + manifest: WorkspaceManifest + conditions: tuple[WorkspaceCondition, ...] = () + last_transition: WorkspaceTransition | None = None + operation_id: WorkspaceIdentifier | None = None + snapshot_ref: WorkspaceIdentifier | None = None + last_activity_at: AwareDatetime | None = None + ownership_generation: Annotated[int, Field(ge=1, strict=True)] = 1 + updated_at: AwareDatetime diff --git a/scripts/kind/create-secrets.sh b/scripts/kind/create-secrets.sh index 95784e4..fb7eca7 100755 --- a/scripts/kind/create-secrets.sh +++ b/scripts/kind/create-secrets.sh @@ -25,18 +25,10 @@ set +a # Create mainloop namespace if not exists kubectl --context="${KIND_CONTEXT}" create namespace mainloop --dry-run=client -o yaml | kubectl --context="${KIND_CONTEXT}" apply -f - -# Create claude-credentials secret (for agent-controller) -echo "Creating claude-credentials..." -kubectl --context="${KIND_CONTEXT}" create secret generic claude-credentials \ - --namespace mainloop \ - --from-literal=oauth-token="${CLAUDE_CODE_OAUTH_TOKEN-}" \ - --dry-run=client -o yaml | kubectl --context="${KIND_CONTEXT}" apply -f - - -# Create mainloop-secrets secret (for backend) +# Create mainloop-secrets secret (for backend control-plane access) echo "Creating mainloop-secrets..." kubectl --context="${KIND_CONTEXT}" create secret generic mainloop-secrets \ --namespace mainloop \ - --from-literal=claude-secret-token="${CLAUDE_CODE_OAUTH_TOKEN-}" \ --from-literal=github-token="${GITHUB_TOKEN-}" \ --from-literal=db-username=mainloop \ --from-literal=db-password=mainloop \ diff --git a/scripts/kind/deploy.sh b/scripts/kind/deploy.sh index ade4e75..c1da51a 100755 --- a/scripts/kind/deploy.sh +++ b/scripts/kind/deploy.sh @@ -12,8 +12,8 @@ echo "Using context: ${KIND_CONTEXT}" # Delete old deployments and wait for pods to terminate echo "Cleaning up old deployments..." -kubectl --context="${KIND_CONTEXT}" delete deployment mainloop-frontend mainloop-backend mainloop-agent-controller -n mainloop --ignore-not-found=true --wait=true -kubectl --context="${KIND_CONTEXT}" wait --for=delete pod -l 'app in (mainloop-frontend, mainloop-backend, mainloop-agent-controller)' -n mainloop --timeout=60s 2>/dev/null || true +kubectl --context="${KIND_CONTEXT}" delete deployment mainloop-frontend mainloop-backend -n mainloop --ignore-not-found=true --wait=true +kubectl --context="${KIND_CONTEXT}" wait --for=delete pod -l 'app in (mainloop-frontend, mainloop-backend)' -n mainloop --timeout=60s 2>/dev/null || true # Apply test overlay echo "Applying manifests..." @@ -23,7 +23,6 @@ kubectl --context="${KIND_CONTEXT}" apply -k "${REPO_ROOT}/k8s/apps/mainloop/ove echo "Waiting for deployments..." kubectl --context="${KIND_CONTEXT}" rollout status deployment/mainloop-backend -n mainloop --timeout=120s kubectl --context="${KIND_CONTEXT}" rollout status deployment/mainloop-frontend -n mainloop --timeout=120s -kubectl --context="${KIND_CONTEXT}" rollout status deployment/mainloop-agent-controller -n mainloop --timeout=120s kubectl --context="${KIND_CONTEXT}" rollout status statefulset/postgres -n mainloop --timeout=120s echo "=== Deployment complete ===" diff --git a/scripts/kind/load-images.sh b/scripts/kind/load-images.sh index c4bd45d..a4870e0 100755 --- a/scripts/kind/load-images.sh +++ b/scripts/kind/load-images.sh @@ -19,15 +19,10 @@ docker build -f frontend/Dockerfile \ --build-arg VITE_API_URL=http://localhost:8081 \ -t mainloop-frontend:test . -echo "Building agent-controller..." -docker build -f claude-agent/Dockerfile \ - -t mainloop-agent-controller:test ./claude-agent - # Load images into Kind echo "Loading images into Kind cluster..." kind load docker-image mainloop-backend:test --name "${CLUSTER_NAME}" kind load docker-image mainloop-frontend:test --name "${CLUSTER_NAME}" -kind load docker-image mainloop-agent-controller:test --name "${CLUSTER_NAME}" echo "=== Images loaded ===" docker exec "${CLUSTER_NAME}-control-plane" crictl images | grep mainloop || true diff --git a/scripts/kind/reset-data.sh b/scripts/kind/reset-data.sh index fe992c2..6e5c1d4 100755 --- a/scripts/kind/reset-data.sh +++ b/scripts/kind/reset-data.sh @@ -1,5 +1,5 @@ #!/usr/bin/env bash -# Reset database and k8s task namespaces +# Reset the local development database set -euo pipefail CLUSTER_NAME="${KIND_CLUSTER_NAME:-mainloop-test}" @@ -7,19 +7,6 @@ CONTEXT="kind-${CLUSTER_NAME}" echo "=== Using context: ${CONTEXT} ===" -echo "=== Cleaning up k8s task namespaces ===" -# Delete all task-* namespaces (legacy worker workflows) -for ns in $(kubectl --context "${CONTEXT}" get ns -o name 2>/dev/null | grep "^namespace/task-" | cut -d/ -f2); do - echo "Deleting namespace: ${ns}" - kubectl --context "${CONTEXT}" delete ns "${ns}" --wait=false 2>/dev/null || true -done - -# Delete all mainloop-session-* namespaces (session workers) -for ns in $(kubectl --context "${CONTEXT}" get ns -o name 2>/dev/null | grep "^namespace/mainloop-session-" | cut -d/ -f2); do - echo "Deleting namespace: ${ns}" - kubectl --context "${CONTEXT}" delete ns "${ns}" --wait=false 2>/dev/null || true -done - echo "=== Resetting database ===" # Drop both public and dbos schemas to fully reset state kubectl --context "${CONTEXT}" exec -n mainloop postgres-0 -- psql -U mainloop -d mainloop -c " diff --git a/scripts/substrate-preview.sh b/scripts/substrate-preview.sh new file mode 100755 index 0000000..bd9aa5b --- /dev/null +++ b/scripts/substrate-preview.sh @@ -0,0 +1,329 @@ +#!/usr/bin/env bash +set -Eeuo pipefail + +CONTEXT=kind-substrate-preview +NAMESPACE=mainloop-control +REGISTRY=localhost:5001 +IMAGE_TAG=substrate-preview +SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd -- "${SCRIPT_DIR}/.." && pwd)" +PREVIEW_KUBECONFIG= +PREFLIGHT_DIGEST= + +usage() { + cat <<'EOF' +Usage: scripts/substrate-preview.sh [component] + +Commands: + build Build app images, mirror PostgreSQL, push and preflight manifests + deploy Apply the Kustomize overlay to kind-substrate-preview + open Port-forward frontend and backend; Ctrl+C stops both forwards + status Show Mainloop pods, Substrate actors, and WorkerPools + logs [component] Follow backend, frontend, or postgres logs (default: backend) + +Cluster commands require SUBSTRATE_PREVIEW_KUBECONFIG to name the preview kubeconfig. +EOF +} + +fail() { + printf 'Error: %s\n' "$*" >&2 + exit 1 +} + +require_command() { + command -v "$1" >/dev/null 2>&1 || fail "required command not found: $1" +} + +require_preview_kubeconfig() { + [[ -n ${SUBSTRATE_PREVIEW_KUBECONFIG-} ]] || + fail 'SUBSTRATE_PREVIEW_KUBECONFIG is unset; set it to the preview kubeconfig path.' + [[ -f ${SUBSTRATE_PREVIEW_KUBECONFIG} && -r ${SUBSTRATE_PREVIEW_KUBECONFIG} ]] || + fail "preview kubeconfig is not a readable file: ${SUBSTRATE_PREVIEW_KUBECONFIG}" + PREVIEW_KUBECONFIG="${SUBSTRATE_PREVIEW_KUBECONFIG}" +} + +kube() { + kubectl --kubeconfig "${PREVIEW_KUBECONFIG}" --context "${CONTEXT}" "$@" +} + +image_digest_file() { + local state_root="${XDG_STATE_HOME:-${HOME-}}" + [[ -n ${state_root} ]] || fail 'HOME is unset and XDG_STATE_HOME is unset; cannot locate the image digest state file.' + [[ ${state_root} == /* ]] || fail 'XDG_STATE_HOME must be an absolute path.' + printf '%s/mainloop/substrate-preview/image-digests\n' "${state_root%/}" +} + +validate_image_digest() { + local image_name="$1" + local digest="$2" + [[ ${digest} =~ ^sha256:[a-f0-9]{64}$ ]] || + fail "recorded ${image_name} digest is missing or invalid; run scripts/substrate-preview.sh build first." +} + +load_image_digests() { + local digest_file + digest_file="$(image_digest_file)" + [[ -f ${digest_file} && -r ${digest_file} ]] || + fail "no recorded image digests at ${digest_file}; run scripts/substrate-preview.sh build first." + + BACKEND_DIGEST="$(sed -n 's/^backend=//p' "${digest_file}")" + FRONTEND_DIGEST="$(sed -n 's/^frontend=//p' "${digest_file}")" + POSTGRES_DIGEST="$(sed -n 's/^postgres=//p' "${digest_file}")" + validate_image_digest backend "${BACKEND_DIGEST}" + validate_image_digest frontend "${FRONTEND_DIGEST}" + validate_image_digest postgres "${POSTGRES_DIGEST}" +} + +record_image_digests() { + local backend_digest="$1" + local frontend_digest="$2" + local postgres_digest="$3" + local digest_file + local digest_dir + local temporary_file + + digest_file="$(image_digest_file)" + digest_dir="${digest_file%/*}" + mkdir -p -- "${digest_dir}" + chmod 700 "${digest_dir}" + temporary_file="$(mktemp "${digest_dir}/.image-digests.XXXXXX")" + chmod 600 "${temporary_file}" + { + printf 'backend=%s\n' "${backend_digest}" + printf 'frontend=%s\n' "${frontend_digest}" + printf 'postgres=%s\n' "${postgres_digest}" + } >"${temporary_file}" + mv -f -- "${temporary_file}" "${digest_file}" + printf 'Recorded preflighted image digests in %s\n' "${digest_file}" +} + +ate() { + kubectl-ate --kubeconfig "${PREVIEW_KUBECONFIG}" --context "${CONTEXT}" "$@" +} + +preflight_image() { + local image_ref="$1" + local repository="$2" + local push_log="${BUILD_TMPDIR}/${repository//\//_}.push.log" + local digest + + docker push "${image_ref}" 2>&1 | tee "${push_log}" + digest="$(sed -nE 's/.*digest: (sha256:[a-f0-9]{64}).*/\1/p' "${push_log}" | tail -n 1)" + [[ ${digest} =~ ^sha256:[a-f0-9]{64}$ ]] || + fail "could not read a sha256 digest from docker push output for ${image_ref}" + + printf 'Preflighting %s at %s\n' "${image_ref}" "${digest}" + curl -fsI \ + -H 'Accept: application/vnd.oci.image.index.v1+json, application/vnd.oci.image.manifest.v1+json, application/vnd.docker.distribution.manifest.list.v2+json, application/vnd.docker.distribution.manifest.v2+json' \ + "http://${REGISTRY}/v2/${repository}/manifests/${digest}" >/dev/null + PREFLIGHT_DIGEST="${digest}" +} + +build_images() { + require_command docker + require_command curl + BUILD_TMPDIR="$(mktemp -d)" + trap 'rm -rf -- "$BUILD_TMPDIR"' EXIT + + docker build -f backend/Dockerfile -t "${REGISTRY}/mainloop-backend:${IMAGE_TAG}" . + docker build -f frontend/Dockerfile \ + --build-arg VITE_API_URL=http://localhost:8000 \ + -t "${REGISTRY}/mainloop-frontend:${IMAGE_TAG}" . + docker pull postgres:16-alpine + docker tag postgres:16-alpine "${REGISTRY}/postgres:16-alpine" + + preflight_image "${REGISTRY}/mainloop-backend:${IMAGE_TAG}" mainloop-backend + local backend_digest="${PREFLIGHT_DIGEST}" + preflight_image "${REGISTRY}/mainloop-frontend:${IMAGE_TAG}" mainloop-frontend + local frontend_digest="${PREFLIGHT_DIGEST}" + preflight_image "${REGISTRY}/postgres:16-alpine" postgres + local postgres_digest="${PREFLIGHT_DIGEST}" + record_image_digests "${backend_digest}" "${frontend_digest}" "${postgres_digest}" +} + +apply_digest_pinned_overlay() ( + set -Eeuo pipefail + local overlay_dir="${REPO_ROOT}/k8s/apps/mainloop/overlays/substrate-preview" + local render_dir + render_dir="$(mktemp -d)" + trap 'rm -rf -- "${render_dir}"' EXIT + cp -R -- "${overlay_dir}/." "${render_dir}/" + cat >>"${render_dir}/kustomization.yaml" </dev/null + unset database_password + printf 'Created mainloop-db-app with a random password.\n' +} + +deploy_overlay() { + load_image_digests + require_command kubectl + require_preview_kubeconfig + kube apply -f "${REPO_ROOT}/k8s/apps/mainloop/overlays/substrate-preview/namespace.yaml" + ensure_database_secret + apply_digest_pinned_overlay +} + +show_status() { + require_command kubectl + require_command kubectl-ate + require_preview_kubeconfig + + printf '%s\n' '== Mainloop pods ==' + kube get pods -n "${NAMESPACE}" -o wide + printf '\n%s\n' '== Substrate actors ==' + ate get actor --all-atespaces + printf '\n%s\n' '== Substrate WorkerPools ==' + kube get workerpools --all-namespaces -o wide +} + +follow_logs() { + local component="${1:-backend}" + local target + + case "${component}" in + backend) target=deployment/mainloop-backend ;; + frontend) target=deployment/mainloop-frontend ;; + postgres) target=statefulset/postgres ;; + *) fail "unknown log component '${component}' (choose backend, frontend, or postgres)" ;; + esac + + require_command kubectl + require_preview_kubeconfig + kube logs --follow --tail=200 -n "${NAMESPACE}" "${target}" +} + +open_preview() { + require_command kubectl + require_preview_kubeconfig + + local forward_dir + local frontend_pid + local backend_pid + local preview_pid + local frontend_ready=0 + local backend_ready=0 + local preview_ready=0 + local attempt + + forward_dir="$(mktemp -d)" + cleanup_forwards() { + local result=$? + trap - EXIT + for child_pid in "${frontend_pid-}" "${backend_pid-}" "${preview_pid-}"; do + if [[ -n ${child_pid} ]]; then + kill "${child_pid}" 2>/dev/null || true + wait "${child_pid}" 2>/dev/null || true + fi + done + if [[ ${result} -ne 0 ]]; then + for forward_log in "${forward_dir}"/*.log; do + [[ -s ${forward_log} ]] && cat "${forward_log}" >&2 + done + fi + rm -rf -- "${forward_dir}" + return "${result}" + } + trap cleanup_forwards EXIT + trap 'exit 130' INT + trap 'exit 143' TERM + + kube port-forward --address 127.0.0.1 -n "${NAMESPACE}" service/mainloop-frontend 3000:3000 \ + >"${forward_dir}/frontend.log" 2>&1 & + frontend_pid=$! + kube port-forward --address 127.0.0.1 -n "${NAMESPACE}" service/mainloop-backend 8000:8000 \ + >"${forward_dir}/backend.log" 2>&1 & + backend_pid=$! + kube port-forward --address 127.0.0.1 -n "${NAMESPACE}" service/mainloop-preview 8001:8001 \ + >"${forward_dir}/preview.log" 2>&1 & + preview_pid=$! + + attempt=0 + while ((attempt < 60)); do + attempt=$((attempt + 1)) + if ! kill -0 "${frontend_pid}" 2>/dev/null || ! kill -0 "${backend_pid}" 2>/dev/null || ! kill -0 "${preview_pid}" 2>/dev/null; then + fail 'a port-forward exited before all local ports became ready' + fi + if (echo >/dev/tcp/127.0.0.1/3000) >/dev/null 2>&1; then + frontend_ready=1 + fi + if (echo >/dev/tcp/127.0.0.1/8000) >/dev/null 2>&1; then + backend_ready=1 + fi + if (echo >/dev/tcp/127.0.0.1/8001) >/dev/null 2>&1; then + preview_ready=1 + fi + [[ ${frontend_ready} -eq 1 && ${backend_ready} -eq 1 && ${preview_ready} -eq 1 ]] && break + sleep 0.5 + done + [[ ${frontend_ready} -eq 1 && ${backend_ready} -eq 1 && ${preview_ready} -eq 1 ]] || + fail 'timed out waiting for preview, frontend, and backend port-forwards' + + printf 'Mainloop: http://127.0.0.1:3000\n' + printf 'Backend API: http://127.0.0.1:8000/docs\n' + printf 'Preview proxy: http://--.preview.localhost:8001\n' + printf 'Press Ctrl+C to stop the port-forwards.\n' + wait -n "${frontend_pid}" "${backend_pid}" "${preview_pid}" +} + +main() { + local command="${1-}" + case "${command}" in + build) + [[ $# -eq 1 ]] || fail 'build takes no additional arguments' + cd "${REPO_ROOT}" + build_images + ;; + deploy) + [[ $# -eq 1 ]] || fail 'deploy takes no additional arguments' + deploy_overlay + ;; + open) + [[ $# -eq 1 ]] || fail 'open takes no additional arguments' + open_preview + ;; + status) + [[ $# -eq 1 ]] || fail 'status takes no additional arguments' + show_status + ;; + logs) + [[ $# -le 2 ]] || fail 'logs accepts at most one component' + follow_logs "${2:-backend}" + ;; + -h | --help | help) + usage + ;; + *) + usage >&2 + fail "unknown command '${command-}'" + ;; + esac +} + +main "$@" diff --git a/spikes/k8s-herdr-agents/Dockerfile b/spikes/k8s-herdr-agents/Dockerfile deleted file mode 100644 index b4c0cda..0000000 --- a/spikes/k8s-herdr-agents/Dockerfile +++ /dev/null @@ -1,17 +0,0 @@ -# Spike image: real Herdr + real claude/codex CLIs (+ deterministic stand-in agents). Non-root. -# herdr, claude and codex binaries are copied from the host into the build context by the -# build script (never committed). No credentials are baked in: they arrive as Kubernetes Secrets. -FROM debian:bookworm-slim -RUN apt-get update && apt-get install -y --no-install-recommends jq ca-certificates git ripgrep curl \ - && rm -rf /var/lib/apt/lists/* \ - && useradd -m -u 10001 agent -COPY herdr /usr/local/bin/herdr -COPY claude /usr/local/bin/claude -COPY codex /usr/local/bin/codex -COPY codex-code-mode-host /usr/local/bin/codex-code-mode-host -COPY bin/standin-agent /usr/local/bin/standin-agent -# One stand-in implementation installed under two Herdr-recognised kind names. -RUN ln -s standin-agent /usr/local/bin/pi && ln -s standin-agent /usr/local/bin/qwen -COPY bin/agentctl bin/entrypoint.sh bin/mainloop /usr/local/bin/ -USER 10001:10001 -ENTRYPOINT ["/usr/local/bin/entrypoint.sh"] diff --git a/spikes/k8s-herdr-agents/bin/agentctl b/spikes/k8s-herdr-agents/bin/agentctl deleted file mode 100755 index 35f944b..0000000 --- a/spikes/k8s-herdr-agents/bin/agentctl +++ /dev/null @@ -1,235 +0,0 @@ -#!/usr/bin/env bash -# shellcheck disable=SC2312,SC2310,SC2311,SC2249 # pedantic optional checks; spike scripts -# Pod-side operations, same verbs for every agent kind. Kind, args and resume syntax come from -# /etc/agent-config/.env (ConfigMap), not from this script. -# agentctl start [--name N] [--new-id ID | --resume ID] start under Herdr -# context-model options: --cwd-rel D (scratch cwd under the workspace root), --model M, -# --effort E, --standing-b64 B (standing context file), --token T (per-binding CLI token) -# agentctl send deliver one prompt (Herdr input only; never reads a reply) -# agentctl native-id discover the native session id (from the native journal) -# agentctl journal print native journal lines after line -# agentctl status Herdr liveness/state hint (JSON) -# agentctl stop -# agentctl prompt stand-in only: deliver and grep the reply from the pane -# agentctl identity -# Replies for real agents are read from the native journals via `journal`, never from the pane. -set -eu -cmd="${1:?usage: agentctl start|send|native-id|journal|status|stop|prompt|identity ...}" -shift -H=(herdr --session "${HERDR_SESSION}") -STATE="${WORKSPACE_PATH}/.mainloop" -conf_dir="${AGENT_CONFIG_DIR:-/etc/agent-config}" - -load_conf() { # - [[ -f "${conf_dir}/$1.env" ]] || { - echo "no binding config: ${conf_dir}/$1.env" >&2 - exit 2 - } - # shellcheck disable=SC1090 - . "${conf_dir}/$1.env" -} - -cwd="${WORKSPACE_PATH}" - -trust_cwd() { # : pre-accept the trust dialog for a scratch cwd (no human at the TUI) - case "$1" in - claude) - local tmp - tmp="$(mktemp)" - jq --arg p "$2" '.projects[$p] = ((.projects[$p] // {}) + {hasTrustDialogAccepted: true, hasCompletedProjectOnboarding: true})' "${HOME}/.claude.json" >"${tmp}" && - cat "${tmp}" >"${HOME}/.claude.json" - rm -f "${tmp}" - ;; - codex) - grep -qF "[projects.\"$2\"]" "${CODEX_HOME}/config.toml" || printf '\n[projects."%s"]\ntrust_level = "trusted"\n' "$2" >>"${CODEX_HOME}/config.toml" - ;; - esac -} - -pane_for_name() { # : one Herdr workspace (labelled with the name) per agent - local ws - ws="$("${H[@]}" workspace list | jq -r --arg b "$1" '.result.workspaces[] | select(.label==$b) | .workspace_id' | head -n1)" - if [[ -z ${ws} ]]; then - ws="$("${H[@]}" workspace create --label "$1" --cwd "${cwd}" | jq -r .result.workspace.workspace_id)" - fi - "${H[@]}" pane list --workspace "${ws}" | jq -r '.result.panes[0].pane_id' -} - -journal_file() { # - case "$1" in - claude) find "${CLAUDE_CONFIG_DIR:-${HOME}/.claude}/projects" -name "$2.jsonl" 2>/dev/null | head -n1 ;; - codex) find "${CODEX_HOME}/sessions" -name "rollout-*-$2.jsonl" 2>/dev/null | head -n1 ;; - *) - echo "no journal for kind $1" >&2 - return 1 - ;; - esac -} - -case "${cmd}" in -start) - binding="${1:?binding required}" - shift - name="${binding}" - mode=new - nid="" - cwd_rel="" - model="" - effort="" - standing_b64="" - token="" - while [[ $# -gt 0 ]]; do - case "$1" in - --name) - name="$2" - shift 2 - ;; - --new-id) - mode=new - nid="$2" - shift 2 - ;; - --resume) - mode=resume - nid="$2" - shift 2 - ;; - --cwd-rel) - cwd_rel="$2" - shift 2 - ;; - --model) - model="$2" - shift 2 - ;; - --effort) - effort="$2" - shift 2 - ;; - --standing-b64) - standing_b64="$2" - shift 2 - ;; - --token) - token="$2" - shift 2 - ;; - *) - echo "unknown option $1" >&2 - exit 2 - ;; - esac - done - load_conf "${binding}" - ident="${STATE}/${name}.identity.json" - if "${H[@]}" agent get "${name}" >/dev/null 2>&1; then - echo "agent ${name} already live" - exit 0 - fi - if [[ ${mode} == resume ]]; then args="${AGENT_RESUME_ARGS:-${AGENT_ARGS}}"; else args="${AGENT_NEW_ARGS:-${AGENT_ARGS}}"; fi - if [[ -n ${cwd_rel} ]]; then - cwd="$(dirname "${WORKSPACE_PATH}")/${cwd_rel}" - mkdir -p "${cwd}/.mainloop" - chmod 700 "${cwd}/.mainloop" - # Secrets and generated context go to files on the PVC (0600), never into the pane command. - [[ -z ${token} ]] || ( - umask 077 - printf '%s' "${token}" >"${cwd}/.mainloop/token" - ) - [[ -z ${standing_b64} ]] || printf '%s' "${standing_b64}" | base64 -d >"${cwd}/.mainloop/standing.md" - [[ ! -f "${conf_dir}/${binding}.settings.json" ]] || cp "${conf_dir}/${binding}.settings.json" "${cwd}/.mainloop/settings.json" - trust_cwd "${AGENT_KIND}" "${cwd}" - fi - args="${args//\{id\}/${nid}}" - args="${args//\{model\}/${model}}" - args="${args//\{effort\}/${effort}}" - args="${args//\{standing\}/${cwd}/.mainloop/standing.md}" - args="${args//\{settings\}/${cwd}/.mainloop/settings.json}" - mkdir -p "${STATE}" - touch "${STATE}/${name}.started" - pane="$(pane_for_name "${name}")" - # $args is intentionally word-split: it is the native executable's argument list. - # shellcheck disable=SC2086 - "${H[@]}" agent start "${name}" --kind "${AGENT_KIND}" --pane "${pane}" --timeout 60000 -- ${args} >/dev/null - "${H[@]}" agent get "${name}" | jq -c --arg b "${binding}" --arg k "${AGENT_KIND}" --arg args "${args}" --arg mode "${mode}" --arg nid "${nid}" \ - '.result.agent | {binding:$b, kind:$k, args:$args, mode:$mode, native_session_id:(if $nid=="" then null else $nid end), herdr_agent:.agent, herdr_name:.name, pane_id, terminal_id, workspace_id, status:.agent_status}' >"${ident}" - cat "${ident}" - ;; -send) - name="${1:?name required}" - text="${2:?text required}" - # One delivery, no --wait retries: Herdr status is a hint, the journal is the receipt. - "${H[@]}" agent prompt "${name}" "${text}" >/dev/null - echo sent - ;; -native-id) - name="${1:?name required}" - ident="${STATE}/${name}.identity.json" - kind="$(jq -r .kind "${ident}")" - known="$(jq -r '.native_session_id // empty' "${ident}")" - if [[ -n ${known} ]]; then - echo "${known}" - exit 0 - fi - case "${kind}" in - codex) - f="$(find "${CODEX_HOME}/sessions" -name 'rollout-*.jsonl' -newer "${STATE}/${name}.started" 2>/dev/null | sort | head -n1)" - [[ -n ${f} ]] || exit 1 - id="$(basename "${f}" .jsonl | sed -E 's/^rollout-[0-9T:-]+-//')" - ;; - *) exit 1 ;; - esac - tmp="$(mktemp)" - jq --arg id "${id}" '.native_session_id=$id' "${ident}" >"${tmp}" && cat "${tmp}" >"${ident}" && rm -f "${tmp}" - echo "${id}" - ;; -journal) - kind="$(jq -r .kind "${STATE}/${1:?name required}.identity.json")" - id="${2:?native id required}" - from="${3:-0}" - f="$(journal_file "${kind}" "${id}")" - [[ -n ${f} ]] || { - echo "#nofile" - exit 0 - } - n="$(wc -l <"${f}")" # complete (newline-terminated) lines only - printf '#file\t%s\t%s\n' "${f}" "${n}" - if [[ ${n} -gt ${from} ]]; then sed -n "$((from + 1)),${n}p" "${f}" | awk -v s="${from}" '{print (NR + s) "\t" $0}'; fi - ;; -status) - # A failed `agent get` must fail the verb (a pipeline would report jq's exit status). - out="$("${H[@]}" agent get "${1:?name required}")" || exit 1 - printf '%s' "${out}" | jq -c '.result.agent | {name, agent, pane_id, terminal_id, status: .agent_status}' - ;; -prompt) # stand-in agents only - binding="${1:?binding required}" - text="${2:?prompt text required}" - "${H[@]}" agent prompt "${binding}" "${text}" --wait --timeout 60000 >/dev/null - "${H[@]}" agent read "${binding}" | grep 'STANDIN-REPLY' | grep -F "echo=${text}" | tail -n1 - ;; -stop) - name="${1:?name required}" - kind="$(jq -r '.kind // empty' "${STATE}/${name}.identity.json" 2>/dev/null || true)" - if [[ ${kind} == claude ]]; then - # A pasted "/exit" is text, and the restricted main thread has slash commands disabled: - # two quick Ctrl-C key presses exit Claude Code (measured). - "${H[@]}" agent send-keys "${name}" ctrl+c ctrl+c >/dev/null - else - "${H[@]}" agent prompt "${name}" "/exit" >/dev/null - fi - for _ in $(seq 1 20); do - "${H[@]}" agent get "${name}" >/dev/null 2>&1 || { - echo "agent ${name} stopped" - exit 0 - } - sleep 0.5 - done - echo "agent ${name} still live after stop" >&2 - exit 1 - ;; -identity) cat "${STATE}/${1:?name required}.identity.json" ;; -*) - echo "unknown command ${cmd}" >&2 - exit 2 - ;; -esac diff --git a/spikes/k8s-herdr-agents/bin/entrypoint.sh b/spikes/k8s-herdr-agents/bin/entrypoint.sh deleted file mode 100755 index a9ab0be..0000000 --- a/spikes/k8s-herdr-agents/bin/entrypoint.sh +++ /dev/null @@ -1,35 +0,0 @@ -#!/usr/bin/env bash -# shellcheck disable=SC2312,SC2310,SC2311,SC2249 # pedantic optional checks; spike scripts -# Pod entrypoint: real Herdr headless server, with all state on the PVC. -# Seeds agent trust/onboarding state so agents start without a human at a dialog, and copies -# the read-only Codex auth Secret to a writable CODEX_HOME on the PVC. Never prints credentials. -set -eu -mkdir -p "${HOME}" "${WORKSPACE_PATH}" "${STANDIN_STATE_DIR}" "${CODEX_HOME}" "${HOME}/.claude" -[[ -d "${WORKSPACE_PATH}/.git" ]] || git -C "${WORKSPACE_PATH}" init -q - -# Claude Code: onboarding done, workspace trusted, bypass-permissions warning accepted. -# CLAUDE_CODE_OAUTH_TOKEN comes from a Secret-backed env var (subscription token). -if [[ ! -s "${HOME}/.claude.json" ]]; then - jq -n --arg p "${WORKSPACE_PATH}" '{ - hasCompletedOnboarding: true, - numStartups: 1, - theme: "dark", - projects: {($p): {hasTrustDialogAccepted: true, hasCompletedProjectOnboarding: true, allowedTools: []}} - }' >"${HOME}/.claude.json" -fi -[[ -s "${HOME}/.claude/settings.json" ]] || echo '{"skipDangerousModePermissionPrompt": true}' >"${HOME}/.claude/settings.json" - -# Codex: copy auth from the read-only Secret once (Codex rewrites auth.json on refresh, so the -# writable copy on the PVC is authoritative afterwards); trust the workspace. -if [[ -f /etc/agent-secrets/codex/auth.json ]] && [[ ! -s "${CODEX_HOME}/auth.json" ]]; then - install -m 600 /etc/agent-secrets/codex/auth.json "${CODEX_HOME}/auth.json" -fi -if [[ ! -s "${CODEX_HOME}/config.toml" ]]; then - printf '[projects."%s"]\ntrust_level = "trusted"\n' "${WORKSPACE_PATH}" >"${CODEX_HOME}/config.toml" -fi -# Codex shows an "Approaching rate limits - switch model?" modal after a turn, which swallows the next -# prompt. Hide only that nudge (Codex's own "keep current model, never show again"); no model change. -grep -q '^\[notice\]' "${CODEX_HOME}/config.toml" || printf '\n[notice]\nhide_rate_limit_model_nudge = true\n' >>"${CODEX_HOME}/config.toml" - -echo "herdr $(herdr --version) server starting (HOME=${HOME} session=${HERDR_SESSION})" -exec herdr --session "${HERDR_SESSION}" server diff --git a/spikes/k8s-herdr-agents/bin/standin-agent b/spikes/k8s-herdr-agents/bin/standin-agent deleted file mode 100755 index 43cced8..0000000 --- a/spikes/k8s-herdr-agents/bin/standin-agent +++ /dev/null @@ -1,63 +0,0 @@ -#!/usr/bin/env bash -# shellcheck disable=SC2312,SC2310,SC2311,SC2249 # pedantic optional checks; spike scripts -# Deterministic STAND-IN for a coding-agent CLI. Not a real provider agent. -# Installed under the executable names Herdr recognises (pi, qwen), so Herdr -# detects the kind from the process name and its bundled screen rules. -# Native state (session id + transcript) lives on the workspace volume and is -# resumed when the same --session-name starts again. -set -u -kind="$(basename "$0")" -session_name="default" -label="" -while [[ $# -gt 0 ]]; do - case "$1" in - --session-name) - session_name="$2" - shift 2 - ;; - --label) - label="$2" - shift 2 - ;; - *) shift ;; - esac -done - -state_dir="${STANDIN_STATE_DIR:-${HOME}/.standin}/${kind}" -mkdir -p "${state_dir}" -meta="${state_dir}/${session_name}.meta" -log="${state_dir}/${session_name}.jsonl" -if [[ -f ${meta} ]]; then - session_id="$(cat "${meta}")" - resumed="resumed" -else - session_id="${kind}-$(od -An -N4 -tx1 /dev/urandom | tr -d ' \n')" - printf '%s' "${session_id}" >"${meta}" - : >"${log}" - resumed="new" -fi -turns="$(wc -l <"${log}")" -last_prompt="none" -[[ ${turns} -gt 0 ]] && last_prompt="$(tail -n1 "${log}" | cut -f2)" - -echo "STAND-IN agent kind=${kind} label=${label:-none} (deterministic, no provider)" -echo "native session: ${session_id} (${resumed}, ${turns} prior turns)" - -while true; do - printf '\n> ' - IFS= read -r line || exit 0 - [[ ${line} == "/exit" ]] && exit 0 - printf '\r\033[2K' - # Working signals differ per kind, mirroring how Herdr detects each agent. - case "${kind}" in - qwen) printf '\033]0;\xe2\x97\x90 working\a' ;; - *) printf '\xe2\xa0\x8b Working... (esc to interrupt)' ;; - esac - sleep "${STANDIN_WORK_SECONDS:-2}" - printf '\r\033[2K' - [[ ${kind} == qwen ]] && printf '\033]0;\xe2\x97\x87 ready\a' - turns=$((turns + 1)) - printf '%s\t%s\n' "${turns}" "${line}" >>"${log}" - echo "STANDIN-REPLY kind=${kind} session=${session_id} turn=${turns} prior=${last_prompt} echo=${line}" - last_prompt="${line}" -done diff --git a/spikes/k8s-herdr-agents/build-real-agents.sh b/spikes/k8s-herdr-agents/build-real-agents.sh deleted file mode 100755 index ac6c928..0000000 --- a/spikes/k8s-herdr-agents/build-real-agents.sh +++ /dev/null @@ -1,30 +0,0 @@ -#!/usr/bin/env bash -# shellcheck disable=SC2312,SC2310,SC2311,SC2249 # pedantic optional checks; spike scripts -# Build the real-agent workspace image (host claude/codex/herdr copied into a transient build -# context, never committed), load it into kind-mainloop-test, create credential Secrets BY PATH -# (values are never printed), and apply the workspace manifest. No cleanup, nothing deleted. -set -euo pipefail -SPIKE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -RUN_ID="${RUN_ID:?RUN_ID required}" -KUBECONFIG_FILE="${KUBECONFIG_FILE:?run-owned kubeconfig required}" -CTX=kind-mainloop-test -NS=herdr-spike -IMAGE="mainloop-spike-herdr:real-${RUN_ID}${IMAGE_SUFFIX-}" -k() { kubectl --kubeconfig "${KUBECONFIG_FILE}" --context "${CTX}" "$@"; } -B="$(mktemp -d)" -trap 'rm -rf "$B"' EXIT -cp "$(readlink -f "$(command -v herdr)")" "${B}/herdr" -cp "$(readlink -f "$(command -v claude)")" "${B}/claude" -cp "$(readlink -f "$(command -v codex)")" "${B}/codex" -# Codex shell tools need its companion helper (without it: "codex-code-mode-host is missing"). -cp "$(dirname "$(readlink -f "$(command -v codex)")")/codex-code-mode-host" "${B}/codex-code-mode-host" -cp -r "${SPIKE_DIR}/bin" "${SPIKE_DIR}/Dockerfile" "${B}/" -sudo -n docker build -q -t "${IMAGE}" "${B}" -sudo -n docker image inspect "${IMAGE}" --format 'image {{.Id}} user={{.Config.User}}' -sudo -n "$(command -v kind)" load docker-image "${IMAGE}" --name mainloop-test -# Secrets by path. Claude token: whitespace stripped through a process substitution, never echoed. -k create ns "${NS}" --dry-run=client -o yaml | k apply -f - >/dev/null -k -n "${NS}" create secret generic claude-oauth --from-file=oauth-token=<(tr -d ' \r\n' <"${HOME}/.claude-token") --dry-run=client -o yaml | k apply -f - >/dev/null -k -n "${NS}" create secret generic codex-auth --from-file=auth.json="${HOME}/.codex/auth.json" --dry-run=client -o yaml | k apply -f - >/dev/null -sed "s#__IMAGE__#${IMAGE}#" "${SPIKE_DIR}/k8s/workspace.yaml" | k apply -f - -k -n "${NS}" rollout status statefulset/workspace --timeout=240s diff --git a/spikes/k8s-herdr-agents/demo.sh b/spikes/k8s-herdr-agents/demo.sh deleted file mode 100755 index 93d1216..0000000 --- a/spikes/k8s-herdr-agents/demo.sh +++ /dev/null @@ -1,122 +0,0 @@ -#!/usr/bin/env bash -# shellcheck disable=SC2312,SC2310,SC2311,SC2249 # pedantic optional checks; spike scripts -# Bounded local demo: kind + real Herdr + two stand-in agent kinds. -# Never cleans up: cluster, PVC, images and evidence are left for inspection. -set -euo pipefail - -SPIKE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -RUN_ID="${RUN_ID:-20260920T022900Z}" -EVIDENCE="${EVIDENCE_DIR:-${SPIKE_DIR}/../../.tasknotes/runs/${RUN_ID}/spike-evidence}" -CLUSTER=mainloop-test -CONTEXT=kind-mainloop-test -IMAGE="mainloop-spike-herdr:${RUN_ID}" -KUBECONFIG_FILE="${KUBECONFIG_FILE:-${EVIDENCE}/kubeconfig-${CLUSTER}}" # run-owned, never committed -NS=herdr-spike -SUDO="${SUDO:-sudo -n}" # docker is root-only on this host -KIND_BIN="$(command -v kind)" -HERDR_BIN="$(readlink -f "$(command -v herdr)")" - -mkdir -p "${EVIDENCE}" -LOG="${EVIDENCE}/demo.log" -BUILD_CTX="$(mktemp -d)" -trap 'rm -rf "$BUILD_CTX"' EXIT # only the transient build context is removed - -say() { printf '%s\n' "$*" | tee -a "${LOG}"; } -run() { - say "\$ $*" - "$@" 2>&1 | tee -a "${LOG}" -} -k() { kubectl --kubeconfig "${KUBECONFIG_FILE}" --context "${CONTEXT}" "$@"; } -kx() { k -n "${NS}" exec workspace-0 -c workspace -- "$@"; } - -say "== Mainloop spike: Kubernetes + Herdr + arbitrary agents (${RUN_ID}) ==" -say "REAL: kind cluster ${CLUSTER}, StatefulSet/PVC, non-root pod, Herdr $(herdr --version | cut -d' ' -f2) server + agent detection" -say "STAND-IN: 'pi' and 'qwen' executables are one deterministic script (no provider, no credentials)" - -# --- static checks --- -bash -n "${SPIKE_DIR}/demo.sh" "${SPIKE_DIR}"/bin/* -say "static: bash -n ok" - -# --- cluster (explicit run-owned kubeconfig; create only if absent) --- -if ! ${SUDO} "${KIND_BIN}" get clusters 2>/dev/null | grep -qx "${CLUSTER}"; then - run ${SUDO} "${KIND_BIN}" create cluster --name "${CLUSTER}" --kubeconfig "${KUBECONFIG_FILE}" --wait 120s - ${SUDO} chown "$(id -u):$(id -g)" "${KUBECONFIG_FILE}" -elif [[ ! -s ${KUBECONFIG_FILE} ]]; then - ${SUDO} "${KIND_BIN}" get kubeconfig --name "${CLUSTER}" >"${KUBECONFIG_FILE}" -fi -chmod 600 "${KUBECONFIG_FILE}" -[[ "$(k config current-context)" == "${CONTEXT}" ]] || { - say "wrong context" - exit 1 -} -say "context: $(k config current-context)" - -# --- image --- -cp "${HERDR_BIN}" "${BUILD_CTX}/herdr" -cp -r "${SPIKE_DIR}/bin" "${SPIKE_DIR}/Dockerfile" "${BUILD_CTX}/" -run ${SUDO} docker build -q -t "${IMAGE}" "${BUILD_CTX}" -IMAGE_ID="$(${SUDO} docker image inspect "${IMAGE}" --format '{{.Id}}')" -say "image: ${IMAGE} id=${IMAGE_ID}" -say "image user: $(${SUDO} docker image inspect "${IMAGE}" --format '{{.Config.User}}')" -say "image herdr: $(${SUDO} docker run --rm --entrypoint herdr "${IMAGE}" --version)" -run ${SUDO} "${KIND_BIN}" load docker-image "${IMAGE}" --name "${CLUSTER}" - -# --- deploy --- -sed "s#__IMAGE__#${IMAGE}#" "${SPIKE_DIR}/k8s/workspace.yaml" >"${EVIDENCE}/workspace.rendered.yaml" -run kubectl --kubeconfig "${KUBECONFIG_FILE}" --context "${CONTEXT}" apply -f "${EVIDENCE}/workspace.rendered.yaml" -k -n "${NS}" rollout status statefulset/workspace --timeout=180s | tee -a "${LOG}" -POD1_UID="$(k -n "${NS}" get pod workspace-0 -o jsonpath='{.metadata.uid}')" -PVC="$(k -n "${NS}" get pod workspace-0 -o jsonpath='{.spec.volumes[?(@.name=="workspace")].persistentVolumeClaim.claimName}')" -say "pod1 uid=${POD1_UID} pvc=${PVC}" -say "pod user: $(kx id)" -say "pod herdr: $(kx herdr --version)" -say "sa token mounted: $(kx sh -c 'ls /var/run/secrets/kubernetes.io 2>&1 | head -1')" -for _ in $(seq 1 20); do - kx herdr --session mainloop-spike status server >/dev/null 2>&1 && break - sleep 1 -done - -# --- journey 1: both kinds, same operation --- -NONCE1="n1-${RANDOM}${RANDOM}" -for b in alpha beta; do - say "-- start ${b} (config: $(kx sh -c "tr '\n' ' ' /dev/null 2>&1 && break - sleep 1 -done -say "persisted identities: $(kx sh -c 'cat /workspace/repo/.mainloop/*.identity.json')" -say "persisted native state: $(kx sh -c 'ls /workspace/.standin/*')" -say "herdr live agents after restart (agent processes do not survive): $(kx herdr --session mainloop-spike agent list | jq -c '.result.agents|length')" - -# --- journey 2: restart agents, resume native state, follow-up prompt --- -NONCE2="n2-${RANDOM}${RANDOM}" -for b in alpha beta; do - kx agentctl start "${b}" | tee -a "${LOG}" - say "-- follow-up prompt ${b} nonce=${NONCE2}-${b}" - REPLY="$(kx agentctl prompt "${b}" "${NONCE2}-${b}")" - say "${REPLY}" - case "${REPLY}" in *"turn=2"*"prior=${NONCE1}-${b}"*) say "OK ${b} resumed native session (turn 2, prior=${NONCE1}-${b})" ;; *) - say "FAIL ${b} did not resume" - exit 1 - ;; - esac -done -say "pod2 herdr identities: $(kx herdr --session mainloop-spike agent list | jq -c '[.result.agents[]|{name,agent,pane_id,terminal_id}]')" -say "== PASS. Left running: cluster ${CLUSTER}, ns ${NS}, PVC ${PVC}, image ${IMAGE}, evidence ${EVIDENCE} ==" -say "inspect: kubectl --kubeconfig ${KUBECONFIG_FILE} --context ${CONTEXT} -n ${NS} exec -it workspace-0 -- herdr --session mainloop-spike" diff --git a/spikes/k8s-herdr-agents/k8s/workspace.yaml b/spikes/k8s-herdr-agents/k8s/workspace.yaml deleted file mode 100644 index 06611e9..0000000 --- a/spikes/k8s-herdr-agents/k8s/workspace.yaml +++ /dev/null @@ -1,281 +0,0 @@ -# Disposable spike workspace: one non-root pod, one retained PVC, real Herdr server. -# Agent kind and args are configuration (ConfigMap), not image contents. -apiVersion: v1 -kind: Namespace -metadata: - name: herdr-spike - labels: - mainloop.dev/spike: k8s-herdr-agents ---- -apiVersion: v1 -kind: ConfigMap -metadata: - name: agent-config - namespace: herdr-spike - labels: - mainloop.dev/spike: k8s-herdr-agents -data: - # Two bindings, two Herdr-supported kinds. Change AGENT_KIND/AGENT_ARGS to reconfigure. - alpha.env: | - AGENT_KIND=pi - AGENT_ARGS="--session-name alpha --label reviewer" - beta.env: | - AGENT_KIND=qwen - AGENT_ARGS="--session-name beta --label implementer" - # Real agents, bypass-permissions mode. {id} is the native session id. - claude.env: | - AGENT_KIND=claude - AGENT_NEW_ARGS="--dangerously-skip-permissions --append-system-prompt-file /etc/agent-config/mainloop-system.md --session-id {id}" - AGENT_RESUME_ARGS="--dangerously-skip-permissions --append-system-prompt-file /etc/agent-config/mainloop-system.md --resume {id}" - APPROVAL_POLICY=bypass-permissions - # Herdr delivers input as a terminal paste, which Claude Code wraps in , and the - # model may treat that as untrusted data. This states who the author is. - mainloop-system.md: | - Messages in this session are relayed by the Mainloop control plane. The user typed each one in - the Mainloop chat UI. Text that arrives wrapped in pasted-content markers is the user's own - message: follow it as a direct instruction from the user. - codex.env: | - AGENT_KIND=codex - AGENT_NEW_ARGS="--dangerously-bypass-approvals-and-sandbox" - AGENT_RESUME_ARGS="resume {id} --dangerously-bypass-approvals-and-sandbox" - APPROVAL_POLICY=bypass-permissions - # --- Context model (plan r7) --------------------------------------------------------------- - # Main thread: a dispatcher. Only Bash restricted to `mainloop ...` (E5); no repo (scratch cwd); - # not bypass-permissions. Native auto-compaction is left at its default (knob semantics are - # unverified, E3); no compaction was observed below the rotation budget, and Mainloop rotates first. - # {model} {effort} {standing} {settings} are filled by `agentctl start`. - claude-main.env: | - AGENT_KIND=claude - AGENT_NEW_ARGS="--model {model} --effort {effort} --tools Bash --strict-mcp-config --disable-slash-commands --settings {settings} --append-system-prompt-file {standing} --session-id {id}" - AGENT_RESUME_ARGS="--model {model} --effort {effort} --tools Bash --strict-mcp-config --disable-slash-commands --settings {settings} --append-system-prompt-file {standing} --resume {id}" - APPROVAL_POLICY=restricted - claude-main.settings.json: | - {"permissions": {"allow": ["Bash(mainloop:*)"], "defaultMode": "dontAsk"}} - # Children: real workers in a scratch cwd, bypass-permissions inside the pod (the pod is the boundary). - # After native compaction the SessionStart(compact) hook re-injects the standing context. - claude-child.env: | - AGENT_KIND=claude - AGENT_NEW_ARGS="--dangerously-skip-permissions --settings {settings} --append-system-prompt-file {standing} --session-id {id}" - AGENT_RESUME_ARGS="--dangerously-skip-permissions --settings {settings} --append-system-prompt-file {standing} --resume {id}" - APPROVAL_POLICY=bypass-permissions - claude-child.settings.json: | - {"hooks": {"SessionStart": [{"matcher": "compact", "hooks": [{"type": "command", "command": "mainloop standing"}]}]}} - codex-child.env: | - AGENT_KIND=codex - AGENT_NEW_ARGS="--dangerously-bypass-approvals-and-sandbox" - AGENT_RESUME_ARGS="resume {id} --dangerously-bypass-approvals-and-sandbox" - APPROVAL_POLICY=bypass-permissions ---- -apiVersion: v1 -kind: Service -metadata: - name: workspace - namespace: herdr-spike - labels: - mainloop.dev/spike: k8s-herdr-agents -spec: - clusterIP: None - selector: - app: workspace - ports: - - name: none - port: 1 ---- -apiVersion: apps/v1 -kind: StatefulSet -metadata: - name: workspace - namespace: herdr-spike - labels: - mainloop.dev/spike: k8s-herdr-agents -spec: - serviceName: workspace - replicas: 1 - selector: - matchLabels: - app: workspace - persistentVolumeClaimRetentionPolicy: - whenDeleted: Retain - whenScaled: Retain - template: - metadata: - labels: - app: workspace - mainloop.dev/spike: k8s-herdr-agents - spec: - automountServiceAccountToken: false - securityContext: - runAsNonRoot: true - runAsUser: 10001 - runAsGroup: 10001 - fsGroup: 10001 - seccompProfile: - type: RuntimeDefault - containers: - - name: workspace - image: __IMAGE__ - imagePullPolicy: Never - env: - - { name: HOME, value: /workspace/.home } - - { name: WORKSPACE_PATH, value: /workspace/repo } - - { name: STANDIN_STATE_DIR, value: /workspace/.standin } - - { name: HERDR_SESSION, value: mainloop-spike } - - { name: AGENT_CONFIG_DIR, value: /etc/agent-config } - - { name: CODEX_HOME, value: /workspace/.codex } - - name: CLAUDE_CODE_OAUTH_TOKEN - valueFrom: - secretKeyRef: { name: claude-oauth, key: oauth-token, optional: true } - securityContext: - allowPrivilegeEscalation: false - readOnlyRootFilesystem: true - capabilities: - drop: [ALL] - resources: - requests: { cpu: 100m, memory: 256Mi } - limits: { cpu: '2', memory: 2Gi } - volumeMounts: - - { name: workspace, mountPath: /workspace } - - { name: tmp, mountPath: /tmp } - - { name: agent-config, mountPath: /etc/agent-config, readOnly: true } - - { name: codex-auth, mountPath: /etc/agent-secrets/codex, readOnly: true } - volumes: - - name: tmp - emptyDir: {} - - name: agent-config - configMap: - name: agent-config - - name: codex-auth - secret: { secretName: codex-auth, optional: true } - volumeClaimTemplates: - - metadata: - name: workspace - labels: - mainloop.dev/spike: k8s-herdr-agents - spec: - accessModes: [ReadWriteOnce] - resources: - requests: - storage: 1Gi ---- -# Main thread pod (owner decision 2): same image and shape as workspace-0, own PVC, no Codex auth. -# The conversation agent runs in a scratch cwd (/workspace/main) with no repository. -apiVersion: v1 -kind: Service -metadata: - name: main - namespace: herdr-spike - labels: - mainloop.dev/spike: k8s-herdr-agents -spec: - clusterIP: None - selector: - app: main - ports: - - name: none - port: 1 ---- -apiVersion: apps/v1 -kind: StatefulSet -metadata: - name: main - namespace: herdr-spike - labels: - mainloop.dev/spike: k8s-herdr-agents -spec: - serviceName: main - replicas: 1 - selector: - matchLabels: - app: main - persistentVolumeClaimRetentionPolicy: - whenDeleted: Retain - whenScaled: Retain - template: - metadata: - labels: - app: main - mainloop.dev/spike: k8s-herdr-agents - spec: - automountServiceAccountToken: false - securityContext: - runAsNonRoot: true - runAsUser: 10001 - runAsGroup: 10001 - fsGroup: 10001 - seccompProfile: - type: RuntimeDefault - containers: - - name: workspace - image: __IMAGE__ - imagePullPolicy: Never - env: - - { name: HOME, value: /workspace/.home } - - { name: WORKSPACE_PATH, value: /workspace/repo } - - { name: STANDIN_STATE_DIR, value: /workspace/.standin } - - { name: HERDR_SESSION, value: mainloop-main } - - { name: AGENT_CONFIG_DIR, value: /etc/agent-config } - - { name: CODEX_HOME, value: /workspace/.codex } - - name: CLAUDE_CODE_OAUTH_TOKEN - valueFrom: - secretKeyRef: { name: claude-oauth, key: oauth-token, optional: true } - securityContext: - allowPrivilegeEscalation: false - readOnlyRootFilesystem: true - capabilities: - drop: [ALL] - resources: - requests: { cpu: 100m, memory: 256Mi } - limits: { cpu: '1', memory: 1Gi } - volumeMounts: - - { name: workspace, mountPath: /workspace } - - { name: tmp, mountPath: /tmp } - - { name: agent-config, mountPath: /etc/agent-config, readOnly: true } - volumes: - - name: tmp - emptyDir: {} - - name: agent-config - configMap: - name: agent-config - volumeClaimTemplates: - - metadata: - name: workspace - labels: - mainloop.dev/spike: k8s-herdr-agents - spec: - accessModes: [ReadWriteOnce] - resources: - requests: - storage: 1Gi ---- -# Least-privilege exec access for the Mainloop backend (transport for the Herdr adapter): -# get/list the workspace pod and exec into it, in this namespace only. -apiVersion: rbac.authorization.k8s.io/v1 -kind: Role -metadata: - name: workspace-exec - namespace: herdr-spike - labels: - mainloop.dev/spike: k8s-herdr-agents -rules: - - apiGroups: [''] - resources: [pods] - verbs: [get, list] - - apiGroups: [''] - resources: [pods/exec] - verbs: [create, get] # the client's WebSocket exec is a GET upgrade ---- -apiVersion: rbac.authorization.k8s.io/v1 -kind: RoleBinding -metadata: - name: mainloop-backend-workspace-exec - namespace: herdr-spike - labels: - mainloop.dev/spike: k8s-herdr-agents -subjects: - - kind: ServiceAccount - name: mainloop-backend - namespace: mainloop -roleRef: - kind: Role - name: workspace-exec - apiGroup: rbac.authorization.k8s.io diff --git a/spikes/substrate-workspace-adapter/k8s/actor-template.yaml.tmpl b/spikes/substrate-workspace-adapter/k8s/actor-template.yaml.tmpl new file mode 100644 index 0000000..8aabb74 --- /dev/null +++ b/spikes/substrate-workspace-adapter/k8s/actor-template.yaml.tmpl @@ -0,0 +1,74 @@ +# WorkerPool (a real K8s CRD, `kubectl apply`) plus the Mainloop workspace ActorTemplate +# (protojson-shaped ateapipb.ActorTemplate, applied with `kubectl ate create actor-template -f -` +# after the __IMAGE__ placeholder is substituted with a digest-pinned reference -- Substrate +# requires containers.image to be pinned by digest). Mirrors the shape of the pinned checkout's +# demos/counter/{counter,counter-template}.yaml.tmpl. +# +# Substitute ${ATESPACE}, ${WORKER_POOL_NAME}, ${TEMPLATE_NAME}, ${BUCKET_NAME}, and __IMAGE__. +# The atespace ("mainloop-workspaces" by default; see backend/src/mainloop/config.py +# substrate_atespace) is both this WorkerPool's k8s namespace and a control-plane atespace +# resource created separately with `kubectl ate create atespace`. +apiVersion: v1 +kind: Namespace +metadata: + name: ${ATESPACE} +--- +apiVersion: ate.dev/v1alpha1 +kind: WorkerPool +metadata: + name: ${WORKER_POOL_NAME} + namespace: ${ATESPACE} + labels: + workload: ${WORKER_POOL_NAME} +spec: + replicas: 2 + workerImage: ko://github.com/agent-substrate/substrate/cmd/ateom-gvisor + template: + resources: + limits: + cpu: '2' + memory: 2Gi + requests: + cpu: 250m + memory: 2Gi +--- +# ActorTemplate: one headless per-turn native-agent container per actor, running under Substrate. +# Substrate owns lifecycle and snapshots; the actor retains workspace files between turns and +# has no Herdr server or terminal manager. Gate 5 uses this product template for its live check. +metadata: + atespace: ${ATESPACE} + name: ${TEMPLATE_NAME} +workerSelector: + matchLabels: + workload: ${WORKER_POOL_NAME} +containers: +- name: workspace + image: __IMAGE__ + command: + - /usr/local/bin/entrypoint.sh + env: + - { name: HOME, value: /work/.home } + - { name: MAINLOOP_API, value: http://mainloop-backend.mainloop-control.svc.cluster.local:8000 } + - { name: WORKSPACE_PATH, value: /work/repo } + - { name: AGENT_CONFIG_DIR, value: /etc/agent-config } + - { name: CODEX_HOME, value: /work/.codex } + - { name: EXEC_SHIM_STATE_DIR, value: /work/repo/.mainloop/exec-shim } + volumeMounts: + - { name: workspace, mountPath: /work } + resources: + limits: + - { name: cpu, quantity: "2" } + - { name: memory, quantity: 2Gi } + wakeupProbe: + httpGet: { path: /healthz, port: 8090 } + timeoutSeconds: 60 +snapshotConfig: + onPause: SNAPSHOT_CONTENT_SCOPE_FULL + onCommit: SNAPSHOT_CONTENT_SCOPE_FULL + storageLocation: gs://${BUCKET_NAME}/mainloop-workspaces/ +sandboxConfig: + sandboxClass: SANDBOX_CLASS_GVISOR + configName: gvisor-default +volumes: +- name: workspace + durableDir: {} diff --git a/spikes/substrate-workspace-adapter/k8s/postgres-target.yaml b/spikes/substrate-workspace-adapter/k8s/postgres-target.yaml new file mode 100644 index 0000000..f9f1557 --- /dev/null +++ b/spikes/substrate-workspace-adapter/k8s/postgres-target.yaml @@ -0,0 +1,68 @@ +# The "actual external PostgreSQL development service" gate 4 measures connectivity to, matching +# k8s/apps/mainloop/overlays/test/postgres-statefulset.yaml's image/auth shape (postgres:16-alpine, +# mainloop/mainloop/mainloop). Deployed as a plain K8s StatefulSet in its own namespace -- outside +# any atespace -- since it represents a real external service, not a Substrate actor. +apiVersion: v1 +kind: Namespace +metadata: + name: postgres-target +--- +apiVersion: v1 +kind: Service +metadata: + name: postgres + namespace: postgres-target +spec: + selector: + app: postgres + ports: + - port: 5432 + targetPort: 5432 +--- +apiVersion: apps/v1 +kind: StatefulSet +metadata: + name: postgres + namespace: postgres-target +spec: + serviceName: postgres + replicas: 1 + selector: + matchLabels: + app: postgres + template: + metadata: + labels: + app: postgres + spec: + containers: + - name: postgres + image: postgres:16-alpine + ports: + - containerPort: 5432 + env: + - name: POSTGRES_USER + value: mainloop + - name: POSTGRES_PASSWORD + value: mainloop + - name: POSTGRES_DB + value: mainloop + volumeMounts: + - name: postgres-data + mountPath: /var/lib/postgresql/data + resources: + requests: { memory: 256Mi, cpu: 100m } + limits: { memory: 512Mi, cpu: 500m } + readinessProbe: + exec: + command: [pg_isready, -U, mainloop] + initialDelaySeconds: 5 + periodSeconds: 5 + volumeClaimTemplates: + - metadata: + name: postgres-data + spec: + accessModes: [ReadWriteOnce] + resources: + requests: + storage: 1Gi diff --git a/spikes/substrate-workspace-adapter/k8s/preview-proxy.yaml.tmpl b/spikes/substrate-workspace-adapter/k8s/preview-proxy.yaml.tmpl new file mode 100644 index 0000000..9fca364 --- /dev/null +++ b/spikes/substrate-workspace-adapter/k8s/preview-proxy.yaml.tmpl @@ -0,0 +1,80 @@ +# Fixed authenticated preview route: an NGINX Deployment/Service that injects the +# ate-target-actor header atenet-router needs (browsers cannot set custom headers), following +# the same pattern as the pinned checkout's demos/jupyter/jupyter.yaml.tmpl proxy. This is the +# route intended for Mainloop's own preview UI (not a bespoke fixture endpoint): the browser +# never talks to atenet-router directly. +# +# proxy_http_version 1.1 is required for the WebSocket upgrade to survive proxy_pass -- NGINX +# defaults to HTTP/1.0 upstream, which silently breaks `Connection: Upgrade`. The Jupyter demo's +# own nginx.conf (copied as the starting point here) does not set it either; measured live +# against this cluster as a 503 "connection closed before message completed" from atenet-router, +# not from NGINX itself, which made it look like a router bug until isolated. See +# docs/spikes/substrate-workspace-adapter.md. +apiVersion: v1 +kind: ConfigMap +metadata: + name: preview-proxy-config + namespace: ${ATESPACE} +data: + nginx.conf: | + events {} + http { + map $http_upgrade $connection_upgrade { + default upgrade; + '' close; + } + server { + listen 80; + location / { + proxy_pass http://atenet-router.ate-system.svc.cluster.local; + proxy_http_version 1.1; + proxy_set_header Host $http_host; + proxy_set_header ate-target-actor ${ATESPACE}/${ACTOR_NAME}; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection $connection_upgrade; + } + } + } +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: preview-proxy + namespace: ${ATESPACE} +spec: + replicas: 1 + selector: + matchLabels: + app: preview-proxy + template: + metadata: + labels: + app: preview-proxy + spec: + containers: + - name: nginx + image: nginx:alpine + ports: + - containerPort: 80 + volumeMounts: + - name: config + mountPath: /etc/nginx/nginx.conf + subPath: nginx.conf + volumes: + - name: config + configMap: + name: preview-proxy-config +--- +apiVersion: v1 +kind: Service +metadata: + name: preview-proxy + namespace: ${ATESPACE} +spec: + type: ClusterIP + selector: + app: preview-proxy + ports: + - port: 80 diff --git a/spikes/substrate-workspace-adapter/k8s/router-ingress-policy.yaml b/spikes/substrate-workspace-adapter/k8s/router-ingress-policy.yaml new file mode 100644 index 0000000..0a18299 --- /dev/null +++ b/spikes/substrate-workspace-adapter/k8s/router-ingress-policy.yaml @@ -0,0 +1,45 @@ +# Mainloop-owned ingress boundary for the Substrate router. NetworkPolicy ports +# are pod ports: Service port 80 maps to the router's HTTP listener on 8080. +# The agentgateway router forwards requests to api.ate-system.svc:443 itself; +# actor/provider egress goes through atenet-egress, so neither ate-system worker +# pods nor the egress gateway are router ingress clients in this install. +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: atenet-router-mainloop-ingress + namespace: ate-system +spec: + podSelector: + matchLabels: + app: atenet-router + policyTypes: + - Ingress + ingress: + # Trusted Mainloop backend. It uses HTTP plus the Substrate CONNECT listener + # for actor control and arbitrary-port workspace traffic. + - from: + - namespaceSelector: + matchLabels: + mainloop.dev/role: control + ports: + - { protocol: TCP, port: 8080 } + - { protocol: TCP, port: 8081 } + - { protocol: TCP, port: 8443 } + - { protocol: TCP, port: 8444 } + # The fixed NGINX preview proxy injects ate-target-actor and only needs HTTP. + - from: + - namespaceSelector: + matchLabels: + mainloop.dev/role: workspace + podSelector: + matchLabels: + app: preview-proxy + ports: + - { protocol: TCP, port: 8080 } + # The installer annotates the router for Prometheus scraping on 15020. + - from: + - namespaceSelector: + matchLabels: + kubernetes.io/metadata.name: otel-system + ports: + - { protocol: TCP, port: 15020 } diff --git a/spikes/substrate-workspace-adapter/live-agent-image/.gitignore b/spikes/substrate-workspace-adapter/live-agent-image/.gitignore new file mode 100644 index 0000000..64443b6 --- /dev/null +++ b/spikes/substrate-workspace-adapter/live-agent-image/.gitignore @@ -0,0 +1,6 @@ +/claude +/codex +/codex-code-mode-host +claude +codex +codex-code-mode-host diff --git a/spikes/substrate-workspace-adapter/live-agent-image/Dockerfile b/spikes/substrate-workspace-adapter/live-agent-image/Dockerfile new file mode 100644 index 0000000..0570c32 --- /dev/null +++ b/spikes/substrate-workspace-adapter/live-agent-image/Dockerfile @@ -0,0 +1,46 @@ +# syntax=docker/dockerfile:1.7 +# Headless Substrate actor image: Substrate owns lifecycle; the shim starts one native CLI +# process per delivered turn. Claude/Codex binaries are supplied in the local build context. +# No credentials are baked in or fetched during startup; the golden actor reaches readiness +# without credentials or external network access. +FROM node:22-bookworm-slim +RUN apt-get update \ + && apt-get install -y --no-install-recommends \ + jq ca-certificates git curl procps ripgrep \ + && rm -rf /var/lib/apt/lists/* \ + && useradd -m -u 10001 agent +# The run-specific Substrate MITM CA is a public trust anchor. BuildKit mounts +# it without retaining the input file or putting the PEM in the build command. +ARG EGRESS_MITM_CA_SHA256="" +RUN --mount=type=secret,id=egress-mitm-ca,target=/run/secrets/egress-mitm-ca,required=false \ + if [ -s /run/secrets/egress-mitm-ca ]; then \ + test -n "${EGRESS_MITM_CA_SHA256}" && \ + test "$(sha256sum /run/secrets/egress-mitm-ca | cut -d' ' -f1)" = "${EGRESS_MITM_CA_SHA256}" && \ + install -m 0644 /run/secrets/egress-mitm-ca /usr/local/share/ca-certificates/substrate-egress-mitm.crt && \ + update-ca-certificates; \ + fi +COPY claude /usr/local/bin/claude +COPY codex /usr/local/bin/codex +COPY codex-code-mode-host /usr/local/bin/codex-code-mode-host +COPY bin/start-native-agent /usr/local/bin/start-native-agent +COPY bin/prepare-native-agent-config.cjs /usr/local/bin/prepare-native-agent-config.cjs +COPY bin/mainloop /usr/local/bin/mainloop +COPY bin/mainloop-reauth /usr/local/bin/mainloop-reauth +COPY agent-config/mainloop-system.txt /etc/agent-config/mainloop-system.txt +COPY exec-shim.js /usr/local/bin/exec-shim.js +COPY entrypoint.sh /usr/local/bin/entrypoint.sh +RUN chmod +x /usr/local/bin/entrypoint.sh /usr/local/bin/start-native-agent /usr/local/bin/mainloop-reauth \ + && mkdir -p /work && chown 10001:10001 /work +WORKDIR /work +ENV EXEC_SHIM=/usr/local/bin/exec-shim.js +ENV NATIVE_AGENT_LAUNCHER=/usr/local/bin/start-native-agent +ENV HOME=/work/.home +ENV WORKSPACE_PATH=/work/repo +ENV CODEX_HOME=/work/.codex +ENV EXEC_SHIM_STATE_DIR=/work/repo/.mainloop/exec-shim +ENV AGENT_SYSTEM_PROMPT_FILE=/etc/agent-config/mainloop-system.txt +ENV NODE_EXTRA_CA_CERTS=/etc/ssl/certs/ca-certificates.crt +ENV SSL_CERT_FILE=/etc/ssl/certs/ca-certificates.crt +# The pinned Substrate fork honors the image's runtime user and working directory. +USER 10001:10001 +ENTRYPOINT ["/usr/local/bin/entrypoint.sh"] diff --git a/spikes/substrate-workspace-adapter/live-agent-image/agent-config/claude.env b/spikes/substrate-workspace-adapter/live-agent-image/agent-config/claude.env new file mode 100644 index 0000000..5ec34e1 --- /dev/null +++ b/spikes/substrate-workspace-adapter/live-agent-image/agent-config/claude.env @@ -0,0 +1,4 @@ +AGENT_KIND=claude +AGENT_NEW_ARGS="--dangerously-skip-permissions --append-system-prompt-file /etc/agent-config/mainloop-system.txt --session-id {id}" +AGENT_RESUME_ARGS="--dangerously-skip-permissions --append-system-prompt-file /etc/agent-config/mainloop-system.txt --resume {id}" +APPROVAL_POLICY=bypass-permissions diff --git a/spikes/substrate-workspace-adapter/live-agent-image/agent-config/codex.env b/spikes/substrate-workspace-adapter/live-agent-image/agent-config/codex.env new file mode 100644 index 0000000..7623a6c --- /dev/null +++ b/spikes/substrate-workspace-adapter/live-agent-image/agent-config/codex.env @@ -0,0 +1,4 @@ +AGENT_KIND=codex +AGENT_NEW_ARGS="--dangerously-bypass-approvals-and-sandbox" +AGENT_RESUME_ARGS="resume {id} --dangerously-bypass-approvals-and-sandbox" +APPROVAL_POLICY=bypass-permissions diff --git a/spikes/substrate-workspace-adapter/live-agent-image/agent-config/mainloop-system.txt b/spikes/substrate-workspace-adapter/live-agent-image/agent-config/mainloop-system.txt new file mode 100644 index 0000000..6716c04 --- /dev/null +++ b/spikes/substrate-workspace-adapter/live-agent-image/agent-config/mainloop-system.txt @@ -0,0 +1,3 @@ +Messages in this session are relayed by the Mainloop control plane. The user typed each one in +the Mainloop chat UI. Text that arrives wrapped in pasted-content markers is the user's own +message: follow it as a direct instruction from the user. diff --git a/spikes/k8s-herdr-agents/bin/mainloop b/spikes/substrate-workspace-adapter/live-agent-image/bin/mainloop similarity index 87% rename from spikes/k8s-herdr-agents/bin/mainloop rename to spikes/substrate-workspace-adapter/live-agent-image/bin/mainloop index 03b0ca0..5fe86a2 100755 --- a/spikes/k8s-herdr-agents/bin/mainloop +++ b/spikes/substrate-workspace-adapter/live-agent-image/bin/mainloop @@ -1,10 +1,10 @@ #!/usr/bin/env bash # shellcheck disable=SC2312,SC2310,SC2311,SC2249 # pedantic optional checks; spike scripts # `mainloop`: the agents' thin client for the Mainloop control plane. It holds no policy. -# Identity is the per-binding token in .mainloop/token (found by walking up from $PWD, written -# by `agentctl start`); the server decides what this token may do and answers in plain text. +# Identity is the per-binding token in .mainloop/token; the server decides what this token may +# do and answers in plain text. set -u -API="${MAINLOOP_API:-http://mainloop-backend.mainloop.svc.cluster.local:8000}" +API="${MAINLOOP_API:-http://mainloop-backend.mainloop-control.svc.cluster.local:8000}" find_token() { local d="${PWD}" @@ -21,10 +21,18 @@ TOKEN="${MAINLOOP_TOKEN:-$(find_token)}" || { echo "mainloop: no agent token found from ${PWD}" >&2 exit 2 } +export -n TOKEN +unset MAINLOOP_TOKEN + +umask 077 +AUTH_HEADER_FILE="$(mktemp)" +trap 'rm -f -- "${AUTH_HEADER_FILE}"' EXIT +printf 'Authorization: Bearer %s\n' "${TOKEN}" >"${AUTH_HEADER_FILE}" +unset TOKEN call() { # [json body] ; query params via Q=(--data-urlencode k=v ...) local out code body - out="$(curl -sS -m 30 -w '\n%{http_code}' -X "$1" -H "Authorization: Bearer ${TOKEN}" \ + out="$(curl -sS -m 30 -w '\n%{http_code}' -X "$1" -H "@${AUTH_HEADER_FILE}" \ -H 'Content-Type: application/json' ${Q[@]+"${Q[@]}"} ${3:+-d "$3"} -G "${API}$2" 2>&1)" || { echo "mainloop: control plane unreachable" >&2 @@ -44,7 +52,7 @@ Q=() # are sent with --json-style separately: post() { # local out code body - out="$(curl -sS -m 30 -w '\n%{http_code}' -X POST -H "Authorization: Bearer ${TOKEN}" \ + out="$(curl -sS -m 30 -w '\n%{http_code}' -X POST -H "@${AUTH_HEADER_FILE}" \ -H 'Content-Type: application/json' --data-binary "$2" "${API}$1" 2>&1)" || { echo "mainloop: control plane unreachable" >&2 diff --git a/spikes/substrate-workspace-adapter/live-agent-image/bin/mainloop-reauth b/spikes/substrate-workspace-adapter/live-agent-image/bin/mainloop-reauth new file mode 100644 index 0000000..84c7e4e --- /dev/null +++ b/spikes/substrate-workspace-adapter/live-agent-image/bin/mainloop-reauth @@ -0,0 +1,92 @@ +#!/usr/bin/env node +// Isolated control-side sign-in helper. Provider output is captured in memory and never logged. +const fs = require('node:fs'); +const os = require('node:os'); +const path = require('node:path'); +const { spawn } = require('node:child_process'); + +const [provider, jobId] = process.argv.slice(2); +const callbackUrl = process.env.MAINLOOP_REAUTH_CALLBACK_URL; +const callbackToken = process.env.MAINLOOP_REAUTH_CALLBACK_TOKEN; +const timeoutMs = Math.max(60, Number(process.env.MAINLOOP_REAUTH_TIMEOUT_SECONDS || 1800)) * 1000; +const allowedHosts = new Set(['auth.openai.com', 'claude.ai', 'console.anthropic.com']); +let captured = ''; +let sentChallenge = ''; + +function addOutput(chunk) { + if (captured.length < 256 * 1024) captured += chunk.toString('utf8').slice(0, 256 * 1024 - captured.length); + const clean = captured.replace(/\u001b\[[0-?]*[ -/]*[@-~]/g, ''); + const urlMatch = clean.match(/https:\/\/[A-Za-z0-9.-]+(?:\/[A-Za-z0-9._~:/?#\[\]@!$&'()*+,;=%-]*)?/g) || []; + const url = urlMatch.map((candidate) => { + try { + const parsed = new URL(candidate); + return allowedHosts.has(parsed.hostname) ? parsed.href : null; + } catch { + return null; + } + }).find(Boolean); + const codeMatch = clean.match(/(?:code|enter|device)\s*[:=]?\s*([A-Z0-9]{4,8}-[A-Z0-9]{4,8})/i); + if (!url || !codeMatch) return; + const code = codeMatch[1].toUpperCase(); + const challenge = JSON.stringify({ url, code }); + if (challenge !== sentChallenge) { + sentChallenge = challenge; + process.stdout.write(`MAINLOOP_REAUTH_CHALLENGE ${challenge}\n`); + } +} + +function fail() { + process.stderr.write('credential re-auth failed\n'); + process.exit(1); +} + +async function postCredential(body) { + const response = await fetch(`${callbackUrl}/${encodeURIComponent(jobId)}`, { + method: 'POST', + headers: { + 'content-type': 'application/json', + 'x-mainloop-reauth-token': callbackToken + }, + body: JSON.stringify(body), + signal: AbortSignal.timeout(15000) + }); + if (!response.ok) throw new Error('callback rejected'); +} + +async function main() { + if (!['codex', 'claude'].includes(provider) || !/^reauth-(codex|claude)-[a-f0-9]{12}$/.test(jobId) || !callbackUrl || !callbackToken) fail(); + const root = fs.mkdtempSync(path.join(os.tmpdir(), 'mainloop-reauth-')); + const home = path.join(root, 'home'); + const codexHome = path.join(home, '.codex'); + const claudeHome = path.join(home, '.claude'); + fs.mkdirSync(codexHome, { recursive: true, mode: 0o700 }); + fs.mkdirSync(claudeHome, { recursive: true, mode: 0o700 }); + const command = provider === 'codex' ? '/usr/local/bin/codex' : '/usr/local/bin/claude'; + const args = provider === 'codex' ? ['login', '--device-auth'] : ['setup-token']; + const child = spawn(command, args, { + cwd: root, + env: { ...process.env, HOME: home, CODEX_HOME: codexHome, CLAUDE_CONFIG_DIR: claudeHome }, + stdio: ['ignore', 'pipe', 'pipe'] + }); + child.stdout.on('data', addOutput); + child.stderr.on('data', addOutput); + const timer = setTimeout(() => child.kill('SIGTERM'), timeoutMs); + const exitCode = await new Promise((resolve, reject) => { + child.once('error', reject); + child.once('close', (code) => resolve(code)); + }).finally(() => clearTimeout(timer)); + if (exitCode !== 0) fail(); + + if (provider === 'codex') { + const auth = JSON.parse(fs.readFileSync(path.join(codexHome, 'auth.json'), 'utf8')); + await postCredential({ provider, auth }); + } else { + const match = captured.match(/\bsk-ant-oat[0-9A-Za-z_-]{16,}\b/); + if (!match) fail(); + await postCredential({ provider, token: match[0] }); + } + process.stdout.write('credential re-auth completed\n'); + fs.rmSync(root, { recursive: true, force: true }); +} + +main().catch(fail); diff --git a/spikes/substrate-workspace-adapter/live-agent-image/bin/prepare-native-agent-config.cjs b/spikes/substrate-workspace-adapter/live-agent-image/bin/prepare-native-agent-config.cjs new file mode 100644 index 0000000..e2de1bc --- /dev/null +++ b/spikes/substrate-workspace-adapter/live-agent-image/bin/prepare-native-agent-config.cjs @@ -0,0 +1,60 @@ +// Seed stable first-run defaults for the headless per-turn Claude and Codex CLIs. +'use strict'; + +const fs = require('node:fs'); +const path = require('node:path'); + +const home = process.env.HOME || '/home/agent'; +const workspace = process.env.WORKSPACE_PATH || '/work/repo'; +const codexHome = process.env.CODEX_HOME || path.join(home, '.codex'); +const claudeConfig = path.join(home, '.claude.json'); +const claudeSettingsDir = path.join(home, '.claude'); +const claudeSettings = path.join(claudeSettingsDir, 'settings.json'); +const codexConfig = path.join(codexHome, 'config.toml'); + +fs.mkdirSync(claudeSettingsDir, { recursive: true, mode: 0o700 }); +fs.mkdirSync(codexHome, { recursive: true, mode: 0o700 }); + +if (!fs.existsSync(claudeConfig) || fs.statSync(claudeConfig).size === 0) { + fs.writeFileSync( + claudeConfig, + `${JSON.stringify( + { + hasCompletedOnboarding: true, + numStartups: 1, + theme: 'dark', + projects: { + [workspace]: { + hasTrustDialogAccepted: true, + hasCompletedProjectOnboarding: true, + allowedTools: [] + } + } + }, + null, + 2 + )}\n`, + { mode: 0o600 } + ); +} +if (!fs.existsSync(claudeSettings) || fs.statSync(claudeSettings).size === 0) { + fs.writeFileSync(claudeSettings, '{"skipDangerousModePermissionPrompt":true}\n', { mode: 0o600 }); +} + +if (!fs.existsSync(codexConfig) || fs.statSync(codexConfig).size === 0) { + const quotedWorkspace = JSON.stringify(workspace); + const defaults = [ + 'check_for_update_on_startup = false', + '', + '[tui]', + 'theme = "dark"', + '', + `[projects.${quotedWorkspace}]`, + 'trust_level = "trusted"', + '', + '[notice]', + 'hide_rate_limit_model_nudge = true', + '' + ].join('\n'); + fs.writeFileSync(codexConfig, defaults, { mode: 0o600 }); +} diff --git a/spikes/substrate-workspace-adapter/live-agent-image/bin/start-native-agent b/spikes/substrate-workspace-adapter/live-agent-image/bin/start-native-agent new file mode 100755 index 0000000..3b58a77 --- /dev/null +++ b/spikes/substrate-workspace-adapter/live-agent-image/bin/start-native-agent @@ -0,0 +1,187 @@ +#!/usr/bin/env bash +# Run one headless native CLI turn. The prompt is inherited on stdin, never passed in argv. +set -euo pipefail + +kind="${1:?usage: start-native-agent claude|codex create|resume [native-session-id]}" +mode="${2:?usage: start-native-agent claude|codex create|resume [native-session-id]}" +if [[ $# -gt 3 ]]; then + echo 'usage: start-native-agent claude|codex create|resume [native-session-id]' >&2 + exit 2 +fi +if [[ ${mode} != create && ${mode} != resume ]]; then + echo 'invalid session mode' >&2 + exit 2 +fi +session_id="${3-}" +if [[ -n ${session_id} && ! ${session_id} =~ ^[A-Za-z0-9._:-]{1,256}$ ]]; then + echo 'invalid native session id' >&2 + exit 2 +fi +if [[ ${mode} == resume && -z ${session_id} ]]; then + echo 'resume requires an established native session id' >&2 + exit 2 +fi + +workspace="${WORKSPACE_PATH:-/work/repo}" +cd "${workspace}" +workspace_root="$(pwd -P)" + +# Keep telemetry and automatic network chatter disabled for every native invocation. +export DISABLE_TELEMETRY=1 +export DISABLE_ERROR_REPORTING=1 +export CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=1 +export DISABLE_AUTOUPDATER=1 + +startup_context_file="" +startup_standing_file="" +startup_standing_text="" +if [[ ${MAINLOOP_STARTUP_PRESENT-} == 1 ]]; then + startup_cwd="${MAINLOOP_STARTUP_CWD_REL-}" + startup_token="${MAINLOOP_STARTUP_TOKEN-}" + startup_standing_b64="${MAINLOOP_STARTUP_STANDING_B64-}" + startup_policy="${MAINLOOP_STARTUP_APPROVAL_POLICY-}" + startup_model="${MAINLOOP_STARTUP_MODEL-}" + startup_effort="${MAINLOOP_STARTUP_EFFORT-}" + if [[ ! ${startup_token} =~ ^ml_[a-f0-9]{64}$ ]] || + [[ ! ${startup_standing_b64} =~ ^([A-Za-z0-9+/]{4})*([A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$ ]]; then + echo 'invalid native startup credentials or context' >&2 + exit 2 + fi + if [[ ${kind} == claude && ${startup_cwd} == main ]]; then + [[ ${startup_policy} == 'restricted: Bash(mainloop:*) only' ]] || { + echo 'invalid main-thread approval policy' >&2 + exit 2 + } + [[ ${startup_model} =~ ^[A-Za-z0-9._:-]{1,128}$ ]] || { + echo 'invalid main-thread model' >&2 + exit 2 + } + [[ ${startup_effort} == low || ${startup_effort} == medium || ${startup_effort} == high || ${startup_effort} == max || ${startup_effort} == xhigh ]] || { + echo 'invalid main-thread effort' >&2 + exit 2 + } + elif [[ ${startup_cwd} =~ ^children/ml-${kind}-[0-9a-f]{8}$ ]]; then + [[ ${startup_policy} == bypass-permissions && -z ${startup_model} && -z ${startup_effort} ]] || { + echo 'invalid child startup policy' >&2 + exit 2 + } + else + echo 'invalid native startup working directory' >&2 + exit 2 + fi + + startup_target="${workspace_root}/${startup_cwd}" + mkdir -p -- "${startup_target}" + startup_target="$(realpath -e -- "${startup_target}")" + case "${startup_target}" in + "${workspace_root}"/*) ;; + *) + echo 'native startup working directory escapes the workspace' >&2 + exit 2 + ;; + esac + cd -- "${startup_target}" + export MAINLOOP_TOKEN="${startup_token}" + + mkdir -p -- "${HOME}/.mainloop" + chmod 700 "${HOME}/.mainloop" + startup_standing_file="$(mktemp "${HOME}/.mainloop/standing.XXXXXX")" + chmod 600 "${startup_standing_file}" + if ! printf '%s' "${startup_standing_b64}" | base64 --decode >"${startup_standing_file}"; then + echo 'invalid native startup context encoding' >&2 + exit 2 + fi + startup_standing_size="$(wc -c <"${startup_standing_file}")" + if [[ ! -s ${startup_standing_file} ]] || [[ ${startup_standing_size} -gt 32768 ]]; then + echo 'invalid native startup context size' >&2 + exit 2 + fi + startup_standing_text="$(cat "${startup_standing_file}")" + trap 'rm -f -- "${startup_standing_file}" "${startup_context_file}"' EXIT +fi + +run_cli() { + local status=0 + if "$@"; then + status=0 + else + status=$? + fi + if [[ -n ${startup_standing_file} ]]; then + rm -f -- "${startup_standing_file}" "${startup_context_file}" + fi + exit "${status}" +} + +case "${kind}" in +claude) + token_file="${HOME}/.mainloop/claude-token" + if [[ ! -s ${token_file} ]]; then + echo 'Claude credential is not installed' >&2 + exit 1 + fi + CLAUDE_CODE_OAUTH_TOKEN="$(tr -d ' \r\n' <"${token_file}")" + if [[ -z ${CLAUDE_CODE_OAUTH_TOKEN} ]]; then + echo 'Claude credential is empty' >&2 + exit 1 + fi + export CLAUDE_CODE_OAUTH_TOKEN + args=(-p) + if [[ ${mode} == resume ]]; then + args+=(--resume "${session_id}") + else + if [[ -z ${session_id} ]]; then + echo 'new Claude sessions require a native session id' >&2 + exit 2 + fi + args+=(--session-id "${session_id}") + fi + args+=(--output-format stream-json --verbose) + if [[ ${MAINLOOP_STARTUP_PRESENT-} == 1 && ${startup_cwd} == main ]]; then + args+=(--tools Bash --allowedTools 'Bash(mainloop:*)' --permission-mode dontAsk + --model "${startup_model}" --effort "${startup_effort}") + else + args+=(--dangerously-skip-permissions) + fi + if [[ ${MAINLOOP_STARTUP_PRESENT-} == 1 ]]; then + startup_context_file="$(mktemp "${HOME}/.mainloop/system-prompt.XXXXXX")" + chmod 600 "${startup_context_file}" + cat "${AGENT_SYSTEM_PROMPT_FILE:-/etc/agent-config/mainloop-system.txt}" >"${startup_context_file}" + printf '\n' >>"${startup_context_file}" + cat "${startup_standing_file}" >>"${startup_context_file}" + args+=(--append-system-prompt-file "${startup_context_file}") + else + args+=(--append-system-prompt-file "${AGENT_SYSTEM_PROMPT_FILE:-/etc/agent-config/mainloop-system.txt}") + fi + if [[ ${MAINLOOP_STARTUP_PRESENT-} == 1 ]]; then + run_cli claude "${args[@]}" + fi + exec claude "${args[@]}" + ;; +codex) + auth_file="${CODEX_HOME:-${HOME}/.codex}/auth.json" + if [[ ! -s ${auth_file} ]]; then + echo 'Codex credential is not installed' >&2 + exit 1 + fi + if [[ ${MAINLOOP_STARTUP_PRESENT-} == 1 ]]; then + instructions_json="$(node -e 'process.stdout.write(JSON.stringify(process.argv[1]))' "${startup_standing_text}")" + codex_args=(--config "developer_instructions=${instructions_json}") + else + codex_args=() + fi + if [[ ${mode} == resume ]]; then + codex_args+=(exec resume --json "${session_id}" --dangerously-bypass-approvals-and-sandbox) + else + codex_args+=(exec --json --dangerously-bypass-approvals-and-sandbox) + fi + if [[ ${MAINLOOP_STARTUP_PRESENT-} == 1 ]]; then + run_cli codex "${codex_args[@]}" + fi + exec codex "${codex_args[@]}" + ;; +*) + echo 'unsupported native CLI' >&2 + exit 2 + ;; +esac diff --git a/spikes/substrate-workspace-adapter/live-agent-image/entrypoint.sh b/spikes/substrate-workspace-adapter/live-agent-image/entrypoint.sh new file mode 100644 index 0000000..35ec798 --- /dev/null +++ b/spikes/substrate-workspace-adapter/live-agent-image/entrypoint.sh @@ -0,0 +1,53 @@ +#!/usr/bin/env bash +# Start only the actor-local shim. Native CLIs run headlessly once per delivered turn. +set -eu + +CURRENT_UID="$(id -u)" +export HOME="${HOME:-/work/.home}" +WORKSPACE_PATH="${WORKSPACE_PATH:-/work/repo}" +EXEC_SHIM_STATE_DIR="${EXEC_SHIM_STATE_DIR:-${WORKSPACE_PATH}/.mainloop/exec-shim}" +export WORKSPACE_PATH EXEC_SHIM_STATE_DIR + +if [[ ${CURRENT_UID} -eq 0 ]]; then + echo 'entrypoint refused to continue as UID 0' >&2 + exit 1 +fi + +if ! mkdir -p "${WORKSPACE_PATH}" || [[ ! -w ${WORKSPACE_PATH} ]]; then + printf 'entrypoint requires a writable workspace directory for UID %s: %s\n' \ + "${CURRENT_UID}" "${WORKSPACE_PATH}" >&2 + exit 1 +fi + +node /usr/local/bin/prepare-native-agent-config.cjs +[[ -d "${WORKSPACE_PATH}/.git" ]] || git -C "${WORKSPACE_PATH}" init -q + +# Credential-free marker retained for the bounded Gate 5 restore/suspend-resume probe. +[[ -f "${WORKSPACE_PATH}/gate5-counter" ]] || echo 0 >"${WORKSPACE_PATH}/gate5-counter" + +node "${EXEC_SHIM}" & +SHIM_PID=$! +trap 'kill "${SHIM_PID}" 2>/dev/null || true' EXIT INT TERM + +ready=0 +for _ in $(seq 1 60); do + if curl --fail --silent http://127.0.0.1:"${EXEC_SHIM_PORT:-8090}"/healthz >/dev/null; then + ready=1 + break + fi + if ! kill -0 "${SHIM_PID}" 2>/dev/null; then + wait "${SHIM_PID}" || true + echo 'CONTROL_SERVICE_READINESS_FAILED: shim exited before healthz passed' >&2 + exit 1 + fi + sleep 0.5 +done +if [[ ${ready} -ne 1 ]]; then + echo 'CONTROL_SERVICE_READINESS_TIMEOUT: shim/workspace not ready within 30s' >&2 + kill "${SHIM_PID}" 2>/dev/null || true + wait "${SHIM_PID}" || true + exit 1 +fi + +echo "CONTROL_SERVICE_READY workspace=${WORKSPACE_PATH} port=${EXEC_SHIM_PORT:-8090}" +wait "${SHIM_PID}" diff --git a/spikes/substrate-workspace-adapter/live-agent-image/exec-shim.js b/spikes/substrate-workspace-adapter/live-agent-image/exec-shim.js new file mode 100644 index 0000000..cc4ff92 --- /dev/null +++ b/spikes/substrate-workspace-adapter/live-agent-image/exec-shim.js @@ -0,0 +1,1196 @@ +// Authenticated, headless turn and command service for the Substrate actor. +// Prompts are piped to the native CLI on stdin and are never logged or persisted. +'use strict'; +const http = require('node:http'); +const { spawn } = require('node:child_process'); +const crypto = require('node:crypto'); +const fs = require('node:fs'); +const path = require('node:path'); + +const MAX_REQUEST_BODY_BYTES = 64 * 1024; +const MAX_TURN_REQUEST_BODY_BYTES = 128 * 1024; +const MAX_CREDENTIAL_REQUEST_BODY_BYTES = 512 * 1024; +const MAX_CREDENTIAL_BYTES = 64 * 1024; +const MAX_JOB_OUTPUT_BYTES = 2 * 1024 * 1024; +const MAX_RESPONSE_OUTPUT_BYTES = 32 * 1024; +const MAX_RETURN_EVENTS = 256; +const MAX_JOURNAL_LINES = 200; +const MAX_JOURNAL_LINE_BYTES = 1024 * 1024; +const MAX_JOURNAL_RESPONSE_BYTES = 2 * 1024 * 1024; +const MAX_JOURNAL_SEARCH_ENTRIES = 100_000; +const MAX_LONG_PROCESSES = 32; +const MAX_LONG_PROCESS_LOG_BYTES = 64 * 1024; +const MAX_LONG_PROCESS_RECORDS = 256; +const DEFAULT_RUN_TIMEOUT_MS = 60_000; +const DEFAULT_TURN_TIMEOUT_MS = 10 * 60_000; +const MAX_TIMEOUT_MS = 10 * 60_000; +const WORKSPACE_PATH = path.resolve(process.env.WORKSPACE_PATH || '/work/repo'); +const HOME_PATH = path.resolve(process.env.HOME || '/home/agent'); +const CODEX_HOME_PATH = path.resolve(process.env.CODEX_HOME || path.join(HOME_PATH, '.codex')); +const STATE_DIR = path.resolve( + process.env.EXEC_SHIM_STATE_DIR || path.join(WORKSPACE_PATH, '.mainloop') +); +const PROC_NET_DIR = path.resolve(process.env.EXEC_SHIM_PROC_NET_DIR || '/proc/net'); +const RUN_DIR = path.join(STATE_DIR, 'runs'); +const TURN_DIR = path.join(STATE_DIR, 'turns'); +const LAUNCHER = process.env.NATIVE_AGENT_LAUNCHER || '/usr/local/bin/start-native-agent'; + +if ( + typeof process.getuid === 'function' && + process.getuid() === 0 && + process.env.EXEC_SHIM_TEST_ALLOW_ROOT !== '1' +) { + process.stderr.write('exec-shim refuses to start as UID 0\n'); + process.exit(1); +} + +for (const directory of [STATE_DIR, RUN_DIR, TURN_DIR]) { + fs.mkdirSync(directory, { recursive: true, mode: 0o700 }); + fs.chmodSync(directory, 0o700); +} + +const tokenPath = path.join(HOME_PATH, '.mainloop', 'exec-shim-token'); +const credentialPaths = new Map([ + ['claude-token', path.join(HOME_PATH, '.mainloop', 'claude-token')], + ['codex-auth', path.join(CODEX_HOME_PATH, 'auth.json')] +]); +let bearerToken = null; +try { + bearerToken = fs.readFileSync(tokenPath, 'utf8'); +} catch (err) { + if (err.code !== 'ENOENT') throw err; +} + +const jobs = new Map(); +const longProcesses = new Map(); +const activeTurns = new Map(); +const latestTurns = new Map(); +const turnProcesses = new Map(); + +function json(res, status, document) { + res.writeHead(status, { 'content-type': 'application/json' }).end(JSON.stringify(document)); +} + +function authorized(req) { + if (bearerToken === null) return false; + const header = req.headers.authorization; + if (typeof header !== 'string' || !header.startsWith('Bearer ')) return false; + const provided = Buffer.from(header.slice('Bearer '.length)); + const expected = Buffer.from(bearerToken); + return provided.length === expected.length && crypto.timingSafeEqual(provided, expected); +} + +function unauthorized(res) { + res.writeHead(401, { 'content-type': 'text/plain' }).end('unauthorized'); +} + +function requestBody(req, res, onBody, maxBytes = MAX_REQUEST_BODY_BYTES) { + const chunks = []; + let size = 0; + let tooLarge = false; + req.on('data', (chunk) => { + size += chunk.length; + if (size > maxBytes) { + if (!tooLarge) res.writeHead(413).end('request too large'); + tooLarge = true; + return; + } + if (!tooLarge) chunks.push(chunk); + }); + req.on('end', () => { + if (!tooLarge && !res.destroyed) onBody(Buffer.concat(chunks).toString('utf8')); + }); +} + +function installToken(token) { + if (bearerToken !== null) return false; + if (typeof token !== 'string' || token.length < 32 || token.length > 4096) return null; + const directory = path.dirname(tokenPath); + fs.mkdirSync(directory, { recursive: true, mode: 0o700 }); + fs.chmodSync(directory, 0o700); + const fd = fs.openSync(tokenPath, 'wx', 0o600); + try { + fs.writeFileSync(fd, token, 'utf8'); + fs.fsyncSync(fd); + } catch (err) { + fs.closeSync(fd); + fs.rmSync(tokenPath, { force: true }); + throw err; + } + fs.closeSync(fd); + bearerToken = token; + return true; +} + +function installCredential(name, contents, replace = false) { + const destination = credentialPaths.get(name); + if (!destination || (replace && !['codex-auth', 'claude-token'].includes(name))) return false; + if ( + typeof contents !== 'string' || + !contents || + Buffer.byteLength(contents, 'utf8') > MAX_CREDENTIAL_BYTES + ) + return null; + if (!isPlaceholderCredential(name, contents)) { + return null; + } + + const directory = path.dirname(destination); + fs.mkdirSync(directory, { recursive: true, mode: 0o700 }); + const directoryStat = fs.lstatSync(directory); + if (!directoryStat.isDirectory() || directoryStat.isSymbolicLink()) return null; + fs.chmodSync(directory, 0o700); + let fd; + try { + fd = fs.openSync(destination, 'wx', 0o600); + } catch (err) { + if (err.code === 'EEXIST' && !replace) return 'exists'; + if (err.code === 'EEXIST' && replace) { + const temporary = destination + '.' + crypto.randomUUID() + '.tmp'; + try { + fd = fs.openSync(temporary, 'wx', 0o600); + fs.writeFileSync(fd, contents, 'utf8'); + fs.fsyncSync(fd); + fs.closeSync(fd); + fs.renameSync(temporary, destination); + fs.chmodSync(destination, 0o600); + return true; + } catch (writeError) { + try { + if (fd !== undefined) fs.closeSync(fd); + } catch {} + fs.rmSync(temporary, { force: true }); + throw writeError; + } + } + throw err; + } + try { + fs.writeFileSync(fd, contents, 'utf8'); + fs.fsyncSync(fd); + fs.closeSync(fd); + fs.chmodSync(destination, 0o600); + } catch (err) { + try { + fs.closeSync(fd); + } catch {} + fs.rmSync(destination, { force: true }); + throw err; + } + return true; +} + +function isSyntheticJwt(value) { + if (typeof value !== 'string') return false; + const parts = value.split('.'); + if (parts.length !== 3 || parts[2] !== 'synthetic') return false; + try { + const header = JSON.parse(Buffer.from(parts[0], 'base64url').toString('utf8')); + const payload = JSON.parse(Buffer.from(parts[1], 'base64url').toString('utf8')); + return header?.alg === 'none' && Number.isSafeInteger(payload?.exp); + } catch { + return false; + } +} + +function isPlaceholderCredential(name, contents) { + if (name === 'claude-token') { + return contents === 'sk-ant-oat01-mainloop-egress-placeholder'; + } + if (name !== 'codex-auth') return false; + try { + const auth = JSON.parse(contents); + const tokens = auth?.tokens; + return ( + auth?.auth_mode === 'chatgpt' && + typeof auth?.last_refresh === 'string' && + typeof tokens?.account_id === 'string' && + tokens.account_id.trim().length > 0 && + tokens.refresh_token === '' && + isSyntheticJwt(tokens.access_token) && + isSyntheticJwt(tokens.id_token) + ); + } catch { + return false; + } +} + +function atomicJsonWrite(file, document) { + const temporary = file + '.' + process.pid + '.tmp'; + fs.writeFileSync(temporary, JSON.stringify(document), { mode: 0o600 }); + fs.renameSync(temporary, file); +} + +function metadataPath(directory, id) { + return path.join(directory, id + '.json'); +} + +function turnKey(agent, sessionKey) { + return `${agent}:${sessionKey}`; +} + +function validSessionKey(value) { + return typeof value === 'string' && /^[A-Za-z0-9._:-]{1,256}$/.test(value); +} + +function validStartupOptions(value, agent) { + if (!value || typeof value !== 'object' || Array.isArray(value)) return false; + const keys = Object.keys(value); + const allowedKeys = new Set([ + 'cwd_rel', + 'token', + 'standing_b64', + 'approval_policy', + 'model', + 'effort' + ]); + if (keys.some((key) => !allowedKeys.has(key))) return false; + if ( + typeof value.cwd_rel !== 'string' || + typeof value.token !== 'string' || + !/^ml_[a-f0-9]{64}$/.test(value.token) || + typeof value.standing_b64 !== 'string' || + value.standing_b64.length === 0 || + value.standing_b64.length > 44 * 1024 || + !/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/.test(value.standing_b64) + ) { + return false; + } + const standing = Buffer.from(value.standing_b64, 'base64'); + if ( + standing.length === 0 || + standing.length > 32 * 1024 || + standing.toString('base64') !== value.standing_b64 + ) { + return false; + } + if (value.cwd_rel === 'main') { + return ( + agent === 'claude' && + value.approval_policy === 'restricted: Bash(mainloop:*) only' && + typeof value.model === 'string' && + /^[A-Za-z0-9._:-]{1,128}$/.test(value.model) && + typeof value.effort === 'string' && + /^(low|medium|high|max|xhigh)$/.test(value.effort) + ); + } + return ( + typeof value.cwd_rel === 'string' && + new RegExp(`^children/ml-${agent}-[0-9a-f]{8}$`).test(value.cwd_rel) && + value.approval_policy === 'bypass-permissions' && + value.model === undefined && + value.effort === undefined + ); +} + +function loadJobs(directory, kind) { + for (const name of fs.readdirSync(directory)) { + if (!name.endsWith('.json')) continue; + try { + const record = JSON.parse(fs.readFileSync(path.join(directory, name), 'utf8')); + if (!record || typeof record.id !== 'string') continue; + if (record.status === 'running') { + record.status = 'interrupted'; + record.exit_code = null; + record.blocking = kind === 'turn'; + record.updated_at = new Date().toISOString(); + atomicJsonWrite(path.join(directory, name), record); + } + jobs.set(record.id, { ...record, kind }); + if (kind === 'turn' && record.agent && validSessionKey(record.session_key)) { + const key = turnKey(record.agent, record.session_key); + const previous = latestTurns.get(key); + if (!previous || String(record.created_at) > String(previous.created_at)) { + latestTurns.set(key, record); + } + if (record.blocking) activeTurns.set(key, record.id); + } + } catch { + // Ignore an incomplete or corrupt record; it cannot safely be resumed. + } + } +} + +loadJobs(RUN_DIR, 'run'); +loadJobs(TURN_DIR, 'turn'); + +function createJob(kind, fields) { + const id = crypto.randomUUID(); + const directory = kind === 'run' ? RUN_DIR : TURN_DIR; + const job = { + id, + kind, + status: 'running', + exit_code: null, + created_at: new Date().toISOString(), + updated_at: new Date().toISOString(), + truncated: false, + stored_bytes: 0, + ...fields + }; + jobs.set(id, job); + atomicJsonWrite(metadataPath(directory, id), publicMetadata(job)); + return job; +} + +function publicMetadata(job) { + const { + id, + kind, + status, + exit_code, + created_at, + updated_at, + truncated, + agent, + session_key, + native_session_id, + blocking + } = job; + return { + id, + kind, + status, + exit_code, + created_at, + updated_at, + truncated, + agent, + session_key, + native_session_id, + blocking + }; +} + +function saveJob(job) { + job.updated_at = new Date().toISOString(); + const directory = job.kind === 'run' ? RUN_DIR : TURN_DIR; + atomicJsonWrite(metadataPath(directory, job.id), publicMetadata(job)); +} + +function appendBounded(job, filename, chunk) { + const remaining = MAX_JOB_OUTPUT_BYTES - job.stored_bytes; + if (remaining <= 0) { + job.truncated = true; + return; + } + const bytes = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk); + const stored = bytes.length > remaining ? bytes.subarray(0, remaining) : bytes; + fs.appendFileSync(filename, stored, { mode: 0o600 }); + job.stored_bytes += stored.length; + if (stored.length < bytes.length) job.truncated = true; +} + +function readBounded(filename, limit = MAX_RESPONSE_OUTPUT_BYTES) { + try { + const fd = fs.openSync(filename, 'r'); + try { + const size = fs.fstatSync(fd).size; + const length = Math.min(size, limit); + const buffer = Buffer.alloc(length); + fs.readSync(fd, buffer, 0, length, Math.max(0, size - length)); + return { text: buffer.toString('utf8'), truncated: size > limit }; + } finally { + fs.closeSync(fd); + } + } catch (err) { + if (err.code === 'ENOENT') return { text: '', truncated: false }; + throw err; + } +} + +function runOutputPath(job) { + return path.join(RUN_DIR, job.id + '.output'); +} + +function turnEventsPath(job) { + return path.join(TURN_DIR, job.id + '.events.jsonl'); +} + +function turnStderrPath(job) { + return path.join(TURN_DIR, job.id + '.stderr'); +} + +function killProcessGroup(child, signal) { + if (!child.pid) return; + try { + process.kill(-child.pid, signal); + } catch (err) { + if (err.code !== 'ESRCH') child.kill(signal); + } +} + +function runChild(job, child, timeoutMs, onStart) { + let timedOut = false; + let spawnError = null; + let finalized = false; + let exitCode = null; + const finish = (code) => { + if (finalized) return; + finalized = true; + if (timedOut) job.status = 'timed_out'; + else if (job.stop_requested) job.status = 'interrupted'; + else if (spawnError) job.status = 'failed'; + else job.status = code === 0 ? 'completed' : 'failed'; + job.exit_code = code; + job.blocking = false; + if (spawnError) job.error = 'process could not start'; + saveJob(job); + const key = turnKey(job.agent, job.session_key); + if (job.kind === 'turn' && activeTurns.get(key) === job.id) { + activeTurns.delete(key); + } + if (job.kind === 'turn') turnProcesses.delete(job.id); + if (job.finishTurn) job.finishTurn(); + }; + const timer = setTimeout(() => { + timedOut = true; + job.timed_out = true; + killProcessGroup(child, 'SIGTERM'); + const killTimer = setTimeout(() => { + killProcessGroup(child, 'SIGKILL'); + finish(exitCode); + }, 500); + killTimer.unref(); + }, timeoutMs); + timer.unref(); + child.once('error', (err) => { + spawnError = err; + }); + onStart(child); + child.once('close', (code) => { + clearTimeout(timer); + exitCode = code; + if (!timedOut) finish(code); + }); +} + +function parseTurn(job) { + const file = turnEventsPath(job); + const content = readBounded(file, MAX_JOB_OUTPUT_BYTES).text; + const lines = content.split('\n'); + const parsed = []; + let nativeSessionId = job.native_session_id || null; + let finalMessage = null; + for (const line of lines) { + if (!line.trim()) continue; + let event; + try { + event = JSON.parse(line); + } catch { + continue; + } + if (!event || typeof event !== 'object' || Array.isArray(event)) continue; + if (event.session_id && job.agent === 'claude') nativeSessionId = event.session_id; + if (event.thread_id && job.agent === 'codex') nativeSessionId = event.thread_id; + if (event.type === 'thread.started' && event.thread_id) nativeSessionId = event.thread_id; + if (event.type === 'result' && typeof event.result === 'string') finalMessage = event.result; + const item = event.item || (event.params && event.params.item); + if ( + job.agent === 'codex' && + item && + (item.type === 'agent_message' || item.type === 'agentMessage') && + typeof item.text === 'string' + ) + finalMessage = item.text; + parsed.push(event); + } + return { + native_session_id: nativeSessionId, + final_message: finalMessage, + events: parsed.slice(-MAX_RETURN_EVENTS) + }; +} + +function turnResponse(job) { + const parsed = parseTurn(job); + if (parsed.native_session_id !== job.native_session_id) { + job.native_session_id = parsed.native_session_id; + saveJob(job); + } + const stderr = readBounded(turnStderrPath(job)); + const authDiagnostic = `${stderr.text}\n${JSON.stringify(parsed.events)}`; + const credentialRejected = + job.status === 'failed' && + /(?:HTTP\s+)?401\b|unauthori[sz]ed|invalid[_ ]api[_ ]key|authentication_failed/i.test( + authDiagnostic + ); + return { + id: job.id, + agent: job.agent, + status: job.status, + exit_code: job.exit_code, + native_session_id: parsed.native_session_id, + final_message: parsed.final_message, + events: parsed.events, + stderr: stderr.text, + credential_rejected: credentialRejected, + truncated: job.truncated || stderr.truncated + }; +} + +function findJournal(agent, id) { + const root = + agent === 'claude' + ? path.join(process.env.CLAUDE_CONFIG_DIR || path.join(HOME_PATH, '.claude'), 'projects') + : path.join(CODEX_HOME_PATH, 'sessions'); + const expected = agent === 'claude' ? `${id}.jsonl` : null; + const stack = [root]; + let visited = 0; + while (stack.length > 0 && visited < MAX_JOURNAL_SEARCH_ENTRIES) { + const directory = stack.pop(); + let entries; + try { + entries = fs.readdirSync(directory, { withFileTypes: true }); + } catch (err) { + if (err.code === 'ENOENT' || err.code === 'ENOTDIR' || err.code === 'EACCES') continue; + throw err; + } + for (const entry of entries) { + visited += 1; + if (visited > MAX_JOURNAL_SEARCH_ENTRIES) break; + const candidate = path.join(directory, entry.name); + if (entry.isDirectory()) stack.push(candidate); + else if ( + entry.isFile() && + (agent === 'claude' + ? entry.name === expected + : entry.name.startsWith('rollout-') && entry.name.endsWith(`-${id}.jsonl`)) + ) + return candidate; + } + } + return null; +} + +async function journalPage(file, from, limit) { + const lines = []; + let totalLines = 0; + let responseBytes = 0; + let pending = Buffer.alloc(0); + const stream = fs.createReadStream(file); + for await (const chunk of stream) { + let offset = 0; + let boundary; + while ((boundary = chunk.indexOf(0x0a, offset)) !== -1) { + const part = chunk.subarray(offset, boundary); + const line = pending.length ? Buffer.concat([pending, part]) : part; + pending = Buffer.alloc(0); + if (line.length > MAX_JOURNAL_LINE_BYTES) throw new RangeError('journal line too large'); + totalLines += 1; + const cost = line.length + 24; + if ( + totalLines > from && + totalLines <= from + limit && + responseBytes + cost <= MAX_JOURNAL_RESPONSE_BYTES + ) { + lines.push({ line: totalLines, text: line.toString('utf8') }); + responseBytes += cost; + } + offset = boundary + 1; + } + if (offset < chunk.length) { + const rest = chunk.subarray(offset); + pending = pending.length ? Buffer.concat([pending, rest]) : rest; + if (pending.length > MAX_JOURNAL_LINE_BYTES) throw new RangeError('journal line too large'); + } + } + return { file, total_lines: totalLines, lines }; +} + +async function getJournal(req, res) { + const query = new URL(req.url, 'http://exec-shim.invalid').searchParams; + const agent = query.get('agent'); + const id = query.get('id'); + const fromText = query.get('from') || '0'; + const limitText = query.get('limit') || String(MAX_JOURNAL_LINES); + if ( + (agent !== 'claude' && agent !== 'codex') || + typeof id !== 'string' || + !/^[A-Za-z0-9._:-]{1,256}$/.test(id) || + !/^\d{1,12}$/.test(fromText) || + !/^\d{1,4}$/.test(limitText) + ) { + res.writeHead(400).end('invalid journal query'); + return; + } + const from = Number(fromText); + const limit = Number(limitText); + if ( + !Number.isSafeInteger(from) || + !Number.isInteger(limit) || + limit < 1 || + limit > MAX_JOURNAL_LINES + ) { + res.writeHead(400).end('invalid journal page'); + return; + } + const file = findJournal(agent, id); + if (!file) { + json(res, 200, { file: null, total_lines: 0, lines: [] }); + return; + } + try { + json(res, 200, await journalPage(file, from, limit)); + } catch (err) { + if (err instanceof RangeError) { + res.writeHead(413).end('journal line too large'); + return; + } + res.writeHead(500).end('journal could not be read'); + } +} + +function workspaceReady() { + try { + const stat = fs.statSync(WORKSPACE_PATH); + if (!stat.isDirectory()) return false; + fs.accessSync(WORKSPACE_PATH, fs.constants.R_OK | fs.constants.W_OK); + return true; + } catch { + return false; + } +} + +function validateTimeout(value, fallback) { + if (value === undefined) return fallback; + if (!Number.isInteger(value) || value < 1 || value > MAX_TIMEOUT_MS) return null; + return value; +} + +function handleBody(req, res, callback, maxBytes = MAX_REQUEST_BODY_BYTES) { + requestBody( + req, + res, + (body) => { + let document; + try { + document = JSON.parse(body); + } catch { + res.writeHead(400).end('invalid json'); + return; + } + if (!document || typeof document !== 'object' || Array.isArray(document)) { + res.writeHead(400).end('invalid request'); + return; + } + callback(document); + }, + maxBytes + ); +} + +function startRun(document, res) { + const command = document.command; + const timeoutMs = validateTimeout(document.timeout_ms, DEFAULT_RUN_TIMEOUT_MS); + if (typeof command !== 'string' || command.length === 0) { + res.writeHead(400).end('missing command'); + return; + } + if (timeoutMs === null) { + res.writeHead(400).end('invalid timeout_ms'); + return; + } + if (!workspaceReady()) { + res.writeHead(503).end('workspace is not ready'); + return; + } + const job = createJob('run', { blocking: false }); + try { + const child = spawn('/bin/sh', ['-lc', command], { + cwd: WORKSPACE_PATH, + env: process.env, + stdio: ['ignore', 'pipe', 'pipe'], + detached: true + }); + runChild(job, child, timeoutMs, (processChild) => { + processChild.stdout.on('data', (chunk) => appendBounded(job, runOutputPath(job), chunk)); + processChild.stderr.on('data', (chunk) => appendBounded(job, runOutputPath(job), chunk)); + }); + json(res, 202, { id: job.id, status: job.status }); + } catch { + job.status = 'failed'; + job.error = 'process could not start'; + saveJob(job); + json(res, 500, { id: job.id, status: job.status }); + } +} + +function startTurn(document, res) { + const agent = document.agent; + const prompt = document.prompt; + const sessionId = document.session_id; + const sessionKey = document.session_key; + const resume = document.resume; + const hasStartupOptions = Object.hasOwn(document, 'startup_options'); + const startupOptions = document.startup_options; + const timeoutMs = validateTimeout(document.timeout_ms, DEFAULT_TURN_TIMEOUT_MS); + if (agent !== 'claude' && agent !== 'codex') { + res.writeHead(400).end('unsupported agent'); + return; + } + if (typeof prompt !== 'string' || prompt.length === 0) { + res.writeHead(400).end('missing prompt'); + return; + } + if (!validSessionKey(sessionKey)) { + res.writeHead(400).end('invalid session_key'); + return; + } + if (typeof resume !== 'boolean') { + res.writeHead(400).end('invalid resume intent'); + return; + } + if (hasStartupOptions && !validStartupOptions(startupOptions, agent)) { + res.writeHead(400).end('invalid startup_options'); + return; + } + if ( + sessionId !== undefined && + (typeof sessionId !== 'string' || !/^[A-Za-z0-9._:-]{1,256}$/.test(sessionId)) + ) { + res.writeHead(400).end('invalid session_id'); + return; + } + if (resume && !sessionId) { + res.writeHead(400).end('resume requires an established session_id'); + return; + } + if (!resume && agent === 'claude' && !sessionId) { + res.writeHead(400).end('new Claude sessions require a session_id'); + return; + } + if (timeoutMs === null) { + res.writeHead(400).end('invalid timeout_ms'); + return; + } + if (!workspaceReady()) { + res.writeHead(503).end('workspace is not ready'); + return; + } + const key = turnKey(agent, sessionKey); + if (activeTurns.has(key)) { + res.writeHead(409).end('turn already in flight for session'); + return; + } + + const job = createJob('turn', { + agent, + session_key: sessionKey, + native_session_id: sessionId || null, + resume, + blocking: true + }); + job.completion = new Promise((resolve) => { + job.finishTurn = resolve; + }); + activeTurns.set(key, job.id); + latestTurns.set(key, job); + try { + const launcherEnv = { ...process.env }; + for (const name of [ + 'MAINLOOP_STARTUP_PRESENT', + 'MAINLOOP_STARTUP_CWD_REL', + 'MAINLOOP_STARTUP_TOKEN', + 'MAINLOOP_STARTUP_STANDING_B64', + 'MAINLOOP_STARTUP_APPROVAL_POLICY', + 'MAINLOOP_STARTUP_MODEL', + 'MAINLOOP_STARTUP_EFFORT' + ]) { + delete launcherEnv[name]; + } + if (hasStartupOptions) { + launcherEnv.MAINLOOP_STARTUP_PRESENT = '1'; + launcherEnv.MAINLOOP_STARTUP_CWD_REL = startupOptions.cwd_rel; + launcherEnv.MAINLOOP_STARTUP_TOKEN = startupOptions.token; + launcherEnv.MAINLOOP_STARTUP_STANDING_B64 = startupOptions.standing_b64; + launcherEnv.MAINLOOP_STARTUP_APPROVAL_POLICY = startupOptions.approval_policy; + if (startupOptions.model !== undefined) { + launcherEnv.MAINLOOP_STARTUP_MODEL = startupOptions.model; + launcherEnv.MAINLOOP_STARTUP_EFFORT = startupOptions.effort; + } + } + const child = spawn( + LAUNCHER, + [agent, resume ? 'resume' : 'create', ...(sessionId ? [sessionId] : [])], + { + cwd: WORKSPACE_PATH, + env: launcherEnv, + stdio: ['pipe', 'pipe', 'pipe'], + detached: true + } + ); + runChild(job, child, timeoutMs, (processChild) => { + turnProcesses.set(job.id, processChild); + processChild.stdout.on('data', (chunk) => appendBounded(job, turnEventsPath(job), chunk)); + processChild.stderr.on('data', (chunk) => appendBounded(job, turnStderrPath(job), chunk)); + processChild.stdin.on('error', () => {}); + processChild.stdin.end(prompt, 'utf8'); + }); + json(res, 202, { id: job.id, status: job.status }); + } catch { + activeTurns.delete(key); + job.status = 'failed'; + job.blocking = false; + job.error = 'process could not start'; + saveJob(job); + json(res, 500, { id: job.id, status: job.status }); + } +} + +function currentTurn(agent, sessionKey, res) { + if (agent !== 'claude' && agent !== 'codex') { + res.writeHead(400).end('unsupported agent'); + return; + } + if (!validSessionKey(sessionKey)) { + res.writeHead(400).end('invalid session_key'); + return; + } + const job = latestTurns.get(turnKey(agent, sessionKey)); + if (!job) { + res.writeHead(404).end('no turn'); + return; + } + json(res, 200, turnResponse(job)); +} + +function agentReady(agent, res) { + if (agent !== 'claude' && agent !== 'codex') { + res.writeHead(400).end('unsupported agent'); + return; + } + const credential = credentialPaths.get(agent === 'claude' ? 'claude-token' : 'codex-auth'); + try { + const stat = fs.statSync(credential); + if (stat.isFile() && stat.size > 0) { + json(res, 200, { agent, configured: true }); + return; + } + } catch { + // A missing credential is a readiness failure; never include file contents or paths. + } + res.writeHead(503).end('agent credential unavailable'); +} + +async function stopTurn(document, res) { + const agent = document.agent; + const sessionKey = document.session_key; + if (agent !== 'claude' && agent !== 'codex') { + res.writeHead(400).end('unsupported agent'); + return; + } + if (!validSessionKey(sessionKey)) { + res.writeHead(400).end('invalid session_key'); + return; + } + const key = turnKey(agent, sessionKey); + const id = activeTurns.get(key); + const child = id && turnProcesses.get(id); + const job = id && jobs.get(id); + if (!job) { + json(res, 200, { status: 'not_running' }); + return; + } + if (!child) { + // A restored actor can retain an interrupted job record without a live process. The caller + // explicitly requested stop, so release the per-agent turn lock without replaying anything. + job.status = 'interrupted'; + job.blocking = false; + saveJob(job); + activeTurns.delete(key); + json(res, 200, { id, status: job.status }); + return; + } + job.stop_requested = true; + saveJob(job); + killProcessGroup(child, 'SIGTERM'); + const killTimer = setTimeout(() => killProcessGroup(child, 'SIGKILL'), 500); + killTimer.unref(); + await job.completion; + clearTimeout(killTimer); + json(res, 200, { id, status: job.status }); +} + +function getJob(kind, id, res) { + if (!/^[0-9a-f-]{36}$/i.test(id)) { + res.writeHead(404).end('not found'); + return; + } + const job = jobs.get(id); + if (!job || job.kind !== kind) { + res.writeHead(404).end('not found'); + return; + } + if (kind === 'turn') { + json(res, 200, turnResponse(job)); + return; + } + const output = readBounded(runOutputPath(job)); + json(res, 200, { + id: job.id, + status: job.status, + exit_code: job.exit_code, + output: output.text, + truncated: job.truncated || output.truncated + }); +} + +function processSummary(entry) { + return { + id: entry.id, + label: entry.label, + status: entry.status, + started_at: entry.started_at, + exit_code: entry.exit_code, + log_tail: entry.log.toString('utf8') + }; +} + +function listProcesses(res) { + json(res, 200, { processes: [...longProcesses.values()].map(processSummary) }); +} + +function startLongProcess(document, res) { + const command = document.command; + const label = document.label === undefined ? 'process' : document.label; + const running = [...longProcesses.values()].filter((entry) => entry.status === 'running'); + if ( + typeof command !== 'string' || + !command.trim() || + Buffer.byteLength(command, 'utf8') > 8192 || + typeof label !== 'string' || + !label.trim() || + label.length > 100 + ) { + res.writeHead(400).end('invalid process'); + return; + } + if (running.length >= MAX_LONG_PROCESSES) { + res.writeHead(409).end('process limit reached'); + return; + } + + const id = crypto.randomUUID(); + let child; + try { + child = spawn('/bin/sh', ['-lc', command], { + cwd: WORKSPACE_PATH, + env: process.env, + detached: true, + stdio: ['ignore', 'pipe', 'pipe'] + }); + } catch { + res.writeHead(500).end('process could not start'); + return; + } + const entry = { + id, + label: label.trim(), + status: 'running', + started_at: new Date().toISOString(), + exit_code: null, + log: Buffer.alloc(0), + child, + completion: null + }; + const appendLog = (chunk) => { + const combined = Buffer.concat([entry.log, chunk]); + entry.log = combined.subarray(Math.max(0, combined.length - MAX_LONG_PROCESS_LOG_BYTES)); + }; + child.stdout.on('data', appendLog); + child.stderr.on('data', appendLog); + entry.completion = new Promise((resolve) => { + child.once('error', () => { + entry.status = 'failed'; + entry.exit_code = null; + resolve(); + }); + child.once('exit', (code, signal) => { + entry.status = signal ? 'stopped' : code === 0 ? 'completed' : 'failed'; + entry.exit_code = Number.isInteger(code) ? code : null; + resolve(); + }); + }); + longProcesses.set(id, entry); + while (longProcesses.size > MAX_LONG_PROCESS_RECORDS) { + const oldest = longProcesses.values().next().value; + if (oldest.status === 'running') break; + longProcesses.delete(oldest.id); + } + json(res, 201, { id, status: entry.status }); +} + +async function stopLongProcess(id, res) { + if (!/^[0-9a-f-]{36}$/i.test(id)) { + res.writeHead(404).end('not found'); + return; + } + const entry = longProcesses.get(id); + if (!entry) { + res.writeHead(404).end('not found'); + return; + } + if (entry.status === 'running') { + killProcessGroup(entry.child, 'SIGTERM'); + const killTimer = setTimeout(() => killProcessGroup(entry.child, 'SIGKILL'), 500); + killTimer.unref(); + await entry.completion; + clearTimeout(killTimer); + } + json(res, 200, { id, status: entry.status }); +} + +function listeningPorts() { + const ports = new Set(); + for (const name of ['tcp', 'tcp6']) { + let contents; + try { + contents = fs.readFileSync(path.join(PROC_NET_DIR, name), 'utf8'); + } catch { + continue; + } + for (const line of contents.split('\n').slice(1)) { + const columns = line.trim().split(/\s+/); + if (columns.length < 4 || columns[3] !== '0A') continue; + const local = columns[1].split(':'); + if (local.length !== 2) continue; + const port = Number.parseInt(local[1], 16); + if (Number.isInteger(port) && port > 0 && port <= 65535) ports.add(port); + } + } + return [...ports].sort((left, right) => left - right); +} + +const server = http.createServer((req, res) => { + if (req.method === 'GET' && (req.url === '/healthz' || req.url === '/readyz')) { + if (!workspaceReady()) { + res.writeHead(503, { 'content-type': 'text/plain' }).end('workspace not ready'); + return; + } + res.writeHead(200, { 'content-type': 'text/plain' }).end('ok'); + return; + } + if (req.method === 'POST' && req.url === '/token') { + if (bearerToken !== null) { + res.writeHead(409).end('token already set'); + return; + } + handleBody(req, res, (document) => { + try { + const installed = installToken(document.token); + if (installed === null) { + res.writeHead(400).end('invalid token'); + return; + } + if (!installed) { + res.writeHead(409).end('token already set'); + return; + } + res.writeHead(201).end('token set'); + } catch { + res.writeHead(500).end('token could not be stored'); + } + }); + return; + } + if ((req.method === 'POST' || req.method === 'PUT') && req.url === '/credential') { + if (!authorized(req)) return unauthorized(res); + const replace = req.method === 'PUT'; + handleBody( + req, + res, + (document) => { + try { + const installed = installCredential(document.name, document.contents, replace); + if (installed === null) { + res.writeHead(400).end('invalid credential'); + return; + } + if (!installed) { + res.writeHead(403).end('credential name is not allowlisted'); + return; + } + if (installed === 'exists') { + res.writeHead(409).end('credential file already exists'); + return; + } + res.writeHead(replace ? 200 : 201).end('credential stored'); + } catch { + res.writeHead(500).end('credential could not be stored'); + } + }, + MAX_CREDENTIAL_REQUEST_BODY_BYTES + ); + return; + } + if (req.method === 'POST' && req.url === '/run') { + if (!authorized(req)) return unauthorized(res); + handleBody(req, res, (document) => startRun(document, res)); + return; + } + if (req.method === 'POST' && req.url === '/processes') { + if (!authorized(req)) return unauthorized(res); + handleBody(req, res, (document) => startLongProcess(document, res)); + return; + } + if (req.method === 'GET' && req.url === '/processes') { + if (!authorized(req)) return unauthorized(res); + listProcesses(res); + return; + } + if (req.method === 'GET' && req.url === '/ports') { + if (!authorized(req)) return unauthorized(res); + json(res, 200, { ports: listeningPorts() }); + return; + } + const processMatch = req.url.match(/^\/processes\/([^/?]+)$/); + if (req.method === 'DELETE' && processMatch) { + if (!authorized(req)) return unauthorized(res); + void stopLongProcess(processMatch[1], res); + return; + } + if (req.method === 'POST' && req.url === '/turn') { + if (!authorized(req)) return unauthorized(res); + handleBody(req, res, (document) => startTurn(document, res), MAX_TURN_REQUEST_BODY_BYTES); + return; + } + if (req.method === 'POST' && req.url === '/turn/stop') { + if (!authorized(req)) return unauthorized(res); + handleBody(req, res, (document) => void stopTurn(document, res)); + return; + } + if (req.method === 'GET' && req.url.startsWith('/turn/status?')) { + if (!authorized(req)) return unauthorized(res); + const query = new URL(req.url, 'http://exec-shim.invalid').searchParams; + currentTurn(query.get('agent'), query.get('session_key'), res); + return; + } + if (req.method === 'GET' && req.url.startsWith('/agent/ready?')) { + if (!authorized(req)) return unauthorized(res); + const query = new URL(req.url, 'http://exec-shim.invalid').searchParams; + agentReady(query.get('agent'), res); + return; + } + if (req.method === 'GET' && req.url.startsWith('/journal?')) { + if (!authorized(req)) return unauthorized(res); + void getJournal(req, res); + return; + } + const match = req.method === 'GET' && req.url.match(/^\/(run|turn)\/([^/?]+)$/); + if (match) { + if (!authorized(req)) return unauthorized(res); + getJob(match[1] === 'run' ? 'run' : 'turn', match[2], res); + return; + } + res.writeHead(404).end(); +}); + +server.listen( + Number(process.env.EXEC_SHIM_PORT || 8090), + process.env.EXEC_SHIM_HOST || '0.0.0.0', + () => { + const address = server.address(); + console.log('exec-shim listening on :' + address.port); + } +); diff --git a/spikes/substrate-workspace-adapter/live/claude_turn_proof.sh b/spikes/substrate-workspace-adapter/live/claude_turn_proof.sh new file mode 100755 index 0000000..d256bef --- /dev/null +++ b/spikes/substrate-workspace-adapter/live/claude_turn_proof.sh @@ -0,0 +1,481 @@ +#!/usr/bin/env bash +# Bounded live Claude continuity proof on the product template. This script +# uses a prebuilt provider image and never builds or pushes images. +set -euo pipefail + +SCRIPT_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd) +# shellcheck source=common.sh +source "${SCRIPT_DIR}/common.sh" + +ATESPACE=lane-a-claude-20260924 +POOL=lane-a-claude-20260924 +ACTOR=claude-live-proof +STATE_FILE=${STATE_ROOT}/claude-gate5-state.json +CLAUDE_TOKEN_FILE=${CLAUDE_TOKEN_FILE:-${HOME}/.claude-token} +CLAUDE_SECRET_URI=ate-secret://kubernetes.io/mainloop-control/claude-oauth/oauth-token +PROVIDER_IMAGE=${CLAUDE_PROVIDER_IMAGE-} +PROVIDER_READY=0 +LANE_STARTED=0 +COMPLETE=0 +declare -A PROVIDER_UIDS=() + +record_provider_uid() { + local resource=$1 object_json=$2 uid + PROVIDER_READY=1 + if ! uid=$(jq -er '.metadata.uid | strings | select(length > 0)' <<<"${object_json}"); then + die "created provider resource ${resource} response omitted metadata.uid" + return 1 + fi + PROVIDER_UIDS["${resource}"]=${uid} +} + +cleanup_provider() { + ((PROVIDER_READY == 1)) || return 0 + local resource kind name recorded_uid live_uid + for resource in networkpolicy/round3-claude-provider deployment/round3-claude-provider \ + service/credprovider serviceaccount/round3-claude-provider secret/claude-oauth; do + recorded_uid=${PROVIDER_UIDS["${resource}"]-} + [[ -n ${recorded_uid} ]] || continue + kind=${resource%%/*} + name=${resource#*/} + # shellcheck disable=SC2310 # kubectl_ctx has one command; inspect its status below. + if live_uid=$(kubectl_ctx -n mainloop-control get "${kind}" "${name}" \ + -o jsonpath='{.metadata.uid}' 2>&1); then + if [[ ${live_uid} != "${recorded_uid}" ]]; then + printf 'provider cleanup skipped %s: UID changed from %s to %s\n' \ + "${resource}" "${recorded_uid}" "${live_uid:-missing}" >&2 + continue + fi + local delete_path + case ${kind} in + networkpolicy) + delete_path="/apis/networking.k8s.io/v1/namespaces/mainloop-control/networkpolicies/${name}" + ;; + deployment) + delete_path="/apis/apps/v1/namespaces/mainloop-control/deployments/${name}" + ;; + service) + delete_path="/api/v1/namespaces/mainloop-control/services/${name}" + ;; + serviceaccount) + delete_path="/api/v1/namespaces/mainloop-control/serviceaccounts/${name}" + ;; + secret) + delete_path="/api/v1/namespaces/mainloop-control/secrets/${name}" + ;; + *) + die "unsupported provider resource kind ${kind}" + return 1 + ;; + esac + if ! kubectl_ctx delete --raw="${delete_path}" -f - </dev/null; then + die "timed out waiting for provider Deployment ${name} deletion" + return 1 + fi + elif [[ ${live_uid} =~ [Nn]ot[Ff]ound|not\ found|does\ not\ exist ]]; then + continue + else + printf '%s\n' "${live_uid}" >&2 + die "could not verify provider resource ${resource} before cleanup" + return 1 + fi + done + PROVIDER_READY=0 +} + +finish() { + local status=$? + stop_router + if ((status != 0 && COMPLETE == 0 && LANE_STARTED == 1)); then + printf 'run failed; attempting cleanup of the owned Claude lane\n' >&2 + # shellcheck disable=SC2310 # cleanup_provider explicitly checks and returns each delete failure. + cleanup_provider || printf 'provider cleanup did not complete; inspect provider resources\n' >&2 + SKIP_PROVIDER_CLEANUP=1 "${SCRIPT_DIR}/cleanup-lane-a.sh" claude || + printf 'cleanup did not complete; inspect %s\n' "${STATE_FILE}" >&2 + fi + return "${status}" +} +trap finish EXIT INT TERM + +require_worker_image +if [[ ! -r ${CLAUDE_TOKEN_FILE} || ! -s ${CLAUDE_TOKEN_FILE} ]]; then + die "Claude credential file is missing or empty: ${CLAUDE_TOKEN_FILE}" +fi +if [[ -z ${PROVIDER_IMAGE} ]]; then + die 'set CLAUDE_PROVIDER_IMAGE to an existing digest-pinned provider image built from the pinned fork commit; this script does not build or push provider images' +fi +if [[ ! ${PROVIDER_IMAGE} =~ @sha256:[0-9a-fA-F]{64}$ ]]; then + die 'CLAUDE_PROVIDER_IMAGE must be an existing reference pinned by a full sha256 digest' +fi +prepare_state_file "${STATE_FILE}" "${ATESPACE}" +LANE_STARTED=1 +if timeout 300s kubectl --context "${CTX}" --kubeconfig "${KC}" \ + get workerpool "${POOL}" -n "${ATESPACE}" -o name >/dev/null 2>&1; then + die "WorkerPool ${ATESPACE}/${POOL} already exists; refusing to adopt it" +fi + +printf 'creating the Claude actor from the product template\n' +gate5_setup "${ATESPACE}" "${POOL}" "${ACTOR}" lane-a-claude-20260924 "${STATE_FILE}" api.anthropic.com +start_router + +uid=$(run_actor_command "${ATESPACE}" "${ACTOR}" "${STATE_FILE}" 'id -u' 30000) +if [[ ${uid} != 10001 ]]; then + die "actor runtime uid is ${uid}; refusing to deliver credentials or submit a Claude turn" +fi +printf 'actor runtime uid=%s\n' "${uid}" + +preflight_provider_resources_absent + +# The Secret receives only a path argument. Its generated manifest is piped directly +# to kubectl apply and is never printed or written to disk. +created_secret=$(kubectl_ctx -n mainloop-control create secret generic claude-oauth \ + --from-file="oauth-token=${CLAUDE_TOKEN_FILE}" \ + --dry-run=client -o json | + jq -c '.metadata.labels["proof.mainloop.dev/lane"] = "lane-a-live-proof"' | + kubectl_ctx create -f - -o json) +record_provider_uid secret/claude-oauth "${created_secret}" +unset created_secret + +created_service_account=$( + kubectl_ctx -n mainloop-control create -f - -o json <<'YAML' +apiVersion: v1 +kind: ServiceAccount +metadata: + name: round3-claude-provider + namespace: mainloop-control + labels: + proof.mainloop.dev/lane: lane-a-live-proof +YAML +) +record_provider_uid serviceaccount/round3-claude-provider "${created_service_account}" + +created_service=$( + kubectl_ctx -n mainloop-control create -f - -o json <<'YAML' +apiVersion: v1 +kind: Service +metadata: + name: credprovider + namespace: mainloop-control + labels: + proof.mainloop.dev/lane: lane-a-live-proof +spec: + selector: + app: round3-claude-provider + ports: + - name: grpc + port: 50051 + targetPort: 50051 +YAML +) +record_provider_uid service/credprovider "${created_service}" + +created_deployment=$( + kubectl_ctx -n mainloop-control create -f - -o json < 0) + and ((.version | tostring | test("^[1-9][0-9]*$")))) | {uid, version: (.version | tostring)}' \ + <<<"${current}") || die 'egress policy lacks metadata.uid and metadata.version preconditions' + jq -c --argjson metadata "${preconditions}" '. + {metadata: $metadata}' <<<"${rules}" | + ate_ctx update egress-policy "${ACTOR}" --atespace "${ATESPACE}" --filename - >/dev/null +} + +egress_policy=$(jq -cn --arg uri "${CLAUDE_SECRET_URI}" \ + '{rules:[{hostnames:{patterns:["api.anthropic.com"],effects:{injectStaticHeaders:[{header:"Authorization",prefix:"Bearer ",credentialUri:$uri}]}}}]}') +update_egress_policy "${egress_policy}" + +# This exact placeholder is allowlisted by the shim and carries no credential. +placeholder_request='{"method":"POST","path":"/credential","authenticated":true,"body":{"name":"claude-token","contents":"sk-ant-oat01-mainloop-egress-placeholder"}}' +response=$(shim_request "${ATESPACE}" "${ACTOR}" "${STATE_FILE}" "${placeholder_request}") +response_status=$(jq -r '.status // 0' <<<"${response}") +[[ ${response_status} == 201 ]] || die 'placeholder Claude credential was not installed through /credential' +ready=$(shim_request "${ATESPACE}" "${ACTOR}" "${STATE_FILE}" \ + '{"method":"GET","path":"/agent/ready?agent=claude","authenticated":true}') +ready_status=$(jq -r '.status // 0' <<<"${ready}") +[[ ${ready_status} == 200 ]] || die 'Claude credential readiness did not become healthy' +printf 'actor credential is the fixed placeholder; real OAuth remains in the control Secret\n' + +provider_probe_command="curl --silent --show-error --head --max-time 30 --output /dev/null --write-out '%{http_code}' --header 'Authorization: Bearer sk-ant-oat01-mainloop-egress-placeholder' https://api.anthropic.com/api/hello" +# The non-model probe is safe to repeat; retry only while the egress path to a +# freshly rolled-out provider is still settling (503, or no response). +for _ in $(seq 1 12); do + provider_probe=$(run_actor_command "${ATESPACE}" "${ACTOR}" "${STATE_FILE}" "${provider_probe_command}" 45000) + [[ ${provider_probe} == 503 || ${provider_probe} == 000 ]] || break + sleep 5 +done +[[ ${provider_probe} == 200 ]] || die "credential provider preflight returned HTTP ${provider_probe}" +provider_logs=$(kubectl_ctx -n mainloop-control logs deployment/round3-claude-provider --since=5m) +provider_fetches=$(grep -c 'credential_fetch=ok actor_identity_match=true' <<<"${provider_logs}" || true) +((provider_fetches > 0)) || die 'provider image did not accept the lane actor identity during the non-model preflight' +printf 'provider_preflight=http-200 matching_fetches=%s\n' "${provider_fetches}" + +nonce_timestamp=$(date +%s%N) +nonce_seed=$(printf '%s-%s-%s' "$$" "${RANDOM}" "${nonce_timestamp}") +nonce_hash=$(printf '%s' "${nonce_seed}" | sha256sum) +N1=${nonce_hash:0:24} +SESSION_ID=$(cat /proc/sys/kernel/random/uuid) +SESSION_KEY=lane-a-claude-liveproof +prompt=$(printf 'Remember this nonce exactly: %s. In /work/repo, create lane-a-claude-marker.txt containing exactly the nonce followed by one newline. Reply with only the nonce after both are done.' "${N1}") +turn_request=$(jq -cn --arg prompt "${prompt}" --arg session "${SESSION_ID}" --arg key "${SESSION_KEY}" \ + '{method:"POST",path:"/turn",authenticated:true,body:{agent:"claude",session_id:$session,session_key:$key,resume:false,timeout_ms:600000,prompt:$prompt}}') +first=$(shim_request "${ATESPACE}" "${ACTOR}" "${STATE_FILE}" "${turn_request}") +first_status=$(jq -r '.status // 0' <<<"${first}") +[[ ${first_status} == 202 ]] || die "first Claude turn was not accepted (HTTP ${first_status})" +TURN_ID=$(jq -r '.body.id // empty' <<<"${first}") +[[ -n ${TURN_ID} ]] || die 'first Claude turn response omitted its ID' + +concurrent_request=$(jq -cn --arg prompt 'This concurrent turn must be rejected; do not execute it.' \ + --arg session "${SESSION_ID}" --arg key "${SESSION_KEY}" \ + '{method:"POST",path:"/turn",authenticated:true,body:{agent:"claude",session_id:$session,session_key:$key,resume:true,timeout_ms:600000,prompt:$prompt}}') +concurrent=$(shim_request "${ATESPACE}" "${ACTOR}" "${STATE_FILE}" "${concurrent_request}") +concurrent_status=$(jq -r '.status // 0' <<<"${concurrent}") +if [[ ${concurrent_status} != 409 ]]; then + if [[ ${concurrent_status} == 202 ]]; then + stop_request=$(jq -cn --arg key "${SESSION_KEY}" '{method:"POST",path:"/turn/stop",authenticated:true,body:{agent:"claude",session_key:$key}}') + # shellcheck disable=SC2310 # This stop request is best effort; its failure is intentionally ignored. + shim_request "${ATESPACE}" "${ACTOR}" "${STATE_FILE}" "${stop_request}" >/dev/null || true + fi + die "concurrent Claude /turn returned HTTP ${concurrent_status}, expected 409; accepted turns are not replayed" +fi +printf 'concurrent_second_turn=HTTP-409\n' + +turn_deadline=$((SECONDS + 610)) +turn_json= +while ((SECONDS < turn_deadline)); do + poll_request=$(jq -cn --arg id "${TURN_ID}" '{method:"GET",path:("/turn/"+$id),authenticated:true}') + turn_response=$(shim_request "${ATESPACE}" "${ACTOR}" "${STATE_FILE}" "${poll_request}") + turn_http_status=$(jq -r '.status // 0' <<<"${turn_response}") + [[ ${turn_http_status} == 200 ]] || die 'could not read first Claude turn status' + turn_state=$(jq -r '.body.status // empty' <<<"${turn_response}") + case "${turn_state}" in + completed | failed | timed_out | interrupted) + turn_json=$(jq -c '.body' <<<"${turn_response}") + break + ;; + *) + # Nonterminal turn states continue through the bounded polling loop. + ;; + esac + sleep 2 +done +[[ -n ${turn_json} ]] || die 'first Claude turn exceeded its 10-minute bound' +turn_status=$(jq -r '.status' <<<"${turn_json}") +turn_exit=$(jq -r '.exit_code // "unknown"' <<<"${turn_json}") +if [[ ${turn_status} != completed || ${turn_exit} != 0 ]]; then + # The actor holds only the placeholder credential, so its diagnostics are safe to print. + jq '{credential_rejected, final_message, stderr_tail: ((.stderr // "")[-2000:]), last_events: ((.events // [])[-5:])}' \ + <<<"${turn_json}" >&2 + die "first Claude turn ended status=${turn_status} exit_code=${turn_exit}; no prompt retry was sent" +fi +native_session=$(jq -r '.native_session_id // empty' <<<"${turn_json}") +final_message=$(jq -r '.final_message // empty' <<<"${turn_json}") +[[ ${native_session} == "${SESSION_ID}" ]] || die 'Claude returned a different native session ID' +[[ ${final_message} == *"${N1}"* ]] || die 'Claude first-turn response did not contain the requested nonce' +printf 'first_turn=completed exit=0 session_id=%s nonce_match=yes\n' "${native_session}" + +marker_command="test \"\$(cat /work/repo/lane-a-claude-marker.txt)\" = '${N1}' && sha256sum /work/repo/lane-a-claude-marker.txt | cut -d ' ' -f 1" +marker_hash=$(run_actor_command "${ATESPACE}" "${ACTOR}" "${STATE_FILE}" "${marker_command}" 30000) +printf 'marker_hash_before_suspend=%s\n' "${marker_hash}" + +started_ns=$(date +%s%N) +ate_ctx suspend actor "${ACTOR}" --atespace "${ATESPACE}" >/dev/null +wait_actor_state "${ATESPACE}" "${ACTOR}" ACTOR_STATE_SUSPENDED 180 >/dev/null +suspend_ms=$((($(date +%s%N) - started_ns) / 1000000)) +started_ns=$(date +%s%N) +resume_ok=0 +for _ in $(seq 1 60); do + if resume_err=$(timeout 120s "${ATE_CLI}" --context "${CTX}" --kubeconfig "${KC}" \ + resume actor "${ACTOR}" --atespace "${ATESPACE}" 2>&1 >/dev/null); then + resume_ok=1 + break + fi + grep -q 'no free workers available' <<<"${resume_err}" || die "resume failed: ${resume_err}" + sleep 2 +done +((resume_ok == 1)) || die 'no worker registered as free within 120 seconds' +wait_actor_state "${ATESPACE}" "${ACTOR}" ACTOR_STATE_RUNNING 240 >/dev/null +health='' +for _ in $(seq 1 60); do + health=$(shim_request "${ATESPACE}" "${ACTOR}" "${STATE_FILE}" \ + '{"method":"GET","path":"/healthz","authenticated":false}') + health_status=$(jq -r '.status // 0' <<<"${health}") + if [[ ${health_status} == 200 ]]; then + break + fi + sleep 1 +done +health_status=$(jq -r '.status // 0' <<<"${health}") +[[ ${health_status} == 200 ]] || die 'resumed Claude actor did not pass /healthz' +resume_ms=$((($(date +%s%N) - started_ns) / 1000000)) +printf 'suspend_ms=%s resume_ms=%s health=200 worker_pool=%s\n' "${suspend_ms}" "${resume_ms}" "${POOL}" + +recall_prompt='Without reading any files, what exact nonce did I ask you to remember? Return only that nonce.' +recall_request=$(jq -cn --arg prompt "${recall_prompt}" --arg session "${SESSION_ID}" --arg key "${SESSION_KEY}" \ + '{method:"POST",path:"/turn",authenticated:true,body:{agent:"claude",session_id:$session,session_key:$key,resume:true,timeout_ms:600000,prompt:$prompt}}') +recall_start=$(shim_request "${ATESPACE}" "${ACTOR}" "${STATE_FILE}" "${recall_request}") +recall_start_status=$(jq -r '.status // 0' <<<"${recall_start}") +[[ ${recall_start_status} == 202 ]] || die 'Claude recall turn was not accepted' +recall_id=$(jq -r '.body.id // empty' <<<"${recall_start}") +[[ -n ${recall_id} ]] || die 'Claude recall response omitted its ID' +recall_deadline=$((SECONDS + 610)) +recall_json= +while ((SECONDS < recall_deadline)); do + poll_request=$(jq -cn --arg id "${recall_id}" '{method:"GET",path:("/turn/"+$id),authenticated:true}') + recall_response=$(shim_request "${ATESPACE}" "${ACTOR}" "${STATE_FILE}" "${poll_request}") + recall_http_status=$(jq -r '.status // 0' <<<"${recall_response}") + [[ ${recall_http_status} == 200 ]] || die 'could not read Claude recall status' + recall_state=$(jq -r '.body.status // empty' <<<"${recall_response}") + case "${recall_state}" in + completed | failed | timed_out | interrupted) + recall_json=$(jq -c '.body' <<<"${recall_response}") + break + ;; + *) + # Nonterminal turn states continue through the bounded polling loop. + ;; + esac + sleep 2 +done +[[ -n ${recall_json} ]] || die 'Claude recall turn exceeded its 10-minute bound' +recall_status=$(jq -r '.status' <<<"${recall_json}") +recall_exit_code=$(jq -r '.exit_code // "unknown"' <<<"${recall_json}") +[[ ${recall_status} == completed && ${recall_exit_code} == 0 ]] || die 'Claude recall turn failed; no retry was sent' +recall_message=$(jq -r '.final_message // empty' <<<"${recall_json}") +[[ ${recall_message} == *"${N1}"* ]] || die 'same-session Claude recall did not contain the nonce' +recall_session=$(jq -r '.native_session_id // empty' <<<"${recall_json}") +[[ ${recall_session} == "${SESSION_ID}" ]] || die 'Claude recall changed native session ID' +marker_hash_after=$(run_actor_command "${ATESPACE}" "${ACTOR}" "${STATE_FILE}" "${marker_command}" 30000) +[[ ${marker_hash_after} == "${marker_hash}" ]] || die 'marker hash changed across suspend/resume' +printf 'recall_turn=completed exit=0 same_session=yes nonce_match=yes marker_hash_match=yes\n' + +ate_ctx suspend actor "${ACTOR}" --atespace "${ATESPACE}" >/dev/null +wait_actor_state "${ATESPACE}" "${ACTOR}" ACTOR_STATE_SUSPENDED 180 >/dev/null + +provider_logs=$(kubectl_ctx -n mainloop-control logs deployment/round3-claude-provider --since=30m) +if ! worker_logs=$(timeout 300s kubectl --context "${CTX}" --kubeconfig "${KC}" -n "${ATESPACE}" \ + logs -l "ate.dev/worker-pool=${POOL}" --all-containers=true --since=30m); then + die 'could not fetch WorkerPool logs; refusing to report a zero credential-leak count' + exit 1 +fi +provider_leaks=$(printf '%s' "${provider_logs}" | ( + cd "${BACKEND_DIR}" + LIVE_PROOF_TOKEN_FILE="${CLAUDE_TOKEN_FILE}" UV_CACHE_DIR=/tmp/uv-cache \ + uv run --no-sync python "${SCRIPT_DIR}/count_token_prefix.py" +)) +worker_leaks=$(printf '%s' "${worker_logs}" | ( + cd "${BACKEND_DIR}" + LIVE_PROOF_TOKEN_FILE="${CLAUDE_TOKEN_FILE}" UV_CACHE_DIR=/tmp/uv-cache \ + uv run --no-sync python "${SCRIPT_DIR}/count_token_prefix.py" +)) +provider_leak_count=${provider_leaks#matches=} +worker_leak_count=${worker_leaks#matches=} +[[ ${provider_leak_count} == 0 ]] || die 'Claude credential prefix appeared in provider logs' +[[ ${worker_leak_count} == 0 ]] || die 'Claude credential prefix appeared in WorkerPool logs' +printf 'credential_leak_count_provider_logs=%s\ncredential_leak_count_worker_logs=%s\n' \ + "${provider_leak_count}" "${worker_leak_count}" + +# Egress policies have no delete; an empty rule set revokes the credential injection. +update_egress_policy '{"rules":[]}' +cleanup_provider +remove_shim_token_from_state "${STATE_FILE}" +COMPLETE=1 +printf 'CLAUDE_TURN_PROOF=PASS actor=%s/%s turn=completed recall=same-session marker=matched concurrent=409 suspend_ms=%s resume_ms=%s\n' \ + "${ATESPACE}" "${ACTOR}" "${suspend_ms}" "${resume_ms}" +printf 'actor remains SUSPENDED; lane namespace, pool, template, and snapshot are retained for review\n' diff --git a/spikes/substrate-workspace-adapter/live/cleanup-lane-a.sh b/spikes/substrate-workspace-adapter/live/cleanup-lane-a.sh new file mode 100755 index 0000000..84f13e3 --- /dev/null +++ b/spikes/substrate-workspace-adapter/live/cleanup-lane-a.sh @@ -0,0 +1,270 @@ +#!/usr/bin/env bash +# Remove only the two uniquely named Lane A resources, after validating the +# gate5 state-file identity and any actor/template/pool UIDs still present. +set -euo pipefail + +SCRIPT_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd) +# shellcheck source=common.sh +source "${SCRIPT_DIR}/common.sh" + +lane=${1-} +case "${lane}" in +claude) + ATESPACE="lane-a-claude-20260924" + POOL="lane-a-claude-20260924" + ACTOR=claude-live-proof + VERSION="lane-a-claude-20260924" + STATE_FILE=${STATE_ROOT}/claude-gate5-state.json + ;; +durable) + ATESPACE="lane-a-durable-20260924" + POOL="lane-a-durable-20260924" + ACTOR=repo-live-proof + VERSION="lane-a-durable-20260924" + STATE_FILE=${STATE_ROOT}/durable-gate5-state.json + ;; +*) + die 'usage: cleanup-lane-a.sh claude|durable' + exit 2 + ;; +esac +TEMPLATE=live-agent-gate-${VERSION} + +if [[ ! -f ${STATE_FILE} ]]; then + die "refusing cleanup without the lane's gate5 state file: ${STATE_FILE}" + exit 1 +fi +state_identity=$(jq -r '[.context,.atespace,.worker_pool,.template_name,.actor_name] | @tsv' "${STATE_FILE}") +expected_identity=$(printf '%s\t%s\t%s\t%s\t%s' "${CTX}" "${ATESPACE}" "${POOL}" "${TEMPLATE}" "${ACTOR}") +[[ ${state_identity} == "${expected_identity}" ]] || { + die 'gate5 state identity does not match the requested cleanup lane' + exit 1 +} + +actor_err=$(mktemp "${STATE_ROOT}/cleanup-actor.XXXXXX") +template_err=$(mktemp "${STATE_ROOT}/cleanup-template.XXXXXX") +pool_err=$(mktemp "${STATE_ROOT}/cleanup-pool.XXXXXX") +provider_err=$(mktemp "${STATE_ROOT}/cleanup-provider.XXXXXX") +namespace_err=$(mktemp "${STATE_ROOT}/cleanup-namespace.XXXXXX") +declare -A provider_uids=() +cleanup_tmp() { + rm -f "${actor_err}" "${template_err}" "${pool_err}" "${provider_err}" "${namespace_err}" +} +trap cleanup_tmp EXIT + +recorded_namespace_uid=$(jq -r '.namespace_uid // empty' "${STATE_FILE}") +if [[ -z ${recorded_namespace_uid} ]]; then + die "gate5 state has no Namespace UID; skipping namespace deletion for ${ATESPACE}" + exit 1 +fi +namespace_exists=0 +# shellcheck disable=SC2310 # kubectl_ctx has one command; its status distinguishes NotFound from errors. +if namespace_json=$(kubectl_ctx get namespace "${ATESPACE}" -o json 2>"${namespace_err}"); then + live_namespace_uid=$(jq -r '.metadata.uid // empty' <<<"${namespace_json}") + if [[ -z ${live_namespace_uid} || ${live_namespace_uid} != "${recorded_namespace_uid}" ]]; then + die "Namespace UID differs from gate5 state; skipping namespace deletion for ${ATESPACE}" + exit 1 + fi + namespace_exists=1 +elif grep -Eiq 'not.?found|code = NotFound|NOT_FOUND|does not exist' "${namespace_err}"; then + : +else + cat "${namespace_err}" >&2 + die "could not verify Namespace UID; skipping namespace deletion for ${ATESPACE}" + exit 1 +fi + +if [[ ${lane} == claude && ${SKIP_PROVIDER_CLEANUP:-0} != 1 ]]; then + for resource in secret/claude-oauth service/credprovider deployment/round3-claude-provider serviceaccount/round3-claude-provider networkpolicy/round3-claude-provider; do + kind=${resource%%/*} + name=${resource#*/} + if object_json=$(timeout 300s kubectl --context "${CTX}" --kubeconfig "${KC}" \ + -n mainloop-control get "${kind}" "${name}" -o json 2>"${provider_err}"); then + label=$(jq -r '.metadata.labels["proof.mainloop.dev/lane"] // empty' <<<"${object_json}") + [[ ${label} == lane-a-live-proof ]] || { + die "${resource} exists without Lane A ownership label; leaving all provider resources intact" + exit 1 + } + uid=$(jq -r '.metadata.uid // empty' <<<"${object_json}") + [[ -n ${uid} ]] || { + die "${resource} has no readable UID; leaving all provider resources intact" + exit 1 + } + provider_uids["${resource}"]=${uid} + elif ! grep -Eiq 'not.?found|code = NotFound|NOT_FOUND|does not exist' "${provider_err}"; then + cat "${provider_err}" >&2 + die "could not reconcile ${resource}; no lane resources were deleted" + exit 1 + fi + done +fi + +actor_json= +actor_exists=0 +actor_state= +if actor_json=$(timeout 120s "${ATE_CLI}" --context "${CTX}" --kubeconfig "${KC}" \ + get actor "${ACTOR}" --atespace "${ATESPACE}" -o json 2>"${actor_err}"); then + recorded_uid=$(actor_uid_from_state "${STATE_FILE}") + actual_uid=$(jq -r '.metadata.uid // empty' <<<"${actor_json}") + [[ -n ${recorded_uid} && ${actual_uid} == "${recorded_uid}" ]] || { + die 'actor UID differs from gate5 state; no lane resources were changed' + exit 1 + } + actor_state=$(jq -r '.status.state // empty' <<<"${actor_json}") + actor_exists=1 +elif ! grep -Eiq 'not.?found|code = NotFound|NOT_FOUND|does not exist' "${actor_err}"; then + cat "${actor_err}" >&2 + die 'could not reconcile actor ownership; no lane resources were changed' + exit 1 +fi + +template_json= +template_exists=0 +if template_json=$(timeout 120s "${ATE_CLI}" --context "${CTX}" --kubeconfig "${KC}" \ + get actor-template "${TEMPLATE}" --atespace "${ATESPACE}" -o json 2>"${template_err}"); then + recorded_template_uid=$(jq -r '.template_uid // empty' "${STATE_FILE}") + actual_template_uid=$(jq -r '.metadata.uid // empty' <<<"${template_json}") + if [[ -z ${recorded_template_uid} || ${actual_template_uid} != "${recorded_template_uid}" ]]; then + die 'ActorTemplate UID differs from gate5 state; no lane resources were changed' + exit 1 + fi + template_exists=1 +elif ! grep -Eiq 'not.?found|code = NotFound|NOT_FOUND|does not exist' "${template_err}"; then + cat "${template_err}" >&2 + die 'could not reconcile ActorTemplate ownership; no lane resources were changed' + exit 1 +fi + +pool_exists=0 +if pool_json=$(timeout 300s kubectl --context "${CTX}" --kubeconfig "${KC}" \ + -n "${ATESPACE}" get workerpool "${POOL}" -o json 2>"${pool_err}"); then + pool_name=$(jq -r '.metadata.name // empty' <<<"${pool_json}") + pool_label=$(jq -r '.metadata.labels.workload // empty' <<<"${pool_json}") + [[ ${pool_name} == "${POOL}" && ${pool_label} == "${POOL}" ]] || { + die 'WorkerPool labels differ from the lane identity; no lane resources were changed' + exit 1 + } + pool_exists=1 +elif ! grep -Eiq 'not.?found|code = NotFound|NOT_FOUND|does not exist' "${pool_err}"; then + cat "${pool_err}" >&2 + die 'could not reconcile WorkerPool ownership; no lane resources were changed' + exit 1 +fi + +atespace_exists=0 +if timeout 120s "${ATE_CLI}" --context "${CTX}" --kubeconfig "${KC}" \ + get atespace "${ATESPACE}" -o json 2>"${actor_err}" >/dev/null; then + atespace_exists=1 +elif ! grep -Eiq 'not.?found|code = NotFound|NOT_FOUND|does not exist' "${actor_err}"; then + cat "${actor_err}" >&2 + die 'could not reconcile atespace; no lane resources were changed' + exit 1 +fi + +if ((actor_exists == 1)); then + if [[ ${actor_state} != ACTOR_STATE_SUSPENDED ]]; then + ate_ctx suspend actor "${ACTOR}" --atespace "${ATESPACE}" >/dev/null + wait_actor_state "${ATESPACE}" "${ACTOR}" ACTOR_STATE_SUSPENDED 180 >/dev/null + fi + ate_ctx delete actor "${ACTOR}" --atespace "${ATESPACE}" --any-state >/dev/null +fi + +if [[ ${lane} == claude && ${SKIP_PROVIDER_CLEANUP:-0} != 1 ]]; then + # Reconcile every provider UID again before deleting anything. DeleteOptions + # below still fences the request against a replacement after this check. + for resource in secret/claude-oauth service/credprovider deployment/round3-claude-provider serviceaccount/round3-claude-provider networkpolicy/round3-claude-provider; do + kind=${resource%%/*} + name=${resource#*/} + # shellcheck disable=SC2310 # kubectl_ctx has one command; its status is checked below. + if object_json=$(kubectl_ctx -n mainloop-control get "${kind}" "${name}" -o json 2>"${provider_err}"); then + live_uid=$(jq -r '.metadata.uid // empty' <<<"${object_json}") + live_label=$(jq -r '.metadata.labels["proof.mainloop.dev/lane"] // empty' <<<"${object_json}") + recorded_uid=${provider_uids["${resource}"]-} + if [[ -z ${live_uid} || -z ${recorded_uid} || ${live_uid} != "${recorded_uid}" || ${live_label} != lane-a-live-proof ]]; then + die "provider cleanup skipped ${resource}: UID or ownership changed since verification" + exit 1 + fi + elif grep -Eiq 'not.?found|code = NotFound|NOT_FOUND|does not exist' "${provider_err}"; then + if [[ -n ${provider_uids["${resource}"]-} ]]; then + die "provider cleanup skipped ${resource}: its UID could not be read before deletion" + exit 1 + fi + continue + else + cat "${provider_err}" >&2 + die "provider cleanup skipped ${resource}: its UID could not be read before deletion" + exit 1 + fi + done + + for resource in networkpolicy/round3-claude-provider deployment/round3-claude-provider service/credprovider serviceaccount/round3-claude-provider secret/claude-oauth; do + recorded_uid=${provider_uids["${resource}"]-} + [[ -n ${recorded_uid} ]] || continue + kind=${resource%%/*} + name=${resource#*/} + case ${kind} in + networkpolicy) + delete_path="/apis/networking.k8s.io/v1/namespaces/mainloop-control/networkpolicies/${name}" + ;; + deployment) + delete_path="/apis/apps/v1/namespaces/mainloop-control/deployments/${name}" + ;; + service) + delete_path="/api/v1/namespaces/mainloop-control/services/${name}" + ;; + serviceaccount) + delete_path="/api/v1/namespaces/mainloop-control/serviceaccounts/${name}" + ;; + secret) + delete_path="/api/v1/namespaces/mainloop-control/secrets/${name}" + ;; + *) + die "unsupported provider resource kind ${kind}" + exit 1 + ;; + esac + # shellcheck disable=SC2310 # kubectl_ctx has one command; delete failure is handled below. + if ! kubectl_ctx delete --raw="${delete_path}" -f - </dev/null; then + die "timed out waiting for provider Deployment ${name} deletion" + exit 1 + fi + done +fi + +if ((template_exists == 1)); then + ate_ctx delete actor-template "${TEMPLATE}" --atespace "${ATESPACE}" >/dev/null +fi +if ((pool_exists == 1)); then + kubectl_ctx -n "${ATESPACE}" delete workerpool "${POOL}" --wait=true --timeout=120s >/dev/null +fi +if ((atespace_exists == 1)); then + ate_ctx delete atespace "${ATESPACE}" >/dev/null +fi +if ((namespace_exists == 1)); then + # shellcheck disable=SC2310 # kubectl_ctx has one command; recheck failure skips namespace deletion. + if namespace_json=$(kubectl_ctx get namespace "${ATESPACE}" -o json 2>"${namespace_err}"); then + live_namespace_uid=$(jq -r '.metadata.uid // empty' <<<"${namespace_json}") + else + cat "${namespace_err}" >&2 + die "could not recheck Namespace UID; skipping namespace deletion for ${ATESPACE}" + exit 1 + fi + if [[ -z ${live_namespace_uid} || ${live_namespace_uid} != "${recorded_namespace_uid}" ]]; then + die "Namespace UID changed during cleanup; skipping namespace deletion for ${ATESPACE}" + exit 1 + fi + kubectl_ctx delete --raw="/api/v1/namespaces/${ATESPACE}" -f - </dev/null +fi +rm -f "${STATE_FILE}" "${BEFORE_MANIFEST}" "${AFTER_MANIFEST}" +printf 'CLEANUP=PASS lane=%s namespace=%s provider_secret_and_service=removed_if_owned\n' "${lane}" "${ATESPACE}" diff --git a/spikes/substrate-workspace-adapter/live/common.sh b/spikes/substrate-workspace-adapter/live/common.sh new file mode 100755 index 0000000..59db9ea --- /dev/null +++ b/spikes/substrate-workspace-adapter/live/common.sh @@ -0,0 +1,256 @@ +#!/usr/bin/env bash + +CTX='kind-substrate-preview' +KC=/tmp/substrate-preview-kubeconfig +ACTOR_IMAGE=${ACTOR_IMAGE:-localhost:5001/live-agent-gate@sha256:eb819c5ae18829a18972fd7be9ec44e0f3dbef08e554f8e0dfc1d6240f962f5d} +ROUTER_PORT=${ROUTER_PORT:-18091} +STATE_ROOT=${LIVE_PROOF_STATE_DIR:-/tmp/mainloop-substrate-live-proof} +# Shared with the durable proof and its cleanup script. +# shellcheck disable=SC2034 # cleanup-lane-a.sh uses this shared manifest path. +BEFORE_MANIFEST=${STATE_ROOT}/durable-before.tsv +# shellcheck disable=SC2034 # cleanup-lane-a.sh uses this shared manifest path. +AFTER_MANIFEST=${STATE_ROOT}/durable-after.tsv +# Fork patched-next 0f9635ae worker build (rebased on upstream, keeps the durable-owner fix). +WORKER_IMAGE_DIGEST=sha256:9cff9f35f68bcad9f37ce3574f2e0a4638368475e4b49b4de606e38d9bbcba92 +WORKER_IMAGE_REFERENCE=localhost:5001/ateom-gvisor@${WORKER_IMAGE_DIGEST} +LIVE_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd) +REPO_ROOT=$(cd -- "${LIVE_DIR}/../../.." && pwd) +BACKEND_DIR=${REPO_ROOT}/backend +MANIFEST=${REPO_ROOT}/spikes/substrate-workspace-adapter/k8s/actor-template.yaml.tmpl +HTTP_HELPER=${LIVE_DIR}/shim_request.py +ATE_CLI=${SUBSTRATE_SRC:-${HOME}/dev/substrate}/bin/kubectl-ate +PORT_FORWARD_PID= +PORT_FORWARD_LOG= + +die() { + printf 'ERROR: %s\n' "$*" >&2 + return 1 +} + +kubectl_ctx() { + timeout 300s kubectl --context "${CTX}" --kubeconfig "${KC}" "$@" +} + +preflight_provider_resources_absent() { + local resource kind name get_result + for resource in secret/claude-oauth service/credprovider deployment/round3-claude-provider \ + serviceaccount/round3-claude-provider networkpolicy/round3-claude-provider; do + kind=${resource%%/*} + name=${resource#*/} + if get_result=$(timeout 300s kubectl --context "${CTX}" --kubeconfig "${KC}" \ + -n mainloop-control get "${kind}" "${name}" -o name 2>&1); then + die "${resource} already exists in mainloop-control; refusing to replace shared provider state" + return 1 + elif [[ ${get_result} =~ [Nn]ot[Ff]ound|not\ found|does\ not\ exist ]]; then + : + else + printf '%s\n' "${get_result}" >&2 + die "could not establish whether ${resource} exists; refusing to write provider state" + return 1 + fi + done +} + +ate_ctx() { + timeout 120s "${ATE_CLI}" --context "${CTX}" --kubeconfig "${KC}" "$@" +} + +require_worker_image() { + if [[ ! ${WORKER_IMAGE-} =~ ^[^[:space:]]+@${WORKER_IMAGE_DIGEST}$ ]]; then + die "set WORKER_IMAGE to the pinned fork WorkerPool build with the durable-owner fix, such as ${WORKER_IMAGE_REFERENCE}" + fi +} + +prepare_state_file() { + local state_file=$1 atespace=$2 namespace_result atespace_result + mkdir -p "${STATE_ROOT}" + chmod 700 "${STATE_ROOT}" + if namespace_result=$(kubectl_ctx get namespace "${atespace}" -o name 2>&1); then + die "namespace ${atespace} already exists; refusing to adopt or overwrite lane resources" + elif [[ ! ${namespace_result} =~ [Nn]ot[Ff]ound|not\ found|does\ not\ exist ]]; then + printf '%s\n' "${namespace_result}" >&2 + die "could not establish whether namespace ${atespace} exists; refusing to continue" + fi + if atespace_result=$(ate_ctx get atespace "${atespace}" -o json 2>&1); then + die "atespace ${atespace} already exists; refusing to adopt or overwrite lane resources" + elif [[ ! ${atespace_result} =~ [Nn]ot[Ff]ound|not\ found|does\ not\ exist ]]; then + printf '%s\n' "${atespace_result}" >&2 + die "could not establish whether atespace ${atespace} exists; refusing to continue" + fi + rm -f "${state_file}" + umask 077 +} + +gate5_setup() { + local atespace=$1 pool=$2 actor=$3 version=$4 state_file=$5 + local egress_host=$6 + require_worker_image + ( + cd "${BACKEND_DIR}" || exit + UV_CACHE_DIR=/tmp/uv-cache uv run --no-sync python scripts/gate5_setup.py \ + --context "${CTX}" \ + --kubeconfig "${KC}" \ + --ate-cli "${SUBSTRATE_SRC:-${HOME}/dev/substrate}/bin/kubectl-ate" \ + --worker-image "${WORKER_IMAGE}" \ + --atespace "${atespace}" \ + --worker-pool "${pool}" \ + --template-version "${version}" \ + --actor-name "${actor}" \ + --image "${ACTOR_IMAGE}" \ + --manifest "${MANIFEST}" \ + --state-file "${state_file}" \ + --egress-hostname "${egress_host}" + ) +} + +port_forward_ready() { + timeout 1 bash -c "exec 3<>/dev/tcp/127.0.0.1/${ROUTER_PORT}" >/dev/null 2>&1 +} + +start_router() { + if port_forward_ready; then + die "127.0.0.1:${ROUTER_PORT} is already in use" + fi + PORT_FORWARD_LOG=$(mktemp "${STATE_ROOT}/router-port-forward.XXXXXX.log") + # Background kubectl itself, not the kubectl_ctx function: $! must be the + # process stop_router kills, and the tunnel must outlive kubectl_ctx's timeout. + kubectl --context "${CTX}" --kubeconfig "${KC}" -n ate-system port-forward \ + --address 127.0.0.1 service/atenet-router "${ROUTER_PORT}:8081" >"${PORT_FORWARD_LOG}" 2>&1 & + PORT_FORWARD_PID=$! + for _ in $(seq 1 80); do + if port_forward_ready; then + printf 'router tunnel ready on 127.0.0.1:%s\n' "${ROUTER_PORT}" + return 0 + fi + if ! kill -0 "${PORT_FORWARD_PID}" 2>/dev/null; then + cat "${PORT_FORWARD_LOG}" >&2 + die 'router port-forward exited before becoming ready' + fi + sleep 0.25 + done + cat "${PORT_FORWARD_LOG}" >&2 + die 'router port-forward did not become ready within 20 seconds' +} + +stop_router() { + if [[ -n ${PORT_FORWARD_PID-} ]] && kill -0 "${PORT_FORWARD_PID}" 2>/dev/null; then + kill "${PORT_FORWARD_PID}" 2>/dev/null || true + wait "${PORT_FORWARD_PID}" 2>/dev/null || true + fi + PORT_FORWARD_PID= + if [[ -n ${PORT_FORWARD_LOG-} ]]; then + rm -f "${PORT_FORWARD_LOG}" + PORT_FORWARD_LOG= + fi +} + +shim_request() { + local atespace=$1 actor=$2 state_file=$3 request=$4 + printf '%s\n' "${request}" | ( + cd "${BACKEND_DIR}" || exit + LIVE_PROOF_ATESPACE=${atespace} \ + LIVE_PROOF_ACTOR=${actor} \ + LIVE_PROOF_ROUTER_PORT=${ROUTER_PORT} \ + LIVE_PROOF_STATE_FILE=${state_file} \ + UV_CACHE_DIR=/tmp/uv-cache uv run --no-sync python "${HTTP_HELPER}" + ) +} + +run_actor_command() { + local atespace=$1 actor=$2 state_file=$3 command=$4 timeout_ms=${5:-180000} + local request response status run_id result deadline + if ! request=$(jq -cn --arg command "${command}" --argjson timeout "${timeout_ms}" \ + '{method:"POST",path:"/run",authenticated:true,body:{command:$command,timeout_ms:$timeout}}'); then + die 'could not encode actor /run request' + return 1 + fi + if ! response=$(shim_request "${atespace}" "${actor}" "${state_file}" "${request}"); then + die 'actor /run request failed before returning a response' + return 1 + fi + status=$(jq -r '.status // 0' <<<"${response}") + if [[ ${status} != 202 ]]; then + die "actor /run was not accepted (HTTP ${status})" + return 1 + fi + run_id=$(jq -r '.body.id // empty' <<<"${response}") + if [[ -z ${run_id} ]]; then + die 'actor /run response omitted its id' + return 1 + fi + deadline=$((SECONDS + (timeout_ms / 1000) + 30)) + while ((SECONDS < deadline)); do + if ! request=$(jq -cn --arg id "${run_id}" '{method:"GET",path:("/run/"+$id),authenticated:true}'); then + die 'could not encode actor /run status request' + return 1 + fi + if ! response=$(shim_request "${atespace}" "${actor}" "${state_file}" "${request}"); then + die 'actor /run status request failed before returning a response' + return 1 + fi + status=$(jq -r '.status // 0' <<<"${response}") + if [[ ${status} != 200 ]]; then + die "actor /run status fetch failed (HTTP ${status})" + return 1 + fi + result=$(jq -r '.body.status // empty' <<<"${response}") + if [[ -z ${result} ]]; then + die 'actor /run status response omitted its state' + return 1 + fi + case "${result}" in + completed) + exit_code=$(jq -r '.body.exit_code' <<<"${response}") + if [[ ${exit_code} != 0 ]]; then + die 'actor /run completed with a nonzero exit code' + return 1 + fi + jq -r '.body.output // ""' <<<"${response}" + return 0 + ;; + failed | timed_out | interrupted) + exit_code=$(jq -r '.body.exit_code // "unknown"' <<<"${response}") + die "actor /run ended with status=${result} exit_code=${exit_code}" + return 1 + ;; + *) + # Nonterminal run states continue through the bounded polling loop. + ;; + esac + sleep 1 + done + die "actor /run ${run_id} exceeded its bounded wait" + return 1 +} + +get_actor_json() { + local atespace=$1 actor=$2 + ate_ctx get actor "${actor}" --atespace "${atespace}" -o json +} + +wait_actor_state() { + local atespace=$1 actor=$2 expected=$3 timeout_s=${4:-120} actor_json state + local deadline=$((SECONDS + timeout_s)) + while ((SECONDS < deadline)); do + if actor_json=$(get_actor_json "${atespace}" "${actor}" 2>/dev/null); then + state=$(jq -r '.status.state // empty' <<<"${actor_json}") + if [[ ${state} == "${expected}" ]]; then + printf '%s\n' "${actor_json}" + return 0 + fi + fi + sleep 1 + done + die "actor ${atespace}/${actor} did not reach ${expected} within ${timeout_s}s" +} + +actor_uid_from_state() { + jq -r '.actor_uid // empty' "$1" +} + +remove_shim_token_from_state() { + local state_file=$1 tmp_file="$1.redacted" + jq 'del(.shim_token)' "${state_file}" >"${tmp_file}" + chmod 600 "${tmp_file}" + mv -f "${tmp_file}" "${state_file}" +} diff --git a/spikes/substrate-workspace-adapter/live/count_token_prefix.py b/spikes/substrate-workspace-adapter/live/count_token_prefix.py new file mode 100644 index 0000000..0d1cebd --- /dev/null +++ b/spikes/substrate-workspace-adapter/live/count_token_prefix.py @@ -0,0 +1,35 @@ +#!/usr/bin/env python3 +"""Count credential-prefix occurrences in stdin, emitting a count only.""" + +import os +import sys + + +def main() -> int: + token_path = os.environ.get("LIVE_PROOF_TOKEN_FILE") + if not token_path: + print("token file path is required", file=sys.stderr) + return 2 + try: + with open(token_path, "rb") as token_handle: + token = token_handle.read().strip() + except OSError: + print("token file is unavailable", file=sys.stderr) + return 2 + if not token: + print("token file is empty", file=sys.stderr) + return 2 + + prefix = token[:64] + carry = b"" + matches = 0 + for chunk in iter(lambda: sys.stdin.buffer.read(65536), b""): + data = carry + chunk + matches += data.count(prefix) + carry = data[-(len(prefix) - 1) :] if len(prefix) > 1 else b"" + print(f"matches={matches}") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/spikes/substrate-workspace-adapter/live/durable_restore_proof.sh b/spikes/substrate-workspace-adapter/live/durable_restore_proof.sh new file mode 100755 index 0000000..17cef8f --- /dev/null +++ b/spikes/substrate-workspace-adapter/live/durable_restore_proof.sh @@ -0,0 +1,180 @@ +#!/usr/bin/env bash +# Snapshot a real repository and restore it after every worker in this lane's +# pool has been deleted. Run only through the supervisor's explicit cluster lane. +set -euo pipefail + +SCRIPT_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd) +# shellcheck source=common.sh +source "${SCRIPT_DIR}/common.sh" + +ATESPACE=lane-a-durable-20260924 +POOL=lane-a-durable-20260924 +ACTOR=repo-live-proof +STATE_FILE=${STATE_ROOT}/durable-gate5-state.json +BEFORE_MANIFEST=${STATE_ROOT}/durable-before.tsv +AFTER_MANIFEST=${STATE_ROOT}/durable-after.tsv +COMPLETE=0 +CLEANUP_ARMED=0 + +finish() { + local status=$? + stop_router + if ((status != 0 && COMPLETE == 0 && CLEANUP_ARMED == 1)); then + printf 'run failed; attempting cleanup of the owned durable lane\n' >&2 + "${SCRIPT_DIR}/cleanup-lane-a.sh" durable || printf 'cleanup did not complete; inspect %s\n' "${STATE_FILE}" >&2 + fi + return "${status}" +} + +require_worker_image +prepare_state_file "${STATE_FILE}" "${ATESPACE}" +rm -f "${BEFORE_MANIFEST}" "${AFTER_MANIFEST}" +if timeout 300s kubectl --context "${CTX}" --kubeconfig "${KC}" \ + get workerpool "${POOL}" -n "${ATESPACE}" -o name >/dev/null 2>&1; then + die "WorkerPool ${ATESPACE}/${POOL} already exists; refusing to adopt it" +fi +trap finish EXIT INT TERM + +printf 'creating the durable restore actor from the product template\n' +# shellcheck disable=SC2310 # handle gate5_setup's returned status and persisted ownership below. +if gate5_setup "${ATESPACE}" "${POOL}" "${ACTOR}" lane-a-durable-20260924 "${STATE_FILE}" github.com; then + CLEANUP_ARMED=1 +else + setup_status=$? + if jq -e '.namespace_uid | strings | length > 0' "${STATE_FILE}" >/dev/null 2>&1; then + CLEANUP_ARMED=1 + fi + exit "${setup_status}" +fi +start_router + +uid=$(run_actor_command "${ATESPACE}" "${ACTOR}" "${STATE_FILE}" 'id -u' 30000) +if [[ ${uid} != 10001 ]]; then + die "actor runtime uid is ${uid}; durable owner proof requires image uid 10001" +fi +printf 'actor runtime uid=%s\n' "${uid}" + +setup_command=$( + cat <<'EOF' +set -eu +git clone --depth=1 --quiet https://github.com/octocat/Hello-World.git /work/repo/source +printf 'durable-owner-check\n' > /work/repo/lane-a-owned-by-10001.txt +chmod 0644 /work/repo/lane-a-owned-by-10001.txt +printf 'durable-private-check\n' > /work/repo/lane-a-mode-0600.txt +chmod 0600 /work/repo/lane-a-mode-0600.txt +test "$(stat -c %u /work/repo/lane-a-owned-by-10001.txt)" = 10001 +test "$(stat -c %g /work/repo/lane-a-owned-by-10001.txt)" = 10001 +test "$(stat -c %a /work/repo/lane-a-mode-0600.txt)" = 600 +test -s /work/repo/source/README +git -C /work/repo/source rev-parse HEAD +EOF +) +repo_commit=$(run_actor_command "${ATESPACE}" "${ACTOR}" "${STATE_FILE}" "${setup_command}" 300000) +printf 'repository commit=%s\n' "${repo_commit}" + +manifest_command=$( + cat <<'EOF' +set -eu +# The shim writes its own run records, including this command's, under runs/. +find /work/repo -path /work/repo/.mainloop/exec-shim/runs -prune -o -type f -print | LC_ALL=C sort | while IFS= read -r path; do + relative=${path#/work/repo/} + owner=$(stat -c %u "$path") + group=$(stat -c %g "$path") + mode=$(stat -c %a "$path") + digest=$(sha256sum "$path" | cut -d ' ' -f 1) + printf '%s\t%s\t%s\t%s\t%s\n' "$relative" "$owner" "$group" "$mode" "$digest" +done +EOF +) +before=$(run_actor_command "${ATESPACE}" "${ACTOR}" "${STATE_FILE}" "${manifest_command}" 120000) +printf '%s\n' "${before}" >"${BEFORE_MANIFEST}" +chmod 600 "${BEFORE_MANIFEST}" +before_count=$(wc -l <"${BEFORE_MANIFEST}" | tr -d ' ') +printf 'manifest_before files=%s owner_file_uid=10001 private_file_mode=600\n' "${before_count}" + +started_ns=$(date +%s%N) +ate_ctx suspend actor "${ACTOR}" --atespace "${ATESPACE}" >/dev/null +suspended_json=$(wait_actor_state "${ATESPACE}" "${ACTOR}" ACTOR_STATE_SUSPENDED 180) +suspend_ms=$((($(date +%s%N) - started_ns) / 1000000)) +snapshot_uri=$(jq -r '.status.externalSnapshot.snapshotUri // .status.externalSnapshot.snapshot_uri // empty' <<<"${suspended_json}") +snapshot_scope=$(jq -r '.status.externalSnapshot.contentScope // .status.externalSnapshot.content_scope // empty' <<<"${suspended_json}") +[[ -n ${snapshot_uri} ]] || die 'suspend completed without an external snapshot URI' +[[ ${snapshot_scope} == SNAPSHOT_CONTENT_SCOPE_FULL || ${snapshot_scope} == 1 ]] || die "snapshot scope was not FULL: ${snapshot_scope}" +printf 'suspend_ms=%s snapshot_scope=FULL\n' "${suspend_ms}" + +old_pods=$(kubectl_ctx -n "${ATESPACE}" get pods -l "ate.dev/worker-pool=${POOL}" -o json) +old_count=$(jq '.items | length' <<<"${old_pods}") +[[ ${old_count} == 2 ]] || die "expected 2 workers in the owned pool, found ${old_count}" +old_uids=$(jq -r '.items[].metadata.uid' <<<"${old_pods}" | sort) +printf 'deleting owned worker pods after completed suspend:\n' +jq -r '.items[] | " \(.metadata.name) uid=\(.metadata.uid)"' <<<"${old_pods}" +kubectl_ctx -n "${ATESPACE}" delete pod -l "ate.dev/worker-pool=${POOL}" --wait=true --timeout=120s >/dev/null + +replacement_json= +for _ in $(seq 1 180); do + candidate=$(kubectl_ctx -n "${ATESPACE}" get pods -l "ate.dev/worker-pool=${POOL}" -o json) + count=$(jq '[.items[] | select(any(.status.conditions[]?; .type == "Ready" and .status == "True"))] | length' <<<"${candidate}") + if [[ ${count} == 2 ]]; then + replacement_json=${candidate} + break + fi + sleep 1 +done +[[ -n ${replacement_json} ]] || die 'replacement WorkerPool pods did not become Ready within 180 seconds' +new_uids=$(jq -r '.items[].metadata.uid' <<<"${replacement_json}" | sort) +while IFS= read -r new_uid; do + [[ -n ${new_uid} ]] || continue + if grep -Fxq "${new_uid}" <<<"${old_uids}"; then + die 'a replacement worker reused an old pod UID; worker-loss proof is inconclusive' + fi +done <<<"${new_uids}" +printf 'replacement workers ready with new pod UIDs\n' + +started_ns=$(date +%s%N) +# Ready pods register as free workers a little later; retry only that refusal. +resume_ok=0 +for _ in $(seq 1 60); do + if resume_err=$(timeout 120s "${ATE_CLI}" --context "${CTX}" --kubeconfig "${KC}" \ + resume actor "${ACTOR}" --atespace "${ATESPACE}" 2>&1 >/dev/null); then + resume_ok=1 + break + fi + grep -q 'no free workers available' <<<"${resume_err}" || die "resume failed: ${resume_err}" + sleep 2 +done +((resume_ok == 1)) || die 'no replacement worker registered as free within 120 seconds' +resumed_json=$(wait_actor_state "${ATESPACE}" "${ACTOR}" ACTOR_STATE_RUNNING 240) +for _ in $(seq 1 60); do + health=$(shim_request "${ATESPACE}" "${ACTOR}" "${STATE_FILE}" \ + '{"method":"GET","path":"/healthz","authenticated":false}') + health_status=$(jq -r '.status // 0' <<<"${health}") + if [[ ${health_status} == 200 ]]; then + break + fi + sleep 1 +done +health_status=$(jq -r '.status // 0' <<<"${health}") +[[ ${health_status} == 200 ]] || die 'restored actor did not pass /healthz' +resume_ms=$((($(date +%s%N) - started_ns) / 1000000)) +assigned_pod=$(jq -r '.status.workerAssignment.workerPod // empty' <<<"${resumed_json}") +[[ -n ${assigned_pod} ]] || die 'restored actor has no worker-pod assignment' +replacement_names=$(jq -r '.items[].metadata.name' <<<"${replacement_json}") +grep -Fxq "${assigned_pod}" <<<"${replacement_names}" || die 'actor resumed on a pod outside the replacement worker set' +printf 'resume_ms=%s worker_pod=%s health=200\n' "${resume_ms}" "${assigned_pod}" + +after=$(run_actor_command "${ATESPACE}" "${ACTOR}" "${STATE_FILE}" "${manifest_command}" 120000) +printf '%s\n' "${after}" >"${AFTER_MANIFEST}" +chmod 600 "${AFTER_MANIFEST}" +after_count=$(wc -l <"${AFTER_MANIFEST}" | tr -d ' ') +diff -u "${BEFORE_MANIFEST}" "${AFTER_MANIFEST}" +printf 'manifest_after files=%s exact_match=yes\n' "${after_count}" +printf 'manifest rows (relative path, uid, gid, mode, sha256):\n' +cat "${AFTER_MANIFEST}" + +ate_ctx suspend actor "${ACTOR}" --atespace "${ATESPACE}" >/dev/null +wait_actor_state "${ATESPACE}" "${ACTOR}" ACTOR_STATE_SUSPENDED 180 >/dev/null +remove_shim_token_from_state "${STATE_FILE}" +COMPLETE=1 +printf 'DURABLE_RESTORE_PROOF=PASS actor=%s/%s suspend_ms=%s resume_ms=%s worker_loss=yes files=%s\n' \ + "${ATESPACE}" "${ACTOR}" "${suspend_ms}" "${resume_ms}" "${after_count}" +printf 'actor remains SUSPENDED; lane resources are retained for review\n' diff --git a/spikes/substrate-workspace-adapter/live/shim_request.py b/spikes/substrate-workspace-adapter/live/shim_request.py new file mode 100644 index 0000000..4e13a23 --- /dev/null +++ b/spikes/substrate-workspace-adapter/live/shim_request.py @@ -0,0 +1,77 @@ +#!/usr/bin/env python3 +"""Send one bounded HTTP request through the explicitly forwarded Substrate router. + +The request document is read from stdin. The shim bearer token is loaded from the +gate5 state file by path and is never written to stdout or stderr. +""" + +import http.client +import json +import os +import sys + + +def main() -> int: + try: + request = json.load(sys.stdin) + atespace = os.environ["LIVE_PROOF_ATESPACE"] + actor = os.environ["LIVE_PROOF_ACTOR"] + port = int(os.environ["LIVE_PROOF_ROUTER_PORT"]) + state_file = os.environ["LIVE_PROOF_STATE_FILE"] + method = request["method"] + path = request["path"] + authenticated = bool(request.get("authenticated", True)) + body = request.get("body") + except (KeyError, TypeError, ValueError, json.JSONDecodeError): + print(json.dumps({"error": "invalid request configuration"})) + return 2 + + if ( + method not in {"GET", "POST"} + or not isinstance(path, str) + or not path.startswith("/") + ): + print(json.dumps({"error": "invalid request target"})) + return 2 + + headers = {"Connection": "close"} + if authenticated: + try: + with open(state_file, encoding="utf-8") as state_handle: + token = json.load(state_handle)["shim_token"] + except (OSError, KeyError, TypeError, json.JSONDecodeError): + print(json.dumps({"error": "shim state unavailable"})) + return 2 + headers["Authorization"] = f"Bearer {token}" + + body_bytes = None + if body is not None: + body_bytes = json.dumps(body, separators=(",", ":")).encode("utf-8") + headers["Content-Type"] = "application/json" + + connection = http.client.HTTPConnection("127.0.0.1", port, timeout=20) + connection.set_tunnel( + "actor-upstream:8090", + headers={"ate-target-actor": f"{atespace}/{actor}"}, + ) + try: + connection.request(method, path, body=body_bytes, headers=headers) + response = connection.getresponse() + response_bytes = response.read() + except (OSError, http.client.HTTPException) as exc: + print(json.dumps({"transport_error": type(exc).__name__})) + return 1 + finally: + connection.close() + + response_text = response_bytes.decode("utf-8", errors="replace") + try: + response_body = json.loads(response_text) + except json.JSONDecodeError: + response_body = response_text + print(json.dumps({"status": response.status, "body": response_body})) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/spikes/substrate-workspace-adapter/tests/exec-shim.test.js b/spikes/substrate-workspace-adapter/tests/exec-shim.test.js new file mode 100644 index 0000000..ff7aa20 --- /dev/null +++ b/spikes/substrate-workspace-adapter/tests/exec-shim.test.js @@ -0,0 +1,1065 @@ +'use strict'; + +import assert from 'node:assert/strict'; +import fs from 'node:fs'; +import http from 'node:http'; +import os from 'node:os'; +import path from 'node:path'; +import { spawn, spawnSync } from 'node:child_process'; +import { once } from 'node:events'; +import { test } from 'node:test'; +import { fileURLToPath } from 'node:url'; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const image = path.resolve(__dirname, '../live-agent-image'); +const shim = path.join(image, 'exec-shim.js'); +const launcher = path.join(image, 'bin/start-native-agent'); +const mainloop = path.join(image, 'bin/mainloop'); +const dockerfile = path.join(image, 'Dockerfile'); +const entrypoint = path.join(image, 'entrypoint.sh'); +const fixtures = path.join(__dirname, 'fixtures/native'); +const token = 'fixture-only-shim-token-long-enough-for-testing'; +const claudePlaceholder = 'sk-ant-oat01-mainloop-egress-placeholder'; +function codexPlaceholder(accountId = 'fixture-account') { + const encode = (value) => Buffer.from(JSON.stringify(value)).toString('base64url'); + const jwt = `${encode({ alg: 'none' })}.${encode({ exp: 1_799_999_999 })}.synthetic`; + return JSON.stringify({ + auth_mode: 'chatgpt', + tokens: { + id_token: jwt, + access_token: jwt, + refresh_token: '', + account_id: accountId + }, + last_refresh: '2026-09-24T00:00:00Z' + }); +} + +test('mainloop sends its bearer header through a protected curl config file', (t) => { + const root = fs.mkdtempSync(path.join(os.tmpdir(), 'mainloop-helper-')); + const fakeBin = path.join(root, 'bin'); + fs.mkdirSync(fakeBin, { recursive: true }); + const fakeCurl = path.join(fakeBin, 'curl'); + fs.writeFileSync( + fakeCurl, + '#!/usr/bin/env bash\n' + + 'set -euo pipefail\n' + + 'args=("$@")\n' + + 'printf \'%s\\0\' "${args[@]}" >"$CURL_ARGS_CAPTURE"\n' + + 'printf \'%s|%s\' "${MAINLOOP_TOKEN-}" "${TOKEN-}" >"$CURL_TOKEN_ENV_CAPTURE"\n' + + 'for ((i = 0; i < ${#args[@]}; i++)); do\n' + + ' if [[ ${args[i]} == -H && ${args[i + 1]-} == @* ]]; then\n' + + ' header_file="${args[i + 1]#@}"\n' + + ' cat "$header_file" >"$CURL_HEADER_CAPTURE"\n' + + ' stat -c \'%a\' "$header_file" >"$CURL_HEADER_MODE_CAPTURE"\n' + + ' fi\n' + + 'done\n' + + 'printf \'{"text":"fixture response"}\\n200\'\n', + { mode: 0o700 } + ); + t.after(() => fs.rmSync(root, { recursive: true, force: true })); + + const helperToken = `ml_${'b'.repeat(64)}`; + for (const [name, helperArgs] of [ + ['get', ['whoami']], + ['post', ['note', 'fixture note']] + ]) { + const capture = (suffix) => path.join(root, `${name}-${suffix}`); + const env = { + ...process.env, + PATH: `${fakeBin}:${process.env.PATH}`, + MAINLOOP_TOKEN: helperToken, + TOKEN: 'inherited-fixture-value', + CURL_ARGS_CAPTURE: capture('args'), + CURL_HEADER_CAPTURE: capture('header'), + CURL_HEADER_MODE_CAPTURE: capture('header-mode'), + CURL_TOKEN_ENV_CAPTURE: capture('token-env') + }; + const result = spawnSync('bash', [mainloop, ...helperArgs], { + encoding: 'utf8', + env + }); + + assert.equal(result.status, 0, result.stderr); + assert.equal(result.stdout.trim(), 'fixture response'); + assert.equal(result.stdout.includes(helperToken), false); + assert.equal(result.stderr.includes(helperToken), false); + const args = fs.readFileSync(capture('args'), 'utf8').split('\0'); + const joinedArgs = args.join('\0'); + assert.equal(joinedArgs.includes(helperToken), false); + assert.ok( + args.includes( + 'http://mainloop-backend.mainloop-control.svc.cluster.local:8000/agent-api/' + + (name === 'get' ? 'whoami' : 'records') + ) + ); + const headerArgument = args.find((arg) => arg.startsWith('@')); + assert.ok(headerArgument); + assert.equal(fs.existsSync(headerArgument.slice(1)), false); + assert.equal( + fs.readFileSync(capture('header'), 'utf8'), + `Authorization: Bearer ${helperToken}\n` + ); + assert.equal(fs.readFileSync(capture('header-mode'), 'utf8').trim(), '600'); + assert.equal(fs.readFileSync(capture('token-env'), 'utf8'), '|'); + } +}); + +async function startShim(root, extraEnv = {}) { + const home = path.join(root, 'home'); + const workspace = path.join(root, 'repo'); + const state = path.join(root, 'state'); + const fakeBin = path.join(root, 'bin'); + const shimCopy = path.join(root, 'exec-shim.js'); + fs.mkdirSync(home, { recursive: true }); + fs.mkdirSync(workspace, { recursive: true }); + fs.mkdirSync(fakeBin, { recursive: true }); + fs.copyFileSync(shim, shimCopy); + const child = spawn(process.execPath, [shimCopy], { + env: { + ...process.env, + ...(process.getuid() === 0 ? { EXEC_SHIM_TEST_ALLOW_ROOT: '1' } : {}), + ...extraEnv, + HOME: home, + CODEX_HOME: path.join(home, '.codex'), + WORKSPACE_PATH: workspace, + EXEC_SHIM_STATE_DIR: state, + EXEC_SHIM_HOST: '127.0.0.1', + EXEC_SHIM_PORT: '0', + NATIVE_AGENT_LAUNCHER: launcher, + PATH: `${fakeBin}:${process.env.PATH}` + }, + stdio: ['ignore', 'pipe', 'pipe'] + }); + let output = ''; + const port = await new Promise((resolve, reject) => { + const timer = setTimeout(() => reject(new Error(`shim did not start: ${output}`)), 5000); + child.once('error', reject); + child.once('exit', (code) => reject(new Error(`shim exited ${code}: ${output}`))); + child.stdout.on('data', (chunk) => { + output += chunk; + const match = output.match(/exec-shim listening on :(\d+)/); + if (match) { + clearTimeout(timer); + resolve(Number(match[1])); + } + }); + child.stderr.on('data', (chunk) => { + output += chunk; + }); + }); + return { child, port, output: () => output, home, workspace, fakeBin, state }; +} + +test('exec shim refuses UID 0 unless its explicit test-only override is set', () => { + const source = `Object.defineProperty(process, 'getuid', { value: () => 0 }); require(${JSON.stringify(shim)});`; + const result = spawnSync(process.execPath, ['-e', source], { + encoding: 'utf8', + env: { ...process.env, EXEC_SHIM_TEST_ALLOW_ROOT: '0' } + }); + assert.equal(result.status, 1); + assert.match(result.stderr, /exec-shim refuses to start as UID 0/); +}); + +test('actor image runs as its non-root user and checks the mounted workspace', () => { + const imageDockerfile = fs.readFileSync(dockerfile, 'utf8'); + const imageEntrypoint = fs.readFileSync(entrypoint, 'utf8'); + assert.match(imageDockerfile, /^USER 10001:10001$/m); + assert.match(imageDockerfile, /^WORKDIR \/work$/m); + assert.doesNotMatch(imageDockerfile, /util-linux|USER 0:0/); + assert.doesNotMatch(imageEntrypoint, /setpriv|--runtime-user|chown/); + const rootRefusal = imageEntrypoint.indexOf('entrypoint refused to continue as UID 0'); + const workspaceCheck = imageEntrypoint.search(/\[\[ ! -w "?\$\{WORKSPACE_PATH\}"? \]\]/); + const workspaceError = imageEntrypoint.indexOf( + 'entrypoint requires a writable workspace directory' + ); + const configPreparation = imageEntrypoint.indexOf( + 'node /usr/local/bin/prepare-native-agent-config.cjs' + ); + const shimStart = imageEntrypoint.indexOf('node "${EXEC_SHIM}"'); + assert.ok(rootRefusal >= 0 && rootRefusal < workspaceCheck); + assert.ok(workspaceCheck >= 0 && workspaceCheck < configPreparation); + assert.ok(workspaceError >= 0 && workspaceError < configPreparation); + assert.ok(configPreparation >= 0 && configPreparation < shimStart); + assert.equal(imageEntrypoint.includes('IS_SANDBOX'), false); + assert.equal(imageDockerfile.includes('EXEC_SHIM_TEST_ALLOW_ROOT'), false); +}); + +function request(port, method, route, { body, bearer } = {}) { + return new Promise((resolve, reject) => { + const headers = {}; + if (body !== undefined) headers['content-type'] = 'application/json'; + if (bearer !== undefined) headers.authorization = `Bearer ${bearer}`; + const req = http.request({ host: '127.0.0.1', port, method, path: route, headers }, (res) => { + const chunks = []; + res.on('data', (chunk) => chunks.push(chunk)); + res.on('end', () => + resolve({ + status: res.statusCode, + body: Buffer.concat(chunks).toString('utf8') + }) + ); + }); + req.once('error', reject); + if (body !== undefined) req.end(JSON.stringify(body)); + else req.end(); + }); +} + +async function installToken(running) { + const result = await request(running.port, 'POST', '/token', { body: { token } }); + assert.equal(result.status, 201, result.body); +} + +async function installCredential(running, name, contents) { + const result = await request(running.port, 'POST', '/credential', { + bearer: token, + body: { name, contents } + }); + assert.equal(result.status, 201, result.body); +} + +async function waitForJob(running, kind, id, bearer = token) { + for (let attempt = 0; attempt < 100; attempt += 1) { + const result = await request(running.port, 'GET', `/${kind}/${id}`, { bearer }); + assert.equal(result.status, 200, result.body); + const job = JSON.parse(result.body); + if (job.status !== 'running') return job; + await new Promise((resolve) => setTimeout(resolve, 20)); + } + throw new Error(`${kind} job ${id} did not finish`); +} + +async function stop(child) { + if (child.exitCode !== null || child.signalCode !== null) return; + child.kill('SIGTERM'); + await once(child, 'exit'); +} + +function fakeCli(file, script) { + fs.writeFileSync(file, script, { mode: 0o700 }); +} + +test('token is private and one-time; readiness is workspace-only and work routes require auth', async (t) => { + const root = fs.mkdtempSync(path.join(os.tmpdir(), 'exec-shim-token-')); + let running = await startShim(root); + t.after(async () => { + await stop(running.child); + fs.rmSync(root, { recursive: true, force: true }); + }); + + assert.equal((await request(running.port, 'GET', '/healthz')).status, 200); + assert.equal((await request(running.port, 'GET', '/readyz')).status, 200); + assert.equal( + (await request(running.port, 'POST', '/run', { body: { command: 'true' } })).status, + 401 + ); + assert.equal( + (await request(running.port, 'POST', '/turn', { body: { agent: 'claude', prompt: 'fixture' } })) + .status, + 401 + ); + await installToken(running); + + const tokenPath = path.join(running.home, '.mainloop', 'exec-shim-token'); + assert.equal(fs.statSync(tokenPath).mode & 0o777, 0o600); + assert.equal(fs.statSync(path.dirname(tokenPath)).mode & 0o777, 0o700); + assert.equal(fs.readFileSync(tokenPath, 'utf8'), token); + assert.equal( + (await request(running.port, 'GET', '/turn/00000000-0000-4000-8000-000000000000')).status, + 401 + ); + assert.equal( + ( + await request(running.port, 'GET', '/turn/00000000-0000-4000-8000-000000000000', { + bearer: `${token}-wrong` + }) + ).status, + 401 + ); + assert.equal( + ( + await request(running.port, 'GET', '/turn/00000000-0000-4000-8000-000000000000', { + bearer: token + }) + ).status, + 404 + ); + assert.equal((await request(running.port, 'POST', '/token', { body: { token } })).status, 409); + assert.equal(running.output().includes(token), false); + + await stop(running.child); + running = await startShim(root); + assert.equal((await request(running.port, 'GET', '/healthz')).status, 200); + assert.equal( + (await request(running.port, 'GET', '/turn/00000000-0000-4000-8000-000000000000')).status, + 401 + ); + assert.equal( + ( + await request(running.port, 'GET', '/turn/00000000-0000-4000-8000-000000000000', { + bearer: token + }) + ).status, + 404 + ); +}); + +test('credential delivery remains token-gated, allowlisted, private, and one-time', async (t) => { + const root = fs.mkdtempSync(path.join(os.tmpdir(), 'exec-shim-credentials-')); + const running = await startShim(root); + t.after(async () => { + await stop(running.child); + fs.rmSync(root, { recursive: true, force: true }); + }); + + const claudeContents = 'fixture-real-provider-token-never-logged'; + const authContents = JSON.stringify({ + auth_mode: 'chatgpt', + tokens: { + id_token: 'fixture.header.signature', + access_token: 'fixture.header.signature', + refresh_token: 'fixture-real-refresh-token', + account_id: 'fixture-account' + }, + last_refresh: '2026-09-24T00:00:00Z' + }); + assert.equal( + ( + await request(running.port, 'POST', '/credential', { + body: { name: 'codex-auth', contents: authContents } + }) + ).status, + 401 + ); + await installToken(running); + assert.equal( + ( + await request(running.port, 'POST', '/credential', { + bearer: `${token}-wrong`, + body: { name: 'claude-token', contents: claudeContents } + }) + ).status, + 401 + ); + assert.equal( + ( + await request(running.port, 'POST', '/credential', { + bearer: token, + body: { name: '../outside', contents: claudeContents } + }) + ).status, + 403 + ); + const claudePath = path.join(running.home, '.mainloop', 'claude-token'); + assert.equal( + ( + await request(running.port, 'POST', '/credential', { + bearer: token, + body: { name: 'claude-token', contents: claudeContents } + }) + ).status, + 400 + ); + assert.equal( + ( + await request(running.port, 'POST', '/credential', { + bearer: token, + body: { name: 'codex-auth', contents: '[]' } + }) + ).status, + 400 + ); + assert.equal( + ( + await request(running.port, 'POST', '/credential', { + bearer: token, + body: { name: 'claude-token', contents: claudePlaceholder } + }) + ).status, + 201 + ); + assert.equal(fs.readFileSync(claudePath, 'utf8'), claudePlaceholder); + assert.equal(fs.statSync(claudePath).mode & 0o777, 0o600); + assert.equal( + ( + await request(running.port, 'POST', '/credential', { + bearer: token, + body: { name: 'claude-token', contents: claudePlaceholder } + }) + ).status, + 409 + ); + assert.equal( + ( + await request(running.port, 'PUT', '/credential', { + bearer: token, + body: { name: 'claude-token', contents: claudePlaceholder } + }) + ).status, + 200 + ); + assert.equal(fs.readFileSync(claudePath, 'utf8'), claudePlaceholder); + assert.equal( + ( + await request(running.port, 'PUT', '/credential', { + bearer: token, + body: { name: 'claude-token', contents: claudeContents } + }) + ).status, + 400 + ); + const authPath = path.join(running.home, '.codex', 'auth.json'); + assert.equal( + ( + await request(running.port, 'POST', '/credential', { + bearer: token, + body: { name: 'codex-auth', contents: authContents } + }) + ).status, + 400 + ); + const placeholder = codexPlaceholder(); + assert.equal( + ( + await request(running.port, 'POST', '/credential', { + bearer: token, + body: { name: 'codex-auth', contents: placeholder } + }) + ).status, + 201 + ); + assert.equal(fs.readFileSync(authPath, 'utf8'), placeholder); + assert.equal(fs.statSync(authPath).mode & 0o777, 0o600); + const replaced = await request(running.port, 'PUT', '/credential', { + bearer: token, + body: { name: 'codex-auth', contents: placeholder } + }); + assert.equal(replaced.status, 200, replaced.body); + assert.equal(fs.readFileSync(authPath, 'utf8'), placeholder); + const rejectedReplacement = await request(running.port, 'PUT', '/credential', { + bearer: token, + body: { name: 'codex-auth', contents: authContents } + }); + assert.equal(rejectedReplacement.status, 400); + assert.equal(fs.readFileSync(authPath, 'utf8'), placeholder); + assert.equal(running.output().includes(claudeContents), false); + assert.equal(running.output().includes(authContents), false); +}); + +test('turn prompt is piped on stdin and never appears in argv or shim logs', async (t) => { + const root = fs.mkdtempSync(path.join(os.tmpdir(), 'exec-shim-stdin-')); + const running = await startShim(root, { + ARGS_CAPTURE: path.join(root, 'claude-args'), + PROMPT_CAPTURE: path.join(root, 'claude-prompt') + }); + fakeCli( + path.join(running.fakeBin, 'claude'), + '#!/bin/sh\nprintf "%s\\n" "$@" >"$ARGS_CAPTURE"\ncat >"$PROMPT_CAPTURE"\ncat <<\'EVENTS\'\n{"type":"system","subtype":"init","session_id":"fixture-session"}\n{"type":"result","result":"fixture answer","session_id":"fixture-session"}\nEVENTS\n' + ); + t.after(async () => { + await stop(running.child); + fs.rmSync(root, { recursive: true, force: true }); + }); + await installToken(running); + await installCredential(running, 'claude-token', claudePlaceholder); + const prompt = 'private fixture prompt must travel only on stdin'; + const started = await request(running.port, 'POST', '/turn', { + bearer: token, + body: { + agent: 'claude', + prompt, + session_key: 'stdin-session', + session_id: 'native-stdin-session', + resume: false + } + }); + assert.equal(started.status, 202, started.body); + const { id } = JSON.parse(started.body); + const result = await waitForJob(running, 'turn', id); + assert.equal(result.status, 'completed'); + assert.equal(result.final_message, 'fixture answer'); + assert.equal(result.native_session_id, 'fixture-session'); + assert.equal(fs.readFileSync(path.join(root, 'claude-prompt'), 'utf8'), prompt); + const argv = fs.readFileSync(path.join(root, 'claude-args'), 'utf8'); + assert.match(argv, /-p/); + assert.match(argv, /--session-id\s+native-stdin-session/); + // Claude Code rejects --print with stream-json output unless --verbose is set. + assert.match(argv, /--output-format\nstream-json\n--verbose\n/); + assert.equal(argv.includes(prompt), false); + assert.equal(running.output().includes(prompt), false); + assert.equal( + fs + .readFileSync(path.join(running.state, 'turns', id + '.events.jsonl'), 'utf8') + .includes(prompt), + false + ); +}); + +test('startup options reach the main Claude launch with scoped tools and standing context', async (t) => { + const root = fs.mkdtempSync(path.join(os.tmpdir(), 'exec-shim-startup-')); + const basePrompt = path.join(root, 'base-prompt.txt'); + fs.writeFileSync(basePrompt, 'base system instructions\n'); + const running = await startShim(root, { + AGENT_SYSTEM_PROMPT_FILE: basePrompt, + CLAUDE_ARGS_CAPTURE: path.join(root, 'claude-args'), + CLAUDE_CWD_CAPTURE: path.join(root, 'claude-cwd'), + CLAUDE_TOKEN_CAPTURE: path.join(root, 'claude-token'), + CLAUDE_CONTEXT_CAPTURE: path.join(root, 'claude-context') + }); + fakeCli( + path.join(running.fakeBin, 'claude'), + '#!/bin/sh\nprintf "%s\\n" "$@" >"$CLAUDE_ARGS_CAPTURE"\npwd >"$CLAUDE_CWD_CAPTURE"\nprintf "%s" "$MAINLOOP_TOKEN" >"$CLAUDE_TOKEN_CAPTURE"\nwhile [ "$#" -gt 0 ]; do\n if [ "$1" = --append-system-prompt-file ]; then cat "$2" >"$CLAUDE_CONTEXT_CAPTURE"; shift 2; else shift; fi\ndone\ncat >/dev/null\nprintf \'%s\\n\' \'{"type":"system","subtype":"init","session_id":"main-startup-session"}\' \'{"type":"result","result":"startup fixture answer","session_id":"main-startup-session"}\'\n' + ); + t.after(async () => { + await stop(running.child); + fs.rmSync(root, { recursive: true, force: true }); + }); + await installToken(running); + await installCredential(running, 'claude-token', claudePlaceholder); + + const startupContext = 'Mainloop scoped standing context\nOnly use mainloop commands.'; + const started = await request(running.port, 'POST', '/turn', { + bearer: token, + body: { + agent: 'claude', + prompt: 'startup fixture prompt', + session_key: 'main-startup-session', + session_id: 'native-main-startup-session', + resume: false, + startup_options: { + cwd_rel: 'main', + token: `ml_${'a'.repeat(64)}`, + standing_b64: Buffer.from(startupContext).toString('base64'), + approval_policy: 'restricted: Bash(mainloop:*) only', + model: 'sonnet', + effort: 'medium' + } + } + }); + assert.equal(started.status, 202, started.body); + await waitForJob(running, 'turn', JSON.parse(started.body).id); + + const args = fs.readFileSync(path.join(root, 'claude-args'), 'utf8'); + assert.match(args, /--tools\nBash\n/); + assert.match(args, /--allowedTools\nBash\(mainloop:\*\)\n/); + assert.match(args, /--permission-mode\ndontAsk\n/); + assert.match(args, /--model\nsonnet\n/); + assert.match(args, /--effort\nmedium\n/); + assert.doesNotMatch(args, /--dangerously-skip-permissions/); + assert.equal( + fs.readFileSync(path.join(root, 'claude-cwd'), 'utf8').trim(), + path.join(running.workspace, 'main') + ); + assert.equal(fs.readFileSync(path.join(root, 'claude-token'), 'utf8'), `ml_${'a'.repeat(64)}`); + const context = fs.readFileSync(path.join(root, 'claude-context'), 'utf8'); + assert.ok(context.startsWith('base system instructions\n')); + assert.ok(context.includes(startupContext)); + assert.equal( + fs + .readdirSync(path.join(running.home, '.mainloop')) + .some((name) => name.startsWith('standing.')), + false + ); +}); + +test('a second concurrent turn for the same logical session receives 409', async (t) => { + const root = fs.mkdtempSync(path.join(os.tmpdir(), 'exec-shim-concurrency-')); + const running = await startShim(root, { TURN_DELAY: '0.4' }); + fakeCli( + path.join(running.fakeBin, 'claude'), + '#!/bin/sh\ncat >/dev/null\nsleep "$TURN_DELAY"\nprintf \'%s\\n\' \'{"type":"result","session_id":"fixture-session","result":"done"}\'\n' + ); + t.after(async () => { + await stop(running.child); + fs.rmSync(root, { recursive: true, force: true }); + }); + await installToken(running); + await installCredential(running, 'claude-token', claudePlaceholder); + const body = { + agent: 'claude', + prompt: 'turn prompt', + session_key: 'same-session', + session_id: 'native-same-session', + resume: false + }; + const first = await request(running.port, 'POST', '/turn', { bearer: token, body }); + assert.equal(first.status, 202, first.body); + const second = await request(running.port, 'POST', '/turn', { bearer: token, body }); + assert.equal(second.status, 409); + const result = await waitForJob(running, 'turn', JSON.parse(first.body).id); + assert.equal(result.status, 'completed'); +}); + +test('same-provider sessions scope status, cancellation, and interleaved sends', async (t) => { + const root = fs.mkdtempSync(path.join(os.tmpdir(), 'exec-shim-session-scope-')); + const running = await startShim(root); + fakeCli( + path.join(running.fakeBin, 'claude'), + '#!/bin/sh\nprompt="$(cat)"\ncase "$prompt" in\n slow-A) native_id=native-A; sleep 5 ;;\n slow-B) native_id=native-B; sleep 1 ;;\n second-B) native_id=native-B ;;\n *) exit 2 ;;\nesac\nprintf \'{"type":"result","session_id":"%s","result":"%s"}\\n\' "$native_id" "$prompt"\n' + ); + t.after(async () => { + await stop(running.child); + fs.rmSync(root, { recursive: true, force: true }); + }); + await installToken(running); + await installCredential(running, 'claude-token', claudePlaceholder); + + const sessionA = { + agent: 'claude', + prompt: 'slow-A', + session_key: 'logical-session-A', + session_id: 'native-A', + resume: false + }; + const sessionB = { + agent: 'claude', + prompt: 'slow-B', + session_key: 'logical-session-B', + session_id: 'native-B', + resume: false + }; + const startedA = await request(running.port, 'POST', '/turn', { + bearer: token, + body: sessionA + }); + assert.equal(startedA.status, 202, startedA.body); + const startedB = await request(running.port, 'POST', '/turn', { + bearer: token, + body: sessionB + }); + assert.equal(startedB.status, 202, startedB.body); + const jobA = JSON.parse(startedA.body).id; + const jobB = JSON.parse(startedB.body).id; + const statusA = await request( + running.port, + 'GET', + '/turn/status?agent=claude&session_key=logical-session-A', + { bearer: token } + ); + const statusB = await request( + running.port, + 'GET', + '/turn/status?agent=claude&session_key=logical-session-B', + { bearer: token } + ); + assert.equal(JSON.parse(statusA.body).id, jobA); + assert.equal(JSON.parse(statusA.body).status, 'running'); + assert.equal(JSON.parse(statusB.body).id, jobB); + assert.equal(JSON.parse(statusB.body).status, 'running'); + assert.equal(JSON.parse(statusB.body).native_session_id, 'native-B'); + + const stoppedA = await request(running.port, 'POST', '/turn/stop', { + bearer: token, + body: { agent: 'claude', session_key: 'logical-session-A' } + }); + assert.equal(JSON.parse(stoppedA.body).status, 'interrupted'); + assert.equal((await waitForJob(running, 'turn', jobA)).status, 'interrupted'); + const statusBAfterStop = await request( + running.port, + 'GET', + '/turn/status?agent=claude&session_key=logical-session-B', + { bearer: token } + ); + assert.equal(JSON.parse(statusBAfterStop.body).status, 'running'); + const completedB = await waitForJob(running, 'turn', jobB); + assert.equal(completedB.final_message, 'slow-B'); + + const secondB = await request(running.port, 'POST', '/turn', { + bearer: token, + body: { ...sessionB, prompt: 'second-B', resume: true } + }); + assert.equal(secondB.status, 202, secondB.body); + const completedSecondB = await waitForJob(running, 'turn', JSON.parse(secondB.body).id); + assert.equal(completedSecondB.final_message, 'second-B'); + + const finalStatusB = await request( + running.port, + 'GET', + '/turn/status?agent=claude&session_key=logical-session-B', + { bearer: token } + ); + assert.equal(JSON.parse(finalStatusB.body).id, JSON.parse(secondB.body).id); + assert.equal(JSON.parse(finalStatusB.body).status, 'completed'); +}); + +test('turn status locates the latest turn after it completes', async (t) => { + const root = fs.mkdtempSync(path.join(os.tmpdir(), 'exec-shim-status-')); + const running = await startShim(root, { + CLAUDE_EVENTS: path.join(fixtures, 'claude-stream-json.jsonl') + }); + fakeCli( + path.join(running.fakeBin, 'claude'), + '#!/bin/sh\ncat >/dev/null\ncat "$CLAUDE_EVENTS"\n' + ); + t.after(async () => { + await stop(running.child); + fs.rmSync(root, { recursive: true, force: true }); + }); + await installToken(running); + await installCredential(running, 'claude-token', claudePlaceholder); + + const submitted = await request(running.port, 'POST', '/turn', { + bearer: token, + body: { + agent: 'claude', + prompt: 'fixture status prompt', + session_key: 'status-session', + session_id: 'native-status-session', + resume: false + } + }); + assert.equal(submitted.status, 202, submitted.body); + const { id } = JSON.parse(submitted.body); + const current = await waitForJob(running, 'turn', id); + const status = await request( + running.port, + 'GET', + '/turn/status?agent=claude&session_key=status-session', + { + bearer: token + } + ); + assert.equal(status.status, 200, status.body); + assert.equal(JSON.parse(status.body).id, id); + assert.equal(JSON.parse(status.body).status, 'completed'); + assert.equal(JSON.parse(status.body).native_session_id, current.native_session_id); + assert.equal( + ( + await request(running.port, 'GET', '/turn/status?agent=claude&session_key=other-session', { + bearer: token + }) + ).status, + 404 + ); +}); + +test('turn status exposes only a boolean provider-auth rejection signal', async (t) => { + const root = fs.mkdtempSync(path.join(os.tmpdir(), 'exec-shim-auth-rejected-')); + const running = await startShim(root); + fakeCli( + path.join(running.fakeBin, 'claude'), + "#!/bin/sh\ncat >/dev/null\nprintf 'provider request failed with HTTP 401 Unauthorized\\n' >&2\nexit 1\n" + ); + t.after(async () => { + await stop(running.child); + fs.rmSync(root, { recursive: true, force: true }); + }); + await installToken(running); + await installCredential(running, 'claude-token', claudePlaceholder); + + const submitted = await request(running.port, 'POST', '/turn', { + bearer: token, + body: { + agent: 'claude', + prompt: 'synthetic rejected turn', + session_key: 'rejected-session', + session_id: 'native-rejected-session', + resume: false + } + }); + assert.equal(submitted.status, 202, submitted.body); + const { id } = JSON.parse(submitted.body); + const result = await waitForJob(running, 'turn', id); + assert.equal(result.status, 'failed'); + assert.equal(result.credential_rejected, true); + assert.equal(running.output().includes(claudePlaceholder), false); +}); + +test('agent readiness requires its credential and rejects unauthenticated checks', async (t) => { + const root = fs.mkdtempSync(path.join(os.tmpdir(), 'exec-shim-agent-ready-')); + const running = await startShim(root); + t.after(async () => { + await stop(running.child); + fs.rmSync(root, { recursive: true, force: true }); + }); + await installToken(running); + + const missing = await request(running.port, 'GET', '/agent/ready?agent=codex', { + bearer: token + }); + assert.equal(missing.status, 503); + const unauthorized = await request(running.port, 'GET', '/agent/ready?agent=codex'); + assert.equal(unauthorized.status, 401); + + await installCredential(running, 'codex-auth', codexPlaceholder()); + const configured = await request(running.port, 'GET', '/agent/ready?agent=codex', { + bearer: token + }); + assert.equal(configured.status, 200, configured.body); + assert.deepEqual(JSON.parse(configured.body), { agent: 'codex', configured: true }); +}); + +test('process registry starts and stops long-lived commands and reports bounded tails and ports', async (t) => { + const root = fs.mkdtempSync(path.join(os.tmpdir(), 'exec-shim-processes-')); + const procNet = path.join(root, 'proc-net'); + fs.mkdirSync(procNet, { recursive: true }); + fs.writeFileSync( + path.join(procNet, 'tcp'), + 'sl local_address rem_address st\n' + + '0: 0100007F:1F90 00000000:0000 0A\n' + + '1: 0100007F:1F91 00000000:0000 01\n' + ); + fs.writeFileSync( + path.join(procNet, 'tcp6'), + 'sl local_address rem_address st\n' + + '0: 00000000000000000000000000000000:1FBB 00000000000000000000000000000000:0000 0A\n' + ); + const running = await startShim(root, { EXEC_SHIM_PROC_NET_DIR: procNet }); + t.after(async () => { + await stop(running.child); + fs.rmSync(root, { recursive: true, force: true }); + }); + await installToken(running); + + assert.equal((await request(running.port, 'GET', '/processes')).status, 401); + assert.equal((await request(running.port, 'GET', '/ports')).status, 401); + const invalid = await request(running.port, 'POST', '/processes', { + bearer: token, + body: { command: ' ' } + }); + assert.equal(invalid.status, 400); + + const started = await request(running.port, 'POST', '/processes', { + bearer: token, + body: { + label: 'vite', + command: + "node -e \"process.stdout.write('x'.repeat(100000) + '\\\\npreview-ready\\\\n')\"; while :; do sleep 1; done" + } + }); + assert.equal(started.status, 201, started.body); + const { id } = JSON.parse(started.body); + + let listed; + for (let attempt = 0; attempt < 50; attempt += 1) { + const result = await request(running.port, 'GET', '/processes', { bearer: token }); + assert.equal(result.status, 200, result.body); + listed = JSON.parse(result.body).processes.find((entry) => entry.id === id); + if (listed?.log_tail.includes('preview-ready')) break; + await new Promise((resolve) => setTimeout(resolve, 20)); + } + assert.equal(listed.status, 'running'); + assert.equal(listed.label, 'vite'); + assert.match(listed.log_tail, /preview-ready/); + assert.ok(Buffer.byteLength(listed.log_tail) <= 64 * 1024); + assert.equal(Object.hasOwn(listed, 'command'), false); + assert.equal(running.output().includes('preview-ready'), false); + + const ports = await request(running.port, 'GET', '/ports', { bearer: token }); + assert.equal(ports.status, 200, ports.body); + assert.deepEqual(JSON.parse(ports.body).ports, [8080, 8123]); + + const stopped = await request(running.port, 'DELETE', `/processes/${id}`, { bearer: token }); + assert.equal(stopped.status, 200, stopped.body); + assert.equal(JSON.parse(stopped.body).status, 'stopped'); +}); + +test('journal returns bounded numbered pages for Claude and Codex and rejects a bad token', async (t) => { + const root = fs.mkdtempSync(path.join(os.tmpdir(), 'exec-shim-journal-')); + const running = await startShim(root); + t.after(async () => { + await stop(running.child); + fs.rmSync(root, { recursive: true, force: true }); + }); + await installToken(running); + + const claudeId = 'fixture-claude-session'; + const claudeFile = path.join(running.home, '.claude', 'projects', 'p1', `${claudeId}.jsonl`); + fs.mkdirSync(path.dirname(claudeFile), { recursive: true }); + fs.writeFileSync(claudeFile, '{"type":"system"}\n{"type":"user"}\n{"type":"assistant"}\npartial'); + const firstPage = await request( + running.port, + 'GET', + `/journal?agent=claude&id=${claudeId}&from=1&limit=1`, + { bearer: token } + ); + assert.equal(firstPage.status, 200, firstPage.body); + assert.deepEqual(JSON.parse(firstPage.body), { + file: claudeFile, + total_lines: 3, + lines: [{ line: 2, text: '{"type":"user"}' }] + }); + const wrongToken = await request( + running.port, + 'GET', + `/journal?agent=claude&id=${claudeId}&from=0`, + { bearer: `${token}-wrong` } + ); + assert.equal(wrongToken.status, 401); + + const codexId = 'fixture-codex-thread'; + const codexFile = path.join( + running.home, + '.codex', + 'sessions', + '2026', + '09', + 'rollout-2026-09-24T00-00-00-fixture-codex-thread.jsonl' + ); + fs.mkdirSync(path.dirname(codexFile), { recursive: true }); + fs.writeFileSync(codexFile, '{"type":"thread.started"}\n{"type":"turn.completed"}\n'); + const codexPage = await request( + running.port, + 'GET', + `/journal?agent=codex&id=${codexId}&from=0&limit=1`, + { bearer: token } + ); + assert.equal(codexPage.status, 200, codexPage.body); + assert.deepEqual(JSON.parse(codexPage.body), { + file: codexFile, + total_lines: 2, + lines: [{ line: 1, text: '{"type":"thread.started"}' }] + }); + assert.equal(running.output().includes(token), false); +}); + +test('stop interrupts an in-flight turn and releases its slot', async (t) => { + const root = fs.mkdtempSync(path.join(os.tmpdir(), 'exec-shim-stop-')); + const running = await startShim(root); + fakeCli(path.join(running.fakeBin, 'claude'), '#!/bin/sh\ncat >/dev/null\nsleep 5\n'); + t.after(async () => { + await stop(running.child); + fs.rmSync(root, { recursive: true, force: true }); + }); + await installToken(running); + await installCredential(running, 'claude-token', claudePlaceholder); + + const submitted = await request(running.port, 'POST', '/turn', { + bearer: token, + body: { + agent: 'claude', + prompt: 'stop fixture turn', + session_key: 'stop-session', + session_id: 'native-stop-session', + resume: false + } + }); + assert.equal(submitted.status, 202, submitted.body); + const { id } = JSON.parse(submitted.body); + const stopped = await request(running.port, 'POST', '/turn/stop', { + bearer: token, + body: { agent: 'claude', session_key: 'stop-session' } + }); + assert.equal(stopped.status, 200, stopped.body); + assert.equal(JSON.parse(stopped.body).status, 'interrupted'); + const result = await waitForJob(running, 'turn', id); + assert.equal(result.status, 'interrupted'); +}); + +test('/run executes a command, stores bounded output, and reports timeout', async (t) => { + const root = fs.mkdtempSync(path.join(os.tmpdir(), 'exec-shim-run-')); + const running = await startShim(root); + t.after(async () => { + await stop(running.child); + fs.rmSync(root, { recursive: true, force: true }); + }); + await installToken(running); + const quick = await request(running.port, 'POST', '/run', { + bearer: token, + body: { command: 'printf fixture-output', timeout_ms: 1000 } + }); + assert.equal(quick.status, 202, quick.body); + const completed = await waitForJob(running, 'run', JSON.parse(quick.body).id); + assert.equal(completed.status, 'completed'); + assert.equal(completed.exit_code, 0); + assert.equal(completed.output, 'fixture-output'); + + const slow = await request(running.port, 'POST', '/run', { + bearer: token, + body: { command: 'sleep 5; printf should-not-finish', timeout_ms: 50 } + }); + assert.equal(slow.status, 202, slow.body); + const timeout = await waitForJob(running, 'run', JSON.parse(slow.body).id); + assert.equal(timeout.status, 'timed_out'); + assert.equal(timeout.output.includes('should-not-finish'), false); +}); + +test('Claude stream-json and Codex JSONL produce native ids and final messages', async (t) => { + const root = fs.mkdtempSync(path.join(os.tmpdir(), 'exec-shim-events-')); + const running = await startShim(root, { + CLAUDE_EVENTS: path.join(fixtures, 'claude-stream-json.jsonl'), + CODEX_EVENTS: path.join(fixtures, 'codex-jsonl.jsonl'), + CLAUDE_ARGS_CAPTURE: path.join(root, 'claude-args'), + CODEX_ARGS_CAPTURE: path.join(root, 'codex-args') + }); + fakeCli( + path.join(running.fakeBin, 'claude'), + '#!/bin/sh\nprintf "%s\\n" "$@" >>"$CLAUDE_ARGS_CAPTURE"\ncat >/dev/null\ncat "$CLAUDE_EVENTS"\n' + ); + fakeCli( + path.join(running.fakeBin, 'codex'), + '#!/bin/sh\nprintf "%s\\n" "$@" >"$CODEX_ARGS_CAPTURE"\ncat >/dev/null\ncat "$CODEX_EVENTS"\n' + ); + t.after(async () => { + await stop(running.child); + fs.rmSync(root, { recursive: true, force: true }); + }); + await installToken(running); + await installCredential(running, 'claude-token', claudePlaceholder); + await installCredential(running, 'codex-auth', codexPlaceholder()); + + const claudeStart = await request(running.port, 'POST', '/turn', { + bearer: token, + body: { + agent: 'claude', + session_key: 'events-claude', + session_id: 'new-claude-session-001', + resume: false, + prompt: 'claude first turn' + } + }); + assert.equal(claudeStart.status, 202, claudeStart.body); + const claude = await waitForJob(running, 'turn', JSON.parse(claudeStart.body).id); + assert.equal(claude.native_session_id, 'fixture-claude-session'); + assert.equal(claude.final_message, 'fixture Claude final'); + assert.equal(claude.events.length, 3); + const firstClaudeArgs = fs.readFileSync(path.join(root, 'claude-args'), 'utf8'); + assert.match(firstClaudeArgs, /--session-id\s+new-claude-session-001/); + + const resumedClaudeStart = await request(running.port, 'POST', '/turn', { + bearer: token, + body: { + agent: 'claude', + session_key: 'events-claude', + session_id: 'fixture-claude-session', + resume: true, + prompt: 'claude follow-up turn' + } + }); + assert.equal(resumedClaudeStart.status, 202, resumedClaudeStart.body); + await waitForJob(running, 'turn', JSON.parse(resumedClaudeStart.body).id); + const claudeArgs = fs.readFileSync(path.join(root, 'claude-args'), 'utf8'); + assert.match(claudeArgs, /--resume\s+fixture-claude-session/); + + const invalidResume = await request(running.port, 'POST', '/turn', { + bearer: token, + body: { + agent: 'claude', + session_key: 'invalid-resume', + resume: true, + prompt: 'must not start' + } + }); + assert.equal(invalidResume.status, 400); + + const codexStart = await request(running.port, 'POST', '/turn', { + bearer: token, + body: { + agent: 'codex', + session_key: 'events-codex', + session_id: 'resume-thread-001', + resume: true, + prompt: 'codex fixture prompt' + } + }); + assert.equal(codexStart.status, 202, codexStart.body); + const codex = await waitForJob(running, 'turn', JSON.parse(codexStart.body).id); + assert.equal(codex.native_session_id, 'fixture-codex-thread'); + assert.equal(codex.final_message, 'fixture Codex final'); + assert.equal(codex.events.length, 4); + assert.equal( + fs.readFileSync(path.join(root, 'codex-args'), 'utf8').trim(), + 'exec\nresume\n--json\nresume-thread-001\n--dangerously-bypass-approvals-and-sandbox' + ); +}); diff --git a/spikes/substrate-workspace-adapter/tests/fixtures/native/claude-stream-json.jsonl b/spikes/substrate-workspace-adapter/tests/fixtures/native/claude-stream-json.jsonl new file mode 100644 index 0000000..5d05fa4 --- /dev/null +++ b/spikes/substrate-workspace-adapter/tests/fixtures/native/claude-stream-json.jsonl @@ -0,0 +1,3 @@ +{"type":"system","subtype":"init","session_id":"fixture-claude-session","model":"claude-fixture"} +{"type":"assistant","message":{"content":[{"type":"text","text":"fixture Claude response"}]}} +{"type":"result","subtype":"success","session_id":"fixture-claude-session","result":"fixture Claude final"} diff --git a/spikes/substrate-workspace-adapter/tests/fixtures/native/codex-jsonl.jsonl b/spikes/substrate-workspace-adapter/tests/fixtures/native/codex-jsonl.jsonl new file mode 100644 index 0000000..c2da5e8 --- /dev/null +++ b/spikes/substrate-workspace-adapter/tests/fixtures/native/codex-jsonl.jsonl @@ -0,0 +1,4 @@ +{"type":"thread.started","thread_id":"fixture-codex-thread"} +{"type":"turn.started","thread_id":"fixture-codex-thread"} +{"type":"item.completed","item":{"id":"fixture-message-id","type":"agent_message","text":"fixture Codex final"}} +{"type":"turn.completed","thread_id":"fixture-codex-thread"} diff --git a/spikes/substrate-workspace-adapter/tools/README-round3-egress.md b/spikes/substrate-workspace-adapter/tools/README-round3-egress.md new file mode 100644 index 0000000..574b939 --- /dev/null +++ b/spikes/substrate-workspace-adapter/tools/README-round3-egress.md @@ -0,0 +1,108 @@ +# Round 3 egress helpers + +These source files are live-only Substrate preview scaffolding, not production +credential-provider code. + +Run these from the pinned Substrate checkout. Credential injection requires the Envoy +dataplane; `ate-setup` rejects the injection flag with agentgateway. The `0f9635ae` +preview used Envoy with sdsmint and both credential-provider overrides: + +```sh +cd "$SUBSTRATE_SRC" +export VERSION=0f9635ae +export KO_DOCKER_REPO=localhost:5001 +KUBECONFIG_PATH=/tmp/substrate-preview-kubeconfig + +go run ./cmd/ate-setup --kind --kubeconfig "$KUBECONFIG_PATH" --context kind-substrate-preview \ + --atenet-dataplane envoy --experimental-use-sdsmint --experimental-egress-credential-injection \ + --credential-provider-name ate-secret://kubernetes.io \ + --credential-provider-address credprovider.mainloop-control.svc:50051 \ + deploy ate-system +go run ./cmd/ate-setup --kind --kubeconfig "$KUBECONFIG_PATH" --context kind-substrate-preview \ + --atenet-dataplane envoy --experimental-use-sdsmint --experimental-egress-credential-injection \ + --credential-provider-name ate-secret://kubernetes.io \ + --credential-provider-address credprovider.mainloop-control.svc:50051 \ + deploy atenet +``` + +At fork commit `0f9635ae`, the defaults are `ate-secret://k8s.io` and +`k8s-credential-provider.ate-system.svc:50051`. A redeploy without the overrides +breaks Claude's provider preflight with HTTP 500: +`credential URI names a provider this gateway does not serve`. + +- `round3-credprovider/main.go` implements a temporary mTLS gRPC credential + provider and HTTPS echo endpoint. It reads only a Kubernetes Secret created + specifically for this test. The echo endpoint compares the injected header + to that dummy value and returns a boolean result; it never returns the value. +- `round3-egress-injection/main.go` updates one actor's egress policy to inject + a credential for one hostname. `--prefix` supports bearer-token headers; + omit it for the dummy echo check. +- `round3-claude-provider/main.go` is the separate Phase 3e preview provider. + It keeps the Claude Secret URI pinned, requires the expected actor SPIFFE ID + through `EXPECTED_ACTOR_SPIFFE_ID`, reads the credential from a read-only + Secret mount, and logs only a success marker. It is test-run scaffolding, not + a general-purpose or production provider. + +The `patched-next` adapter follows the current API shape: actor template UIDs are +read from `status.externalSnapshot.actorTemplateUid`; ActorTemplates use +`wakeupProbe` and `snapshotConfig`; and egress-policy updates carry the current +`metadata.uid` and `metadata.version` preconditions. The API has no egress-policy +delete operation, so the Claude proof revokes injected credentials by updating +the policy to an empty rule set. + +Build from the pinned Substrate checkout (`0f9635aed37bd5dde604a9bca1975421cd07181a`), +where the imported internal packages and protobuf modules are available: + +```sh +CGO_ENABLED=0 GOFLAGS=-mod=vendor go build -o /tmp/round3-credprovider ./cmd/round3-credprovider +CGO_ENABLED=0 GOFLAGS=-mod=vendor go build -o /tmp/round3-egress-injection ./cmd/round3-egress-injection +``` + +## Build and push the Lane A Claude provider image + +Run this block from the Mainloop worktree root with access to pull the builder +and distroless images and push to the local registry. It makes a temporary +checkout of Substrate at `0f9635ae`, stages this provider source and Dockerfile +there, builds a static nonroot image, pushes a unique tag, then reads the digest +from the registry. The expected actor SPIFFE ID is supplied at runtime by the +Lane A Deployment, not baked into this image. + +```sh +set -euo pipefail +MAINLOOP_ROOT=$(git rev-parse --show-toplevel) +BUILD_ROOT=$(mktemp -d /tmp/round3-claude-provider-build.XXXXXX) +trap 'rm -rf "$BUILD_ROOT"' EXIT +: "${SUBSTRATE_SRC:?set SUBSTRATE_SRC to the pinned Substrate checkout path}" +SUBSTRATE_TMP=$BUILD_ROOT/substrate +SUBSTRATE_COMMIT=0f9635aed37bd5dde604a9bca1975421cd07181a +IMAGE_TAG=0f9635ae-lane-a +IMAGE=localhost:5001/round3-claude-provider:$IMAGE_TAG + +mkdir -p "$SUBSTRATE_TMP" +test "$(git -C "$SUBSTRATE_SRC" rev-parse "$SUBSTRATE_COMMIT^{commit}")" = "$SUBSTRATE_COMMIT" +git -C "$SUBSTRATE_SRC" archive "$SUBSTRATE_COMMIT" | tar -x -C "$SUBSTRATE_TMP" +mkdir -p "$SUBSTRATE_TMP/tools/round3-claude-provider" +cp "$MAINLOOP_ROOT/spikes/substrate-workspace-adapter/tools/round3-claude-provider/main.go" \ + "$SUBSTRATE_TMP/tools/round3-claude-provider/main.go" +cp "$MAINLOOP_ROOT/spikes/substrate-workspace-adapter/tools/round3-claude-provider/Dockerfile" \ + "$SUBSTRATE_TMP/tools/round3-claude-provider/Dockerfile" + +cd "$SUBSTRATE_TMP" +docker build --pull --platform=linux/amd64 \ + -f tools/round3-claude-provider/Dockerfile \ + -t "$IMAGE" . +docker push "$IMAGE" + +DIGEST=$(curl --fail --silent --show-error --head --max-time 15 \ + -H 'Accept: application/vnd.oci.image.index.v1+json, application/vnd.docker.distribution.manifest.list.v2+json, application/vnd.oci.image.manifest.v1+json, application/vnd.docker.distribution.manifest.v2+json' \ + "http://localhost:5001/v2/round3-claude-provider/manifests/$IMAGE_TAG" | + awk 'tolower($1) == "docker-content-digest:" { gsub("\r", "", $2); print $2; exit }') +[[ $DIGEST =~ ^sha256:[0-9a-f]{64}$ ]] +printf 'CLAUDE_PROVIDER_IMAGE=localhost:5001/round3-claude-provider@%s\n' "$DIGEST" +``` + +Use only a throwaway dummy Secret with the first provider in a disposable +preview cluster. Set `EXPECTED_ACTOR_SPIFFE_ID` in the provider Deployment to +the one authorized actor ID. Pass the provider's credential only as a read-only +Secret mount. Never put a credential in source, logs, actor commands, or echo +responses. diff --git a/spikes/substrate-workspace-adapter/tools/round3-claude-provider/Dockerfile b/spikes/substrate-workspace-adapter/tools/round3-claude-provider/Dockerfile new file mode 100644 index 0000000..a090bd0 --- /dev/null +++ b/spikes/substrate-workspace-adapter/tools/round3-claude-provider/Dockerfile @@ -0,0 +1,22 @@ +FROM golang:1.27.1-bookworm AS build + +WORKDIR /src +COPY go.mod go.sum ./ +COPY vendor/ ./vendor/ +COPY pkg/proto/credproviderpb/ ./pkg/proto/credproviderpb/ +COPY tools/round3-claude-provider/ ./tools/round3-claude-provider/ + +ENV CGO_ENABLED=0 \ + GOOS=linux \ + GOFLAGS=-mod=vendor + +RUN mkdir -p /out && \ + go build -trimpath -ldflags='-s -w' \ + -o /out/round3-claude-provider ./tools/round3-claude-provider + +FROM gcr.io/distroless/static-debian12:nonroot + +COPY --from=build /out/round3-claude-provider /round3-claude-provider +USER 65532:65532 +EXPOSE 50051 +ENTRYPOINT ["/round3-claude-provider"] diff --git a/spikes/substrate-workspace-adapter/tools/round3-claude-provider/main.go b/spikes/substrate-workspace-adapter/tools/round3-claude-provider/main.go new file mode 100644 index 0000000..a155802 --- /dev/null +++ b/spikes/substrate-workspace-adapter/tools/round3-claude-provider/main.go @@ -0,0 +1,82 @@ +package main + +import ( + "context" + "crypto/tls" + "crypto/x509" + "log" + "net" + "os" + "strings" + + "google.golang.org/grpc" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/credentials" + "google.golang.org/grpc/status" + + "github.com/agent-substrate/substrate/pkg/proto/credproviderpb" +) + +const ( + expectedURI = "ate-secret://kubernetes.io/mainloop-control/claude-oauth/oauth-token" + credentialPath = "/run/claude/oauth-token" + servingBundlePath = "/run/servicedns/credential-bundle.pem" + clientCAPath = "/run/podidentity-ca/trust-bundle.pem" +) + +type provider struct { + credproviderpb.UnimplementedCredentialProviderServer + expectedActorSPIFFEID string +} + +func (p provider) FetchSecret(_ context.Context, req *credproviderpb.FetchSecretRequest) (*credproviderpb.FetchSecretResponse, error) { + if req.GetUri() != expectedURI || req.GetActorSpiffeId() != p.expectedActorSPIFFEID { + return nil, status.Error(codes.PermissionDenied, "credential request rejected") + } + value, err := os.ReadFile(credentialPath) + if err != nil { + log.Printf("credential_fetch=unavailable actor_identity_match=true") + return nil, status.Error(codes.Unavailable, "credential unavailable") + } + if len(value) == 0 { + return nil, status.Error(codes.NotFound, "credential unavailable") + } + log.Printf("credential_fetch=ok actor_identity_match=true credential_kind=claude-oauth") + return &credproviderpb.FetchSecretResponse{OpaqueBytes: value}, nil +} + +func main() { + expectedActorSPIFFEID := strings.TrimSpace(os.Getenv("EXPECTED_ACTOR_SPIFFE_ID")) + if expectedActorSPIFFEID == "" { + log.Fatal("EXPECTED_ACTOR_SPIFFE_ID is required") + } + + servingCert, err := tls.LoadX509KeyPair(servingBundlePath, servingBundlePath) + if err != nil { + log.Fatal("serving certificate unavailable") + } + caBytes, err := os.ReadFile(clientCAPath) + if err != nil { + log.Fatal("client CA unavailable") + } + clientCAs := x509.NewCertPool() + if !clientCAs.AppendCertsFromPEM(caBytes) { + log.Fatal("client CA bundle invalid") + } + tlsConfig := &tls.Config{ + MinVersion: tls.VersionTLS12, + Certificates: []tls.Certificate{servingCert}, + ClientAuth: tls.RequireAndVerifyClientCert, + ClientCAs: clientCAs, + } + grpcServer := grpc.NewServer(grpc.Creds(credentials.NewTLS(tlsConfig))) + credproviderpb.RegisterCredentialProviderServer(grpcServer, provider{expectedActorSPIFFEID: expectedActorSPIFFEID}) + listener, err := net.Listen("tcp", ":50051") + if err != nil { + log.Fatal("gRPC listener unavailable") + } + log.Printf("credential_provider_ready=true") + if err := grpcServer.Serve(listener); err != nil { + log.Fatal("gRPC server failed") + } +} diff --git a/spikes/substrate-workspace-adapter/tools/round3-credprovider/main.go b/spikes/substrate-workspace-adapter/tools/round3-credprovider/main.go new file mode 100644 index 0000000..a08dbb7 --- /dev/null +++ b/spikes/substrate-workspace-adapter/tools/round3-credprovider/main.go @@ -0,0 +1,95 @@ +package main + +import ( + "bytes" + "context" + "crypto/tls" + "crypto/x509" + "io" + "log" + "net" + "net/http" + "os" + "strings" + + "google.golang.org/grpc" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/credentials" + "google.golang.org/grpc/status" + + "github.com/agent-substrate/substrate/pkg/proto/credproviderpb" +) + +const expectedURI = "ate-secret://kubernetes.io/ate-system/round3-dummy/token" + +type provider struct { + credproviderpb.UnimplementedCredentialProviderServer + secretPath string +} + +func (p provider) FetchSecret(_ context.Context, req *credproviderpb.FetchSecretRequest) (*credproviderpb.FetchSecretResponse, error) { + if req.GetUri() != expectedURI || !strings.HasPrefix(req.GetActorSpiffeId(), "spiffe://") { + return nil, status.Error(codes.PermissionDenied, "dummy provider request rejected") + } + value, err := os.ReadFile(p.secretPath) + if err != nil { + return nil, status.Error(codes.Unavailable, "dummy value unavailable") + } + log.Printf("credential_fetch=ok actor_identity_present=true") + return &credproviderpb.FetchSecretResponse{OpaqueBytes: bytes.TrimSpace(value)}, nil +} + +func main() { + bundle := "/run/servicedns/credential-bundle.pem" + servingCert, err := tls.LoadX509KeyPair(bundle, bundle) + if err != nil { + log.Fatal("serving certificate unavailable") + } + caBytes, err := os.ReadFile("/run/podidentity-ca/trust-bundle.pem") + if err != nil { + log.Fatal("client CA unavailable") + } + clientCAs := x509.NewCertPool() + if !clientCAs.AppendCertsFromPEM(caBytes) { + log.Fatal("client CA bundle invalid") + } + tlsConfig := &tls.Config{MinVersion: tls.VersionTLS12, Certificates: []tls.Certificate{servingCert}, ClientAuth: tls.RequireAndVerifyClientCert, ClientCAs: clientCAs} + grpcServer := grpc.NewServer(grpc.Creds(credentials.NewTLS(tlsConfig))) + credproviderpb.RegisterCredentialProviderServer(grpcServer, provider{secretPath: "/run/dummy/token"}) + listener, err := net.Listen("tcp", ":50051") + if err != nil { + log.Fatal("gRPC listener unavailable") + } + go func() { + log.Printf("credential_provider_ready=true") + if err := grpcServer.Serve(listener); err != nil { + log.Fatal("gRPC server failed") + } + }() + http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("content-type", "text/plain") + if r.URL.Path == "/readyz" { + w.WriteHeader(http.StatusNoContent) + return + } + want, err := os.ReadFile("/run/dummy/token") + if err != nil { + http.Error(w, "dummy value unavailable", http.StatusServiceUnavailable) + return + } + if bytes.Equal(bytes.TrimSpace(want), []byte(r.Header.Get("X-Mainloop-Dummy"))) { + _, _ = io.WriteString(w, "injected-header-matched") + return + } + w.WriteHeader(http.StatusForbidden) + _, _ = io.WriteString(w, "injected-header-mismatch") + }) + server := &http.Server{ + Addr: ":8443", + Handler: nil, + TLSConfig: &tls.Config{MinVersion: tls.VersionTLS12, Certificates: []tls.Certificate{servingCert}}, + } + if err := server.ListenAndServeTLS("", ""); err != nil { + log.Fatal("TLS echo listener unavailable") + } +} diff --git a/spikes/substrate-workspace-adapter/tools/round3-egress-injection/main.go b/spikes/substrate-workspace-adapter/tools/round3-egress-injection/main.go new file mode 100644 index 0000000..9b4b6a6 --- /dev/null +++ b/spikes/substrate-workspace-adapter/tools/round3-egress-injection/main.go @@ -0,0 +1,44 @@ +package main + +import ( + "context" + "flag" + "fmt" + "log" + + "github.com/agent-substrate/substrate/internal/ateclient" + "github.com/agent-substrate/substrate/internal/resources" + "github.com/agent-substrate/substrate/pkg/proto/ateapipb" +) + +func main() { + kubeconfig := flag.String("kubeconfig", "", "") + contextName := flag.String("context", "", "") + atespace := flag.String("atespace", "", "") + actor := flag.String("actor", "", "") + hostname := flag.String("hostname", "", "") + header := flag.String("header", "", "") + prefix := flag.String("prefix", "", "") + uri := flag.String("credential-uri", "", "") + flag.Parse() + ctx := context.Background() + cli, err := ateclient.NewClient(ctx, *kubeconfig, *contextName, "", "", false) + if err != nil { + log.Fatal("ateapi client unavailable") + } + defer cli.Close() + ref := resources.ActorRef{Atespace: *atespace, Name: *actor}.ToObjectRef() + existing, err := cli.GetActorEgressPolicy(ctx, &ateapipb.GetActorEgressPolicyRequest{Actor: ref}) + if err != nil { + log.Fatal("actor egress policy unavailable") + } + policy := &ateapipb.EgressPolicy{Metadata: existing.GetMetadata(), Rules: []*ateapipb.EgressRule{{ + Hostnames: &ateapipb.HostnameRule{Patterns: []string{*hostname}, Effects: &ateapipb.EgressRuleEffects{ + InjectStaticHeaders: []*ateapipb.CredentialHeaderInjection{{Header: *header, Prefix: *prefix, CredentialUri: *uri}}, + }}, + }}} + if _, err := cli.UpdateActorEgressPolicy(ctx, &ateapipb.UpdateActorEgressPolicyRequest{Actor: ref, EgressPolicy: policy}); err != nil { + log.Fatal("injection policy update failed") + } + fmt.Println("egress_header_injection_policy=updated") +} diff --git a/spikes/substrate-workspace-adapter/tools/router-client.js b/spikes/substrate-workspace-adapter/tools/router-client.js new file mode 100644 index 0000000..bb29c97 --- /dev/null +++ b/spikes/substrate-workspace-adapter/tools/router-client.js @@ -0,0 +1,111 @@ +// Small control-plane probe for the Substrate CONNECT router. Request data, including any +// bearer token or credential body, is read from stdin so it never appears in argv or logs. +'use strict'; + +const http = require('node:http'); + +let input = ''; +process.stdin.setEncoding('utf8'); +process.stdin.on('data', (chunk) => { + input += chunk; + if (input.length > 1_200_000) process.stdin.destroy(); +}); +process.stdin.on('end', () => { + let request; + try { + request = JSON.parse(input); + } catch { + process.stdout.write('{"error":"invalid request"}\n'); + process.exitCode = 2; + return; + } + + const targetPort = Number(request.targetPort || 8090); + if ( + !/^[a-z0-9-]+$/.test(request.atespace || '') || + !/^[a-z0-9-]+$/.test(request.actor || '') || + !Number.isInteger(targetPort) || + targetPort < 1 || + targetPort > 65535 || + !['GET', 'POST'].includes(request.method) || + typeof request.path !== 'string' || + !request.path.startsWith('/') + ) { + process.stdout.write('{"error":"invalid request"}\n'); + process.exitCode = 2; + return; + } + + let completed = false; + const finish = (result) => { + if (completed) return; + completed = true; + process.stdout.write(`${JSON.stringify(result)}\n`); + }; + + const tunnel = http.request({ + hostname: 'atenet-router.ate-system.svc.cluster.local', + port: 8081, + method: 'CONNECT', + path: `actor-upstream:${targetPort}`, + headers: { + host: `actor-upstream:${targetPort}`, + 'ate-target-actor': `${request.atespace}/${request.actor}` + }, + timeout: 8000 + }); + tunnel.on('connect', (response, socket) => { + if (response.statusCode !== 200) { + socket.destroy(); + finish({ connectStatus: response.statusCode }); + return; + } + + const headers = { connection: 'close', host: `actor-upstream:${targetPort}` }; + let body = Buffer.alloc(0); + if (request.body !== undefined) { + body = Buffer.from(JSON.stringify(request.body)); + headers['content-type'] = 'application/json'; + headers['content-length'] = String(body.length); + } else if (typeof request.rawBody === 'string') { + body = Buffer.from(request.rawBody); + headers['content-type'] = 'application/octet-stream'; + headers['content-length'] = String(body.length); + } + if (typeof request.bearerToken === 'string') { + headers.authorization = `Bearer ${request.bearerToken}`; + } + const headerLines = Object.entries(headers).map(([name, value]) => `${name}: ${value}`); + const requestHeader = Buffer.from( + `${request.method} ${request.path} HTTP/1.1\r\n${headerLines.join('\r\n')}\r\n\r\n` + ); + const responseChunks = []; + if (response.head && response.head.length) responseChunks.push(response.head); + socket.setTimeout(15000, () => socket.destroy(new Error('response timeout'))); + socket.on('data', (chunk) => responseChunks.push(chunk)); + socket.on('end', () => { + const responseBytes = Buffer.concat(responseChunks); + const headerEnd = responseBytes.indexOf('\r\n\r\n'); + if (headerEnd < 0) { + finish({ transportError: 'invalid-http-response', receivedBytes: responseBytes.length }); + return; + } + const statusLine = responseBytes.subarray(0, headerEnd).toString('latin1').split('\r\n')[0]; + const status = Number(statusLine.split(' ')[1]); + const responseBody = responseBytes.subarray(headerEnd + 4).toString('utf8'); + finish(request.includeResponseBody ? { status, body: responseBody } : { status }); + }); + socket.on('error', (err) => finish({ transportError: err.code || 'request-failed' })); + socket.write(Buffer.concat([requestHeader, body])); + }); + tunnel.on('response', (response) => { + response.resume(); + finish({ connectStatus: response.statusCode }); + }); + tunnel.on('timeout', () => { + tunnel.destroy(); + finish({ transportError: 'connect-timeout' }); + }); + tunnel.on('error', (err) => finish({ transportError: err.code || 'connect-failed' })); + tunnel.end(); +});