diff --git a/.github/workflows/security-scans.yaml b/.github/workflows/security-scans.yaml index 2c4aad4ef..77740b4c3 100644 --- a/.github/workflows/security-scans.yaml +++ b/.github/workflows/security-scans.yaml @@ -44,7 +44,21 @@ jobs: # version exists (affected <= 1.5.9, crewai pins chromadb < 1.2). The vulnerability # requires running a chromadb HTTP server with trust_remote_code=true, which the # github_agent demo does not do. See demo/agents/github_agent/pyproject.toml. - allow-ghsas: GHSA-f4j7-r4q5-qw2c + # + # keycloak-services (aiac/keycloak-spi, provided-scope): these are Keycloak *server* + # advisories, not code shipped in our provider jar (only jnats is shaded). They are + # remediated operationally by patching the Keycloak deployment; our pom + Dockerfile + # already track a patched line (26.6.4, see keycloak-spi/pom.xml). The three below have + # NO released fix (affected <= 26.6.4, patched version "None"), so they cannot be + # resolved by a version bump: + # GHSA-wcvj-vpvw-9rr5 Improper Validation of Consistency within Input + # GHSA-p3v8-fm5p-v84h Improper Verification of Cryptographic Signature + # GHSA-q6h7-xxp7-7429 Authentication Bypass by Primary Weakness + allow-ghsas: >- + GHSA-f4j7-r4q5-qw2c, + GHSA-wcvj-vpvw-9rr5, + GHSA-p3v8-fm5p-v84h, + GHSA-q6h7-xxp7-7429 shellcheck: name: Shell Script Lint diff --git a/aiac/CLAUDE.md b/aiac/CLAUDE.md index 0a6e04e55..48ce9b7ae 100644 --- a/aiac/CLAUDE.md +++ b/aiac/CLAUDE.md @@ -1,6 +1,6 @@ # AIAC Codebase Guide -All paths below are relative to `kagenti-extensions/aiac/`. +All paths below are relative to `cortex/aiac/`. ## Requirements / PRD docs @@ -50,23 +50,18 @@ Per-task handoff documents live under `docs/handoffs/` — one markdown file per ## Source code -`src/aiac/` — Python package root (`__init__.py` is empty). +`src/aiac/` — Python package root (`__init__.py` is empty). It is organized by +subsystem: an IdP configuration layer, a PDP policy-writer layer, the AIAC Agent +layer (built on the SPM/APM model — a Controller dispatching to use-case +sub-agents and a policy-rules builder), and a two-layer policy stack (models, a +model store, and the Policy Computation Engine). -Key stable structure: -- `idp/` — IdP configuration service and models -- `pdp/` — PDP policy writer service and library -- `agent/` — the AIAC Agent layer (rebuilt on the SPM/APM model): - - `agent/controller/` — FastAPI Controller (`routes.py` + Dockerfile); `/apply/*` routes dispatch to the UC sub-agents and make the single `compute_and_apply` (PCE) call - - `agent/uc/` — use-case sub-agents: `onboarding/` (provision + policy_builder + orchestrator), `policy_update/` (build/rebuild), `role_update/` - - `agent/policy_rules_builder/` — PRB: `build_role_rules` / `build_scope_rules` emit `list[PolicyRule]` - - `agent/shared/` — shared helpers (`roles.py`, e.g. `flatten_role`) - - `agent/onboarding.old/` — archived prior implementation (built on the superseded `ProposedDiff` model); not part of the active build -- `policy/` — the two-layer policy stack (all implemented): - - `policy/model/` — `PolicyRule`, `ServicePolicyModel` (SPM), `AgentPolicyModel` (APM), `PolicyModel` - - `policy/store/` — Policy Store service + library (SPM CRUD) - - `policy/computation/` — Policy Computation Engine (`compute_and_apply`, SPM-based) +Discover the concrete layout live rather than relying on a memorized tree: -For current file list, `ls` or `find` under `src/aiac/`. +```bash +find src/aiac -maxdepth 2 -type d # subsystems and their immediate children +ls src/aiac// # drill into any layer +``` ## Tests @@ -78,24 +73,32 @@ For current file list, `ls` or `find` under `src/aiac/`. .venv/bin/pytest test/ -m "not integration" ``` -The whole `test/` tree (including `test/policy/`) collects and runs green. The Policy -Computation Engine (`aiac.policy.computation.engine`) was migrated to the SPM store surface in -**Wave 3 / Handoff 05**, so the earlier PCE-chain collection failures (which required ignoring -`test/policy/computation`, `test/agent/controller/test_routes.py`, and -`test/integration/test_policy_pipeline.py`) are resolved — no `--ignore` flags are needed. - -Use `ls test/` to discover current test directories. - -**Integration tests** (`-m integration`) need live config — Keycloak + admin creds + an LLM -endpoint (`opa` on PATH for the policy-pipeline suite). Those variables live in +The whole `test/` tree collects and runs green — no `--ignore` flags are needed. +(This wasn't always true: the Policy Computation Engine was migrated to the SPM +store surface in Wave 3, which resolved the earlier PCE-chain collection +failures.) + +Use `ls test/` / `find test -type d` to discover current test directories. + +**Integration tests** (`-m integration`) now close the **real OPA evaluation loop** — they onboard +through the in-cluster Controller, then drive real HTTP requests **through AuthBridge** and assert the +**deployed OPA plugin's** allow/deny (no `opa eval`, no `.rego` dump, so `opa` on PATH is no longer +needed). They therefore need a live **rossoctl/Kind cluster with the AuthBridge OPA pipeline wired +into both legs** (the demo `github-agent`/`github-tool` deployed + registered), plus Keycloak admin +creds and an LLM endpoint for onboarding. Stand the pipeline up with `k8s/opa-kind-enable.sh`; +the full prerequisites, wiring, and manual probe commands are in `k8s/opa-kind-runbook.md`, and the +per-loop shape is documented in `test/integration/uc1_onboard.py`. Config lives in `test/integration/.env` (gitignored): `LLM_BASE_URL`, `LLM_API_KEY`, `LLM_MODEL`, `KEYCLOAK_URL`, `KEYCLOAK_ADMIN_USERNAME`, `KEYCLOAK_ADMIN_PASSWORD`. Source it before running: ```bash +k8s/opa-kind-enable.sh # one-time: wire the OPA plugin into the Kind cluster set -a; . test/integration/.env; set +a .venv/bin/pytest test/integration/ -m integration ``` +When the cluster is not wired or the env is unset, the suite **skips cleanly** (it never false-passes). + **Smoke test** (requires live service at `AIAC_PDP_CONFIG_URL`, default `http://127.0.0.1:7071`): ```bash @@ -106,10 +109,10 @@ Exercises all `Configuration` methods — run `ls test/idp/configuration/` to se ## Python environment -Virtual environment: `kagenti-extensions/aiac/.venv` +Virtual environment: `cortex/aiac/.venv` -Activate: `source kagenti-extensions/aiac/.venv/bin/activate` -Run directly: `kagenti-extensions/aiac/.venv/bin/python` / `kagenti-extensions/aiac/.venv/bin/pytest` +Activate: `source cortex/aiac/.venv/bin/activate` +Run directly: `cortex/aiac/.venv/bin/python` / `cortex/aiac/.venv/bin/pytest` Always use this venv for any Python execution, test runs, or dependency checks. @@ -117,15 +120,17 @@ Always use this venv for any Python execution, test runs, or dependency checks. Config: `k8s/`, `pyproject.toml`, `pyrightconfig.json` -Docker images: +Docker images: each service ships a `Dockerfile` next to its service code (build +context is `src/`, except `rag-ingest/`, which is a separate top-level +directory). Discover the current set of images and their Dockerfiles live: -| Image | Dockerfile location | -|-------|-------------------| -| `aiac-agent` | `src/aiac/agent/controller/Dockerfile` (build context `src/`) | -| `aiac-pdp-config` | `src/aiac/idp/service/configuration/keycloak/Dockerfile` | -| `aiac-pdp-policy-opa` | `src/aiac/pdp/service/policy/opa/Dockerfile` | -| `aiac-policy-store` | `src/aiac/policy/store/service/Dockerfile` | -| `aiac-rag-ingest` | `rag-ingest/` (separate directory) | +```bash +find src -name Dockerfile # per-service Dockerfiles under src/ +ls rag-ingest/ # the out-of-tree ingest image +``` + +Image names and build contexts are declared in the build/CI config and the +`k8s/` manifests — grep there for the authoritative name→Dockerfile mapping. ### Non-root container / volume-ownership pattern @@ -152,9 +157,12 @@ spec: fsGroup: 10001 # makes the mounted volume group-writable by UID 10001 ``` -Services requiring this today: -- **Policy store** — PVC at `/data` (SQLite backend), `policy-store-statefulset.yaml` -- **PDP policy OPA writer** — emptyDir at `/rego` (`REGO_OUTPUT_DIR`), `pdp-interface-deployment.yaml` +This applies to any service that writes to a mounted volume (a PVC or an +emptyDir). Find them live by grepping the manifests for volume mounts / claims: + +```bash +grep -rlniE 'volumeMounts|volumeClaimTemplates|emptyDir|persistentVolumeClaim' k8s/ +``` Services that mount no volumes still need the Dockerfile `USER` directive; the pod-level `fsGroup`/volume-chown block above is only required for those that @@ -196,7 +204,7 @@ demo workloads, which don't) and CPU/memory requests + limits. ## External references -- [Kagenti Developer Guide](https://github.com/kagenti/kagenti/blob/main/docs/dev-guide.md) — upstream Kagenti dev guide: per-persona workflows (agent, tool, extensions developers, MCP gateway operators), Git/PR process, pre-commit hooks, feature flags, local Kagenti UI v2 development (React frontend + FastAPI backend, building/deploying images to Kubernetes), and HyperShift-based testing on ephemeral OpenShift clusters (cluster lifecycle, cost management, troubleshooting). +- [Rosso Developer Guide](https://github.com/rossoctl/rossoctl/blob/main/docs/dev-guide.md) — upstream Rosso dev guide (formerly the Kagenti Developer Guide; Kagenti was renamed to Rosso): per-persona workflows (agent, tool, extensions developers, MCP gateway operators), Git/PR process, pre-commit hooks, feature flags (all new features must be gated behind a disabled-by-default `ROSSOCTL_FEATURE_FLAG_` flag), local Rosso UI v2 development (React frontend + FastAPI backend, building/deploying images to Kubernetes), and HyperShift-based testing on ephemeral OpenShift clusters (cluster lifecycle, cost management, troubleshooting). ## Agent skills diff --git a/aiac/demo/agents/github_agent/.dockerignore b/aiac/demo/agents/github_agent/.dockerignore new file mode 100644 index 000000000..287f001bc --- /dev/null +++ b/aiac/demo/agents/github_agent/.dockerignore @@ -0,0 +1,7 @@ +.venv + +# Secrets — never bake into the image +.env + +# `expect` scripts +test_startup.exp diff --git a/aiac/demo/agents/github_agent/.env.template b/aiac/demo/agents/github_agent/.env.template new file mode 100644 index 000000000..65b53af09 --- /dev/null +++ b/aiac/demo/agents/github_agent/.env.template @@ -0,0 +1,28 @@ +# Github Agent - configuration template +# Copy to .env and fill in your values. + +# LLM configuration +TASK_MODEL_ID=ollama/ibm/granite4:latest +LLM_API_BASE=http://host.docker.internal:11434 +LLM_API_KEY=my_api_key +MODEL_TEMPERATURE=0 + +# MCP Tool endpoint +MCP_URL=http://github-tool-mcp:9090/mcp + +# Agent service +PORT=8000 +LOG_LEVEL=INFO + +# Optional: static GitHub PAT passed to MCP as Bearer token. +# If unset, the inbound Authorization header (AuthBridge path) is forwarded instead. +GITHUB_TOKEN= + +# Optional: override the URL advertised in the agent card +AGENT_ENDPOINT= + +# Optional: override the curated tool allow-list (comma-separated tool names) +ENABLED_TOOLS= + +# Optional: expected issuer of inbound JWTs (informational) +ISSUER= diff --git a/aiac/demo/agents/github_agent/Dockerfile b/aiac/demo/agents/github_agent/Dockerfile new file mode 100644 index 000000000..04c77f077 --- /dev/null +++ b/aiac/demo/agents/github_agent/Dockerfile @@ -0,0 +1,15 @@ +FROM ghcr.io/astral-sh/uv:python3.12-bookworm-slim +ARG RELEASE_VERSION="main" + +WORKDIR /app +COPY . . +RUN uv sync --no-cache --locked --link-mode copy + +ENV PRODUCTION_MODE=True \ + HOME=/app \ + RELEASE_VERSION=${RELEASE_VERSION} + +RUN chown -R 1001:1001 /app +USER 1001 + +CMD ["uv", "run", "--no-sync", "server"] diff --git a/aiac/demo/agents/github_agent/README.md b/aiac/demo/agents/github_agent/README.md new file mode 100644 index 000000000..3eaf180bc --- /dev/null +++ b/aiac/demo/agents/github_agent/README.md @@ -0,0 +1,113 @@ +# github-agent + +An autonomous A2A agent that acts on a user's behalf against GitHub **source repositories** and an **issue/PR tracker**, using the [`github-tool-mcp`](https://github.com/kagenti/kagenti-extensions) MCP server. + +This agent implements the canonical `github-agent` used by the AIAC policy-pipeline integration test — the two skills match the policy scenario's `source_operations` and `issue_operations` roles. + +## Skills + +| Skill id | Name | Description | +|---|---|---| +| `source_operations` | Source repository operations | Browse and search code; read, create, and modify repository file contents, branches, and commits. | +| `issue_operations` | Issue & PR tracker operations | Read, search, create, and update issues, comments, sub-issues, and pull requests. | + +## Prerequisite: `github-tool-mcp` (production tool) + +This agent connects to **`github-tool-mcp:9090/mcp`** — the production 44-tool MCP server — at `MCP_URL`. +Deploy it before starting the agent: + +``` +authbridge/demos/github-issue/k8s/github-tool-deployment.yaml +``` + +> **Not the same as `demo/tools/github_tool/`.** +> `demo/tools/github_tool/` is a simplified 4-tool stub (`source-read`, `source-write`, `issues-read`, +> `issues-write`) deployed as Service `github-tool` for **UC-1 onboarding discovery** only. +> The agent never connects to it — it connects to the production `github-tool-mcp` server which +> exposes the 44-tool GitHub API federation. + +## Configuration + +All settings are read from environment variables (or a `.env` file). Copy one of the presets: + +| Preset | Description | +|---|---| +| `.env.ollama` | Default — local Ollama (ibm/granite4) | +| `.env.openai` | OpenAI gpt-4o-mini | +| `.env.claude` | Anthropic Claude Sonnet | +| `.env.template` | Documented placeholder for all vars | + +### Variables + +| Variable | Description | Default | +|---|---|---| +| `TASK_MODEL_ID` | litellm model id | `ollama/ibm/granite4:latest` | +| `LLM_API_BASE` | OpenAI-compatible base URL | `http://host.docker.internal:11434` | +| `LLM_API_KEY` | LLM API key | `my_api_key` | +| `MODEL_TEMPERATURE` | Sampling temperature | `0` | +| `EXTRA_HEADERS` | Extra LLM headers (JSON) | `{}` | +| `MCP_URL` | MCP tool endpoint | `http://github-tool-mcp:9090/mcp` | +| `MCP_TIMEOUT` | MCP connect timeout (s) | `600` | +| `ENABLED_TOOLS` | Override the curated tool allow-list (comma-separated) | (unset → default set) | +| `PORT` | A2A listen port | `8000` | +| `LOG_LEVEL` | Log level | `INFO` | +| `GITHUB_TOKEN` | Static Bearer to MCP (else inbound passthrough) | (unset) | +| `ISSUER` | Expected `iss` of inbound JWTs (informational) | (unset) | +| `AGENT_ENDPOINT` | Override the URL advertised in the card | (unset) | + +## Running locally + +```bash +cd aiac/demo/agents/github_agent +cp .env.ollama .env # or another preset +uv sync +uv run server +# In another terminal: +curl -s localhost:8000/.well-known/agent-card.json | python3 -m json.tool +``` + +Optionally, run `expect -f test_startup.exp` instead to check startup automatically. + +## Deploying to Kagenti (Kind cluster) + +Prerequisites: a running Kagenti cluster (Keycloak realm `kagenti`, namespace `team1`) with `github-tool-mcp` already deployed. + +1. **Build the image:** + ```bash + cd aiac/demo/agents/github_agent + podman build -t github-agent:latest . + # or: docker build -t github-agent:latest . + ``` + +2. **Load into the Kind cluster:** + ```bash + kind load docker-image github-agent:latest --name kagenti + ``` + +3. **Apply manifests:** + ```bash + kubectl apply -f k8s/configmaps.yaml + kubectl apply -f k8s/github-agent-deployment.yaml + ``` + +4. **Confirm AuthBridge injection:** + ```bash + kubectl get pod -n team1 -l app.kubernetes.io/name=github-agent -o jsonpath='{.items[0].spec.containers[*].name}' + ``` + You should see the `authbridge-proxy` (or `envoy-proxy`) sidecar alongside `agent`. + +5. **Port-forward and send a message:** + ```bash + kubectl port-forward svc/github-agent 8080:8080 -n team1 & + # Send an A2A message/send request: + curl -s http://localhost:8080/.well-known/agent-card.json | python3 -m json.tool + ``` + +## Architecture + +``` +A2A client ──(JSON-RPC /)──► github-agent (:8000) + │ CrewAI: prereq extract → researcher + └──(streamable-http, MCP_URL)──► github-tool-mcp:9090/mcp ──► GitHub + (AuthBridge sidecar: inbound JWT validation; outbound RFC-8693 token exchange for MCP_URL host) +``` diff --git a/aiac/demo/agents/github_agent/a2a_agent.py b/aiac/demo/agents/github_agent/a2a_agent.py new file mode 100644 index 000000000..bb7703a90 --- /dev/null +++ b/aiac/demo/agents/github_agent/a2a_agent.py @@ -0,0 +1,242 @@ +""" +Module for A2A Agent. +""" + +import asyncio +import logging +import os +import sys +import traceback + +import uvicorn +from crewai_tools import MCPServerAdapter +from crewai_tools.adapters.tool_collection import ToolCollection + +from a2a.server.agent_execution import AgentExecutor, RequestContext +from a2a.server.events.event_queue import EventQueue +from a2a.server.request_handlers import DefaultRequestHandler +from a2a.server.routes import create_agent_card_routes, create_jsonrpc_routes +from a2a.server.tasks import InMemoryTaskStore, TaskUpdater +from a2a.helpers import new_task_from_user_message, new_text_part +from a2a.types import ( + AgentCapabilities, + AgentCard, + AgentInterface, + AgentSkill, + TaskState, + SecurityScheme, + HTTPAuthSecurityScheme, +) +from starlette.applications import Starlette + +from github_agent.config import settings, Settings +from github_agent.event import Event +from github_agent.tools import select_enabled_tools + +logger = logging.getLogger(__name__) +logging.basicConfig(level=settings.LOG_LEVEL, stream=sys.stdout, format="%(levelname)s: %(message)s") + + +def get_agent_card(host: str, port: int): + """Returns the Agent Card for the Github Agent.""" + capabilities = AgentCapabilities(streaming=True) + + skill_source = AgentSkill( + id="source_operations", + name="Source repository operations", + description="Browse and search code; read, create, and modify repository file contents, branches, and commits.", + tags=["git", "github", "source", "repositories", "files", "branches", "commits"], + examples=[ + "Show the README of kagenti/kagenti", + "List the branches of owner/repo", + "Create a branch and commit a fix to owner/repo", + ], + ) + + skill_issue = AgentSkill( + id="issue_operations", + name="Issue & PR tracker operations", + description="Read, search, create, and update issues, comments, sub-issues, and pull requests.", + tags=["git", "github", "issues", "pull-requests"], + examples=[ + "List open issues in kubernetes/kubernetes", + "Open an issue in owner/repo titled ...", + "Summarise PR #42 in owner/repo", + ], + ) + + if settings.AGENT_ENDPOINT: + url = settings.AGENT_ENDPOINT.rstrip("/") + "/" + else: + url = f"http://{host}:{port}/" + + return AgentCard( + name="Github agent", + description=( + "Autonomous Agent acting on a user's behalf against source repositories and an issue tracker. " + "It inspects and changes repository source contents and reads, creates, and updates issues and their threads." + ), + supported_interfaces=[AgentInterface(url=url, protocol_binding="JSONRPC")], + version="1.0.0", + default_input_modes=["text"], + default_output_modes=["text"], + capabilities=capabilities, + skills=[skill_source, skill_issue], + security_schemes={ + "Bearer": SecurityScheme( + http_auth_security_scheme=HTTPAuthSecurityScheme( + scheme="bearer", bearer_format="JWT", description="OAuth 2.0 JWT token" + ) + ) + }, + ) + + +class A2AEvent(Event): + """ + A class to handle events for A2A Agent. + + Attributes: + task_updater (TaskUpdater): The task updater instance. + """ + + def __init__(self, task_updater: TaskUpdater): + """ + Initializes the A2AEvent instance. + + Args: + task_updater (TaskUpdater): The task updater instance. + """ + self.task_updater = task_updater + + async def emit_event(self, message: str, final: bool = False) -> None: + """ + Emits an event with the given message. + + Args: + message (str): The event message. + final (bool): Whether the event is final. Defaults to False. + """ + logger.info("Emitting event %s", message) + + if final: + parts = [new_text_part(message)] + await self.task_updater.add_artifact(parts) + await self.task_updater.complete() + else: + await self.task_updater.update_status( + TaskState.TASK_STATE_WORKING, + self.task_updater.new_agent_message([new_text_part(message)]), + ) + + +class GithubExecutor(AgentExecutor): + """ + A class to handle execution for A2A Agent. + """ + + async def _run_agent(self, messages: dict, settings: Settings, event_emitter: Event, toolkit: ToolCollection): + from github_agent.main import GithubAgent + + github_agent = GithubAgent( + config=settings, + eventer=event_emitter, + mcp_toolkit=toolkit, + ) + result = await github_agent.execute(messages) + await event_emitter.emit_event(result, True) + + async def execute(self, context: RequestContext, event_queue: EventQueue): + """ + Executes the task. + + Args: + context (RequestContext): The request context. + event_queue (EventQueue): The event queue instance. + + Returns: + None + """ + # If GITHUB_TOKEN is set, pass it as Bearer header to MCP. + # If not set, assume AuthBridge handles auth transparently (envoy injects tokens). + headers = {} + if settings.GITHUB_TOKEN: + headers["Authorization"] = f"Bearer {settings.GITHUB_TOKEN}" + elif context.call_context and (context.call_context.state or {}).get("headers", {}).get("authorization"): + headers["Authorization"] = context.call_context.state["headers"]["authorization"] + else: + logging.warning( + "No GITHUB_TOKEN or inbound Authorization header; outbound requests will be unauthenticated" + ) + + user_input = [context.get_user_input()] + task = context.current_task + if not task: + task = new_task_from_user_message(context.message) + await event_queue.enqueue_event(task) + task_updater = TaskUpdater(event_queue, task.id, task.context_id) + event_emitter = A2AEvent(task_updater) + messages = [] + for message in user_input: + messages.append( + { + "role": "User", + "content": message, + } + ) + + # Hook up MCP tools + try: + if settings.MCP_URL: + logging.info("Connecting to MCP server at %s", settings.MCP_URL) + + server_params = { + "url": settings.MCP_URL, + "transport": "streamable-http", + "headers": headers, + } + adapter = MCPServerAdapter(server_params, connect_timeout=settings.MCP_TIMEOUT) + # MCPServerAdapter.__enter__/__exit__ perform blocking MCP I/O; + # run them off the event loop so we don't stall other async tasks. + mcp_tools = await asyncio.to_thread(adapter.__enter__) + try: + curated_tools = select_enabled_tools(mcp_tools, settings) + if not curated_tools: + raise RuntimeError( + "No enabled tools found from the GitHub MCP server. " + "Check the ENABLED_TOOLS setting and ensure the server is reachable." + ) + await self._run_agent(messages, settings, event_emitter, curated_tools) + finally: + await asyncio.to_thread(adapter.__exit__, None, None, None) + else: + await self._run_agent(messages, settings, event_emitter, None) + + except Exception as e: + traceback.print_exc() + await event_emitter.emit_event( + f"I'm sorry I was unable to fulfill your request. I encountered the following exception: {str(e)}", True + ) + + async def cancel(self, context: RequestContext, event_queue: EventQueue) -> None: + """ + Not implemented + """ + raise Exception("cancel not supported") + + +def run(): + """ + Runs the A2A Agent application. + """ + agent_card = get_agent_card(host="0.0.0.0", port=settings.SERVICE_PORT) + request_handler = DefaultRequestHandler( + agent_executor=GithubExecutor(), + task_store=InMemoryTaskStore(), + agent_card=agent_card, + ) + routes = create_jsonrpc_routes(request_handler, rpc_url="/", enable_v0_3_compat=True) + routes += create_agent_card_routes(agent_card) + routes += create_agent_card_routes(agent_card, card_url="/.well-known/agent.json") + app = Starlette(routes=routes) + uvicorn.run(app, host="0.0.0.0", port=settings.SERVICE_PORT) diff --git a/aiac/src/aiac/pdp/service/policy/keycloak/__init__.py b/aiac/demo/agents/github_agent/github_agent/__init__.py similarity index 100% rename from aiac/src/aiac/pdp/service/policy/keycloak/__init__.py rename to aiac/demo/agents/github_agent/github_agent/__init__.py diff --git a/aiac/demo/agents/github_agent/github_agent/agents.py b/aiac/demo/agents/github_agent/github_agent/agents.py new file mode 100644 index 000000000..fa2ba63c7 --- /dev/null +++ b/aiac/demo/agents/github_agent/github_agent/agents.py @@ -0,0 +1,79 @@ +from crewai import Agent, Crew, Process, Task +from github_agent.config import Settings +from github_agent.llm import CrewLLM +from github_agent.prompts import TOOL_CALL_PROMPT, INFO_PARSER_PROMPT + + +class GithubAgents: + def __init__(self, config: Settings, github_tools): + self.llm = CrewLLM(config) + + ################### + # Pre-requisite extractor (no tools) + ################### + self.prereq_identifier = Agent( + role="Pre-requisite Extractor", + goal="Extract information about GitHub artifacts from the user's query", + backstory=INFO_PARSER_PROMPT, + verbose=True, + llm=self.llm.llm, + ) + + self.prereq_identifier_task = Task( + description="User query: {request}", + agent=self.prereq_identifier, + expected_output=( + 'A JSON object with keys "owner", "repo", "ref", "path", "numbers". ' + 'Example: {"owner": "kagenti", "repo": "kagenti", "ref": null, "path": null, "numbers": null}' + ), + ) + + self.prereq_id_crew = Crew( + agents=[self.prereq_identifier], + tasks=[self.prereq_identifier_task], + process=Process.sequential, + verbose=True, + ) + + ################### + # GitHub operations researcher + ################### + self.github_researcher = Agent( + role="GitHub Operations Analyst", + goal=( + "Answer the user's query using MCP tools. " + "Prefer read-only operations. Be explicit about repo owner/name and filters." + ), + backstory=TOOL_CALL_PROMPT, + tools=github_tools, + verbose=True, + llm=self.llm.llm, + inject_date=True, + max_iter=6, + max_retry_limit=3, + respect_context_window=True, + ) + + self.github_query_task = Task( + description=( + "Use GitHub MCP tools to answer the user's query.\n" + "User query: {request}\n" + "Repository owner: {owner}\n" + "Repository name: {repo}\n" + "Branch/tag/sha: {ref}\n" + "File path: {path}\n" + "Issue/PR numbers: {numbers}" + ), + agent=self.github_researcher, + expected_output=( + "A well formatted, detailed report directly answering the user's query, " + "citing the tool output to support the answer." + ), + ) + + self.crew = Crew( + agents=[self.github_researcher], + tasks=[self.github_query_task], + process=Process.sequential, + verbose=True, + ) diff --git a/aiac/demo/agents/github_agent/github_agent/config.py b/aiac/demo/agents/github_agent/github_agent/config.py new file mode 100644 index 000000000..75accc315 --- /dev/null +++ b/aiac/demo/agents/github_agent/github_agent/config.py @@ -0,0 +1,66 @@ +import json +import os +from pydantic_settings import BaseSettings +from pydantic import model_validator +from pydantic import Field +from typing import Literal, Optional + + +class Settings(BaseSettings): + LOG_LEVEL: Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] = Field( + os.getenv("LOG_LEVEL", "INFO"), + description="Application log level", + ) + TASK_MODEL_ID: str = Field( + os.getenv("TASK_MODEL_ID", "ollama/ibm/granite4:latest"), + description="The ID of the task model", + ) + LLM_API_BASE: str = Field( + os.getenv("LLM_API_BASE", "http://host.docker.internal:11434"), + description="The URL for OpenAI API", + ) + LLM_API_KEY: str = Field(os.getenv("LLM_API_KEY", "my_api_key"), description="The key for OpenAI API") + EXTRA_HEADERS: dict = Field({}, description="Extra headers for the OpenAI API") + MODEL_TEMPERATURE: float = Field( + os.getenv("MODEL_TEMPERATURE", 0), + description="The temperature for the model", + ge=0, + ) + MCP_URL: str = Field( + os.getenv("MCP_URL", "http://github-tool-mcp:9090/mcp"), + description="Endpoint for the MCP server", + ) + MCP_TIMEOUT: int = Field(os.getenv("MCP_TIMEOUT", 600), description="Timeout in seconds for MCP server connection") + ENABLED_TOOLS: Optional[str] = Field( + os.getenv("ENABLED_TOOLS", None), + description="Comma-separated list of tool names to enable (overrides the default curated allow-list)", + ) + + # auth variables for token validation + ISSUER: Optional[str] = Field(os.getenv("ISSUER", None), description="The issuer for incoming JWT tokens") + AGENT_ENDPOINT: Optional[str] = Field( + os.getenv("AGENT_ENDPOINT", None), + description="Override the URL advertised in the agent card", + ) + SERVICE_PORT: int = Field(os.getenv("PORT", 8000), description="Port on which the service will run.") + GITHUB_TOKEN: Optional[str] = Field( + os.getenv("GITHUB_TOKEN", None), + description="If not using agent with authorization, the default Github token to use", + ) + + class Config: + env_file = ".env" + env_file_encoding = "utf-8" + + @model_validator(mode="after") + def validate_extra_headers(self) -> "Settings": + if os.getenv("EXTRA_HEADERS"): + try: + self.EXTRA_HEADERS = json.loads(os.getenv("EXTRA_HEADERS")) + except json.JSONDecodeError: + raise ValueError("EXTRA_HEADERS must be a valid JSON string") + + return self + + +settings = Settings() # type: ignore[call-arg] diff --git a/aiac/demo/agents/github_agent/github_agent/data_types.py b/aiac/demo/agents/github_agent/github_agent/data_types.py new file mode 100644 index 000000000..d97d6065d --- /dev/null +++ b/aiac/demo/agents/github_agent/github_agent/data_types.py @@ -0,0 +1,25 @@ +import json +from pydantic import BaseModel, Field, field_validator + + +class GithubQueryInfo(BaseModel): + owner: str | None = Field(None, description="The repository owner or organization.") + repo: str | None = Field(None, description="The repository name.") + ref: str | None = Field(None, description="Branch, tag, or sha if named.") + path: str | None = Field(None, description="File path if named.") + numbers: list[int] | None = Field( + None, description="Issue or PR numbers mentioned by the user." + ) + + @field_validator("numbers", mode="before") + @classmethod + def coerce_string_to_list(cls, v): + """Small LLMs often serialize arrays as strings in tool call args.""" + if isinstance(v, str): + try: + parsed = json.loads(v) + if isinstance(parsed, list): + return parsed + except (json.JSONDecodeError, ValueError): + pass + return v diff --git a/aiac/demo/agents/github_agent/github_agent/event.py b/aiac/demo/agents/github_agent/github_agent/event.py new file mode 100644 index 000000000..511156326 --- /dev/null +++ b/aiac/demo/agents/github_agent/github_agent/event.py @@ -0,0 +1,8 @@ +from abc import ABC, abstractmethod + + +class Event(ABC): + @abstractmethod + async def emit_event(self, message: str, final: bool = False) -> None: + """Emit Event""" + pass diff --git a/aiac/demo/agents/github_agent/github_agent/llm.py b/aiac/demo/agents/github_agent/github_agent/llm.py new file mode 100644 index 000000000..5a9e380ef --- /dev/null +++ b/aiac/demo/agents/github_agent/github_agent/llm.py @@ -0,0 +1,22 @@ +from crewai import LLM +from github_agent.config import Settings + + +class CrewLLM: + def __init__(self, config: Settings): + kwargs = {} + if config.EXTRA_HEADERS: + kwargs["extra_headers"] = config.EXTRA_HEADERS + + # For Ollama models, pass num_ctx to set the context window size. + # Ollama defaults to 2048 tokens which is too small for agent workflows. + if config.TASK_MODEL_ID.startswith(("ollama/", "ollama_chat/")): + kwargs["num_ctx"] = 8192 + + self.llm = LLM( + model=config.TASK_MODEL_ID, + base_url=config.LLM_API_BASE, + api_key=config.LLM_API_KEY, + temperature=config.MODEL_TEMPERATURE, + **kwargs, + ) diff --git a/aiac/demo/agents/github_agent/github_agent/main.py b/aiac/demo/agents/github_agent/github_agent/main.py new file mode 100644 index 000000000..7f9d54df2 --- /dev/null +++ b/aiac/demo/agents/github_agent/github_agent/main.py @@ -0,0 +1,113 @@ +import json +import logging +import re +import sys + +from crewai_tools.adapters.tool_collection import ToolCollection + +from github_agent.agents import GithubAgents +from github_agent.config import Settings, settings +from github_agent.data_types import GithubQueryInfo +from github_agent.event import Event + +logger = logging.getLogger(__name__) +logging.basicConfig(level=settings.LOG_LEVEL, stream=sys.stdout, format="%(levelname)s: %(message)s") + + +def _parse_prereq_from_raw(raw: str) -> GithubQueryInfo: + """Parse GithubQueryInfo from raw LLM text when instructor/pydantic parsing fails. + + Some Ollama models don't produce structured tool calls that crewai's instructor + integration expects. This fallback extracts JSON from the raw text output. + """ + # Only matches flat JSON (no nested braces). Sufficient for the current + # GithubQueryInfo schema; revisit if the model gains nested fields. + json_match = re.search(r"\{[^{}]*\}", raw) + if json_match: + try: + data = json.loads(json_match.group()) + return GithubQueryInfo(**data) + except (json.JSONDecodeError, ValueError): + pass + + logger.warning("Could not parse prereq JSON from raw output: %s", raw) + return GithubQueryInfo() + + +class GithubAgent: + def __init__( + self, + config: Settings, + eventer: Event = None, + mcp_toolkit: ToolCollection = None, + logger=None, + ): + self.agents = GithubAgents(config, mcp_toolkit) + self.eventer = eventer + self.logger = logger or logging.getLogger(__name__) + + async def _send_event(self, message: str, final: bool = False): + logger.info(message) + if self.eventer: + await self.eventer.emit_event(message, final) + else: + logger.warning("No event handler registered") + + def extract_user_input(self, body): + content = body[-1]["content"] + latest_content = "" + + if isinstance(content, str): + latest_content = content + else: + for item in content: + if item["type"] == "text": + latest_content += item["text"] + else: + self.logger.warning(f"Ignoring content with type {item['type']}") + + return latest_content + + async def _get_prereq_output(self, query: str) -> GithubQueryInfo: + """Run the prereq crew and extract GithubQueryInfo from raw text output. + + We avoid using crewai's output_pydantic because it relies on instructor's + tool-call-based parsing, which fails with Ollama models that don't produce + structured tool calls. Instead, we ask the LLM for JSON and parse it ourselves. + """ + try: + await self.agents.prereq_id_crew.kickoff_async( + inputs={"request": query, "repo": "", "owner": "", "ref": "", "path": "", "numbers": []} + ) + raw = self.agents.prereq_identifier_task.output.raw + self.logger.info(f"Prereq raw output: {raw}") + return _parse_prereq_from_raw(raw) + except Exception as e: + self.logger.warning(f"Prereq crew failed: {e}") + return GithubQueryInfo() + + async def execute(self, user_input): + query = self.extract_user_input(user_input) + await self._send_event("Evaluating requirements...") + prereq = await self._get_prereq_output(query) + + # Validation gates — return helpful message without tool call when unmet + if prereq.numbers: + if not prereq.owner or not prereq.repo: + return "When supplying issue or PR numbers, you must provide both a repository name and owner." + if prereq.repo: + if not prereq.owner: + return "When supplying a repository name, you must also provide an owner of the repo." + + await self._send_event("Searching GitHub...") + await self.agents.crew.kickoff_async( + inputs={ + "request": query, + "owner": prereq.owner or "", + "repo": prereq.repo or "", + "ref": prereq.ref or "", + "path": prereq.path or "", + "numbers": prereq.numbers or [], + } + ) + return self.agents.github_query_task.output.raw diff --git a/aiac/demo/agents/github_agent/github_agent/prompts.py b/aiac/demo/agents/github_agent/github_agent/prompts.py new file mode 100644 index 000000000..88dca7925 --- /dev/null +++ b/aiac/demo/agents/github_agent/github_agent/prompts.py @@ -0,0 +1,127 @@ +TOOL_CALL_PROMPT = """ +You are a GitHub operations analyst with access to MCP tools that can read and write GitHub repositories, issues, and pull requests. +You will receive instructions in the following format: + User query: The original query/instruction from the user + Repository owner: The github owner they are referring to, if present + Repository name: The github repo they are referring to, if present + Branch/tag/sha: The git ref they are referring to, if present + File path: The file path they are referring to, if present + Issue/PR numbers: Any issue or PR numbers they are referring to, if present + +YOUR JOB +1. Decide whether the user's request can be fulfilled with a tool from the catalog. +2. When a tool is required, emit **only** a single tool call in the exact format below. +3. Use the exact owner, repo, ref, path, and numbers as written by the user as input to the tools, without modification. +4. After tool results arrive, produce the final answer grounded strictly in those results. + +## *MODES* +──────────────────────────────────────────────────────── +⚙️ TOOL CALL PHASE + +- If a tool is required, you MUST output EXACTLY the following four lines in this order: + 1) Thought: + 2) Action: + 3) Action Input: + 4) Observation: + +- After the Observation is provided by the system, output: + Thought: I now know the final answer + Final Answer: + +STRICT RULES FOR TOOL CALLS +- Output NOTHING except those exact lines when calling a tool. No code fences, no XML, no extra braces. +- The JSON after "Action Input:" MUST be valid and single-line. No trailing commas, no extra closing braces. +- Use ONLY properties defined in the tool schema (required + optional). Exact key names and value types. +- Use only one tool per call. +- Always copy owner/organization names, repository names, file paths, ref names, and issue/PR numbers exactly as provided by the user or upstream context. Do not truncate, split, normalize, or otherwise modify these identifiers. +- When a tool call succeeds, do not immediately call another tool just to reformat or summarize the same results. + +CORRECT EXAMPLE +Thought: The user provided owner and repo; list_issues fits. +Action: list_issues +Action Input: {"owner":"kagenti","repo":"kagenti"} +Observation: + +──────────────────────────────────────────────────────── +🧩 FINAL ANSWER PHASE + +After tool results arrive: +- Synthesize the returned data to produce a human-readable answer grounded only in the tool output. +- Summarize or aggregate long lists instead of echoing raw JSON. +- Clearly cite or reference the tool results. +- If a tool failed or inputs were missing, say so explicitly. + + Thought: I now know the final answer + Final Answer: + +──────────────────────────────────────────────────────── +TOOL SELECTION GUIDELINES + +Choose the right tool based on the user's intent: + +**SOURCE OPERATIONS** (files, branches, commits, code) +- `get_file_contents` — retrieve the content of a file at an optional ref; requires owner, repo, path +- `list_branches` — list branches; requires owner, repo +- `get_commit` — get details of a specific commit; requires owner, repo, sha +- `list_commits` — list commits on a branch; requires owner, repo; optional ref +- `search_code` — search for code across repositories; use when no specific repo is given or searching across repos +- `create_or_update_file` — create or update a file; requires owner, repo, path, message, content +- `delete_file` — delete a file; requires owner, repo, path, message, sha +- `push_files` — push multiple files in one commit; requires owner, repo, branch, files, message +- `create_branch` — create a new branch; requires owner, repo, branch name + +**ISSUE & PR OPERATIONS** (issues, pull requests, comments) +- `issue_read` — get details of a specific issue; requires owner, repo, issue_number +- `list_issues` — list issues; requires owner and repo; optional state, labels, etc. +- `search_issues` — search issues across repos or within a repo; use when no specific repo is given +- `list_issue_types` — enumerate available issue types for an organization; requires org +- `pull_request_read` — get details of a specific PR; requires owner, repo, pull_number +- `list_pull_requests` — list PRs in a repo; requires owner and repo +- `issue_write` — create or update an issue; requires owner, repo +- `add_issue_comment` — add a comment to an issue; requires owner, repo, issue_number, body +- `sub_issue_write` — create or update a sub-issue; requires owner, repo, issue_number +- `create_pull_request` — create a pull request; requires owner, repo, title, head, base +- `update_pull_request` — update an existing PR; requires owner, repo, pull_number + +Decision rules: +- For source content: use `get_file_contents` when path is given, `list_branches` for branches, `search_code` for broad code search. +- For issues: prefer `list_issues` when owner+repo are known; use `search_issues` for broad searches. +- For PRs: prefer `list_pull_requests` when owner+repo are known; use `pull_request_read` when PR number is given. +- Never infer missing identifiers. +- Never call a tool when you do not have all required parameters. +- When owner/repo/ref/path/issue identifiers are provided, reuse them verbatim. + +Examples: +- "Show README of kagenti/kagenti" → get_file_contents (owner=kagenti, repo=kagenti, path=README.md) +- "List branches of owner/repo" → list_branches +- "Open issues in kagenti/agent-examples" → list_issues +- "Find issues mentioning timeout across all repos" → search_issues +- "Sub-issues under #134 in openai/triton" → sub_issue_write / issue_read with issue_number +- "PR #42 in owner/repo" → pull_request_read + +Carefully inspect the user's request for filters such as labels, date ranges, keywords, state (open/closed), etc. Use available optional parameters where appropriate. +""" + +INFO_PARSER_PROMPT = """ +You are an analyst that will extract out information from a user's instruction/query to determine the following information, if it exists: +- Github owner or organization +- Github repository +- Branch, tag, or sha (ref) if explicitly named +- File path if explicitly named +- Issue or PR number(s) + +Extraction Rules: +- Copy owner/organization names, repository names, ref names, file paths, and issue/PR identifiers exactly as the user typed them. Preserve casing, punctuation, spacing, diacritics, and hyphenation; never rewrite, normalize, or translate these strings. +- Only return values that are explicitly present in the user request. If any item is missing, output None for that field. +- Do not infer or guess missing identifiers. If you are unsure about any value, leave it as None. + +Output format: a JSON object with keys "owner", "repo", "ref", "path", "numbers". +Example: {"owner": "kagenti", "repo": "kagenti", "ref": null, "path": null, "numbers": null} + +Examples: +- "summarize open issues across the foo organization" → {"owner": "foo", "repo": null, "ref": null, "path": null, "numbers": null} +- "kagenti/agent-examples" → {"owner": "kagenti", "repo": "agent-examples", "ref": null, "path": null, "numbers": null} +- "Show README of kagenti/kagenti on branch main" → {"owner": "kagenti", "repo": "kagenti", "ref": "main", "path": "README.md", "numbers": null} +- "How long has issue 2 in modelcontextprotocol/servers been open?" → {"owner": "modelcontextprotocol", "repo": "servers", "ref": null, "path": null, "numbers": [2]} +- "Review PR #87 for CoolOrg/Next-Gen-Repo" → {"owner": "CoolOrg", "repo": "Next-Gen-Repo", "ref": null, "path": null, "numbers": [87]} +""" diff --git a/aiac/demo/agents/github_agent/github_agent/tools.py b/aiac/demo/agents/github_agent/github_agent/tools.py new file mode 100644 index 000000000..db8c0b7f3 --- /dev/null +++ b/aiac/demo/agents/github_agent/github_agent/tools.py @@ -0,0 +1,42 @@ +import logging + +logger = logging.getLogger(__name__) + +# Module constants grouped by skill/scope per spec §5: +SOURCE_READ = ["get_file_contents", "list_branches", "get_commit", "list_commits", "search_code"] +SOURCE_WRITE = ["create_or_update_file", "delete_file", "push_files", "create_branch"] +ISSUE_READ = ["issue_read", "list_issues", "search_issues", "list_issue_types", + "pull_request_read", "list_pull_requests"] +ISSUE_WRITE = ["issue_write", "add_issue_comment", "sub_issue_write", + "create_pull_request", "update_pull_request"] +DEFAULT_ENABLED_TOOLS = SOURCE_READ + SOURCE_WRITE + ISSUE_READ + ISSUE_WRITE + +# Named-but-excluded (documented; re-enable via ENABLED_TOOLS): +EXCLUDED = ["get_me", "get_team_members", "get_teams", "run_secret_scanning", + "create_repository", "fork_repository", "list_tags", "get_tag", "get_label"] + + +def enabled_tool_names(settings) -> list[str]: + """Return the list of enabled tool names from settings or the default curated list.""" + if settings.ENABLED_TOOLS: + return [name.strip() for name in settings.ENABLED_TOOLS.split(",")] + return DEFAULT_ENABLED_TOOLS + + +def select_enabled_tools(mcp_tools, settings) -> list: + """Filter mcp_tools keeping only items whose .name is in the enabled set. + + Logs dropped tool names at DEBUG level. + Caller raises RuntimeError if the result is empty — not this function's job. + """ + allowed = set(enabled_tool_names(settings)) + kept = [] + dropped = [] + for tool in mcp_tools: + if tool.name in allowed: + kept.append(tool) + else: + dropped.append(tool.name) + if dropped: + logger.debug("Dropping MCP tools not in enabled set: %s", dropped) + return kept diff --git a/aiac/demo/agents/github_agent/k8s/configmaps.yaml b/aiac/demo/agents/github_agent/k8s/configmaps.yaml new file mode 100644 index 000000000..362bcfb70 --- /dev/null +++ b/aiac/demo/agents/github_agent/k8s/configmaps.yaml @@ -0,0 +1,65 @@ +# Demo-specific ConfigMap overrides for GitHub Issue Agent + AuthBridge +# +# The Kagenti installer already creates correct defaults for authbridge-config, +# spiffe-helper-config, and envoy-config (with the kagenti realm). This file +# overrides: +# - authbridge-config: Keycloak connection, inbound validation, and outbound exchange settings +# - authproxy-routes: per-target token exchange routes +# +# Authbridge defaults to "passthrough" for outbound traffic, meaning +# requests to unknown hosts (LLM APIs, telemetry, etc.) pass through without +# token exchange. Only hosts listed in authproxy-routes get token exchange. +# +# Usage: +# kubectl apply -f configmaps.yaml +# +# Note: These manifests are configured for namespace "team1" (installer-provided +# and enrolled for AuthBridge sidecar injection) and the installer's +# "kagenti" Keycloak realm (set via KEYCLOAK_REALM below). + +--- +# authbridge-config ConfigMap - Unified config for both client-registration and envoy-proxy +# +# This ConfigMap controls: +# 1. Client registration: KEYCLOAK_URL, KEYCLOAK_REALM for Keycloak client setup +# 2. INBOUND validation: Validates JWT tokens on incoming requests to the agent +# 3. OUTBOUND exchange: Exchanges tokens when the agent calls the GitHub tool +# +# KEYCLOAK_URL and KEYCLOAK_REALM are included because kubectl apply replaces +# the entire ConfigMap. +# TOKEN_URL and ISSUER are auto-derived from KEYCLOAK_URL + KEYCLOAK_REALM. +apiVersion: v1 +kind: ConfigMap +metadata: + name: authbridge-config + namespace: team1 +data: + KEYCLOAK_URL: "http://keycloak-service.keycloak.svc:8080" + KEYCLOAK_REALM: "kagenti" + # TOKEN_URL: Auto-derived from KEYCLOAK_URL + KEYCLOAK_REALM. Only set explicitly + # when the token endpoint differs from the standard Keycloak path. + # TOKEN_URL: "http://keycloak-service.keycloak.svc:8080/realms/kagenti/protocol/openid-connect/token" + # ISSUER: Set explicitly because the internal KEYCLOAK_URL differs from the frontend URL. + ISSUER: "http://keycloak.localtest.me:8080/realms/kagenti" + # Audience validation is automatic — uses CLIENT_ID from /shared/client-id.txt + # (written by client-registration or operator). No manual configuration needed. + +--- +# authproxy-routes ConfigMap - Per-target token exchange routes +# +# Defines which outbound hosts should get token exchange. All other hosts +# pass through unchanged (the default outbound policy is "passthrough"). +# This allows the agent to call LLM APIs, telemetry endpoints, and other +# services without interference from the token exchange proxy. +# +# Update the host pattern if the github-tool service name or namespace changes. +apiVersion: v1 +kind: ConfigMap +metadata: + name: authproxy-routes + namespace: team1 +data: + routes.yaml: | + - host: "github-tool-mcp" + target_audience: "github-tool" + token_scopes: "openid github-tool-aud github-full-access" diff --git a/aiac/demo/agents/github_agent/k8s/github-agent-deployment.yaml b/aiac/demo/agents/github_agent/k8s/github-agent-deployment.yaml new file mode 100644 index 000000000..42a588e2d --- /dev/null +++ b/aiac/demo/agents/github_agent/k8s/github-agent-deployment.yaml @@ -0,0 +1,184 @@ +# GitHub Agent Deployment with AuthBridge +# +# This deployment uses the kagenti-operator webhook to automatically inject +# a single combined AuthBridge sidecar (post-kagenti-extensions#411). The exact +# container shape depends on the resolved AuthBridge mode: +# - proxy-sidecar (default): one container "authbridge-proxy" from the +# "authbridge" image. spiffe-helper is bundled inside; gated per-workload +# by SPIRE_ENABLED. +# - envoy-sidecar: a "proxy-init" init container (iptables) plus one +# container "envoy-proxy" from the "authbridge-envoy" image (Envoy + +# ext_proc + bundled spiffe-helper). +# Either way the sidecar handles inbound JWT validation, outbound token +# exchange (HTTP), and HTTPS passthrough. +# +# Keycloak client registration is operator-managed (no in-pod sidecar); +# the operator creates a kagenti-keycloak-client-credentials- Secret +# and the webhook mounts it at /shared/client-{id,secret}.txt. +# +# The agent container: +# - Runs the A2A GitHub Agent on port 8000 +# - Calls the GitHub MCP tool via HTTP (MCP_URL below) +# - Token exchange to the tool is handled transparently by the AuthBridge sidecar +# +# Labels (on Pod template): +# kagenti.io/inject: enabled - Enables AuthBridge sidecar injection +# kagenti.io/spire: enabled - Enables SPIRE-based identity (set to "disabled" if no SPIRE) +# +# kagenti.io/type is applied automatically by the operator when the +# AgentRuntime CR (at the end of this file) is created. +# +# Prerequisites (NOT created by this file — must exist before applying): +# 1. github-tool Deployment + Service named "github-tool-mcp" (the MCP tool server) +# 2. github-tool-secrets Secret (GitHub token used by the tool) +# 3. authbridge-config ConfigMap (apply configmaps.yaml first) +# 4. authproxy-routes ConfigMap (apply configmaps.yaml first) +# +# Invariant: MCP_URL host ("github-tool-mcp") == authproxy-routes host entry +# ("github-tool-mcp") == the Kubernetes Service name for the tool server. +# All three must match or token exchange will not fire for tool calls. +# +# Usage: +# kubectl apply -f configmaps.yaml +# kubectl apply -f github-agent-deployment.yaml + +--- +apiVersion: v1 +kind: ServiceAccount +metadata: + name: github-agent + namespace: team1 + +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: github-agent + namespace: team1 + labels: + app.kubernetes.io/name: github-agent + protocol.kagenti.io/a2a: "" +spec: + replicas: 1 + selector: + matchLabels: + app.kubernetes.io/name: github-agent + template: + metadata: + labels: + app.kubernetes.io/name: github-agent + kagenti.io/inject: enabled + kagenti.io/spire: enabled + protocol.kagenti.io/a2a: "" + spec: + serviceAccountName: github-agent + containers: + - name: agent + image: localhost/github-agent:latest + imagePullPolicy: IfNotPresent + ports: + - containerPort: 8000 + name: http + env: + # LLM configuration - Ollama (local LLM) + # Matches upstream .env.ollama from agent-examples repo + - name: TASK_MODEL_ID + value: "ollama/ibm/granite4:latest" + # Ollama API base URL. Required by litellm (used by crewai >=1.10). + # For Docker Desktop / Kind: http://host.docker.internal:11434 + # For in-cluster Ollama: http://ollama.ollama.svc:11434 + - name: LLM_API_BASE + value: "http://host.docker.internal:11434" + - name: OLLAMA_API_BASE + value: "http://host.docker.internal:11434" + - name: LLM_API_KEY + value: "ollama" + - name: MODEL_TEMPERATURE + value: "0" + # For OpenAI (uncomment and set your key): + # - name: TASK_MODEL_ID + # value: "gpt-4.1-nano" + # - name: LLM_API_KEY + # valueFrom: + # secretKeyRef: + # name: openai-secret + # key: apikey + # - name: OPENAI_API_KEY + # valueFrom: + # secretKeyRef: + # name: openai-secret + # key: apikey + + # Agent service settings + # PORT tells the agent where to listen for A2A traffic. + # The kagenti operator may override this via proxy-sidecar + # port-stealing to relocate the agent to a free port. + - name: PORT + value: "8000" + - name: LOG_LEVEL + value: "DEBUG" + + # MCP Tool endpoint - the GitHub tool service + # AuthBridge will exchange the token when the agent calls this URL. + # This hostname MUST match the authproxy-routes host entry and the + # Kubernetes Service name for the tool (see invariant in header above). + - name: MCP_URL + value: "http://github-tool-mcp:9090/mcp" + + # JWKS URI for validating incoming tokens from Keycloak + - name: JWKS_URI + value: "http://keycloak-service.keycloak.svc:8080/realms/kagenti/protocol/openid-connect/certs" + resources: + limits: + cpu: 500m + memory: 512Mi + requests: + cpu: 100m + memory: 128Mi + volumeMounts: + - name: shared-data + mountPath: /shared + volumes: + - name: shared-data + emptyDir: {} + +--- +apiVersion: v1 +kind: Service +metadata: + name: github-agent + namespace: team1 +spec: + selector: + app.kubernetes.io/name: github-agent + ports: + # Port 8001 is the agent's direct port after authbridge port-stealing (8000→8001). + # Listed first so getServicePort() picks it up for AgentCard fetching. + - name: agent + port: 8001 + targetPort: 8001 + protocol: TCP + # Port 8080 is the public/proxy port (authbridge reverse proxy on 8000). + - name: proxy + port: 8080 + targetPort: 8000 + protocol: TCP + type: ClusterIP + +--- +# AgentRuntime triggers operator-managed Keycloak client registration +# (creates kagenti-keycloak-client-credentials- Secret with +# client-id.txt and client-secret.txt that the authbridge sidecar +# mounts at /shared/). The operator also applies kagenti.io/type=agent +# to the Deployment and Pod template, enabling webhook sidecar injection. +apiVersion: agent.kagenti.dev/v1alpha1 +kind: AgentRuntime +metadata: + name: github-agent + namespace: team1 +spec: + type: agent + targetRef: + apiVersion: apps/v1 + kind: Deployment + name: github-agent diff --git a/aiac/demo/agents/github_agent/pyproject.toml b/aiac/demo/agents/github_agent/pyproject.toml new file mode 100644 index 000000000..4290a9fb6 --- /dev/null +++ b/aiac/demo/agents/github_agent/pyproject.toml @@ -0,0 +1,47 @@ +[project] +name = "github-agent" +version = "0.1.0" +requires-python = ">=3.11,<3.13" +description = "Github Agent module" +dependencies = [ + "python-dotenv>=1.2.2", + "a2a-sdk>=1.1.0,<2", + # We use 1.6.1 instead of the latest because + # crewai requires openai>=2.30.0 + # but litellm requires openai==2.24.0 + "crewai[litellm]>=1.6.1", + "litellm>=1.90.2", # Indirect, prevents CVE-2026-42271 + "crewai-tools[mcp]>=1.6.1", + "urllib3>=2.7.0", # Indirect; prevents CVE-2025-66418 + "python-multipart>=0.0.32", # Indirect; prevents CVE-2026-24486 + "cryptography>=50.0.0", # Indirect; prevents CVE-2026-26007 and GHSA-g6cj-pr64-35w5 (PKCS#7 Bleichenbacher oracle) + "pyasn1>=0.6.3", # Indirect; prevents CVE-2026-30922 + "starlette>=1.3.1", # Indirect; prevents CVE-2025-62727 + "pillow>=12.3.0", # Indirect; prevents CVE-2026-40192 + "lxml>=6.1.1", # Indirect; prevents CVE-2026-41066 + "orjson>=3.11.6", # Indirect; prevents CVE-2025-67221 + "json-repair>=0.60.1", # Indirect (via crewai); prevents GHSA-xf7x-x43h-rpqh (DoS) + "aiohttp>=3.14.3", # Indirect; prevents GHSA-cq5v-8q36-5273 (OOB heap read) + # CVE-2026-45829 (chromadb): no fixed version published; crewai pins chromadb<1.2. + # Vulnerability requires running a chromadb HTTP server with trust_remote_code=true, + # which this agent does not do. +] + +[tool.ruff] +line-length = 120 +target-version = "py311" + +[project.scripts] +server = "a2a_agent:run" + +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[dependency-groups] +dev = [ + "pytest>=9.1.1", +] + +[tool.pytest.ini_options] +testpaths = ["test"] diff --git a/aiac/demo/agents/github_agent/test/test_agent_card.py b/aiac/demo/agents/github_agent/test/test_agent_card.py new file mode 100644 index 000000000..eb90f1cf6 --- /dev/null +++ b/aiac/demo/agents/github_agent/test/test_agent_card.py @@ -0,0 +1,95 @@ +import os +import json +import pytest +from starlette.testclient import TestClient +from starlette.applications import Starlette +from a2a.server.routes import create_agent_card_routes + +from a2a_agent import get_agent_card + + +@pytest.fixture +def card(): + return get_agent_card("localhost", 8000) + + +def test_card_name_and_version(card): + assert card.name == "Github agent" + assert card.version == "1.0.0" + + +def test_card_description(card): + assert card.description == ( + "Autonomous Agent acting on a user's behalf against source repositories and an issue tracker. " + "It inspects and changes repository source contents and reads, creates, and updates issues and their threads." + ) + + +def test_card_two_skills(card): + assert len(card.skills) == 2 + skill_ids = {s.id for s in card.skills} + assert skill_ids == {"source_operations", "issue_operations"} + + +def test_skill_tags_and_examples(card): + skills_by_id = {s.id: s for s in card.skills} + + source = skills_by_id["source_operations"] + assert "github" in source.tags + assert len(source.examples) >= 1 + + issue = skills_by_id["issue_operations"] + assert "github" in issue.tags + assert len(issue.examples) >= 1 + + +def test_bearer_security_scheme(card): + assert "Bearer" in card.security_schemes + assert card.security_schemes["Bearer"].http_auth_security_scheme.scheme == "bearer" + + +def test_agent_interface_jsonrpc(card): + assert card.supported_interfaces[0].protocol_binding == "JSONRPC" + + +def test_agent_endpoint_override(): + import a2a_agent + + original = a2a_agent.settings.AGENT_ENDPOINT + a2a_agent.settings.AGENT_ENDPOINT = "http://custom.host:9999" + try: + card = get_agent_card("localhost", 8000) + assert card.supported_interfaces[0].url == "http://custom.host:9999/" + finally: + a2a_agent.settings.AGENT_ENDPOINT = original + + +def test_capabilities_streaming(card): + assert card.capabilities.streaming is True + + +def test_input_output_modes(card): + assert list(card.default_input_modes) == ["text"] + assert list(card.default_output_modes) == ["text"] + + +def test_well_known_agent_card_route(card): + routes = create_agent_card_routes(card) + app = Starlette(routes=routes) + client = TestClient(app) + response = client.get("/.well-known/agent-card.json") + assert response.status_code == 200 + data = response.json() + assert data["name"] == "Github agent" + assert len(data["skills"]) == 2 + + +def test_well_known_agent_json_route(card): + routes = create_agent_card_routes(card, card_url="/.well-known/agent.json") + app = Starlette(routes=routes) + client = TestClient(app) + response = client.get("/.well-known/agent.json") + assert response.status_code == 200 + data = response.json() + assert data["name"] == "Github agent" + assert len(data["skills"]) == 2 diff --git a/aiac/demo/agents/github_agent/test/test_prereq.py b/aiac/demo/agents/github_agent/test/test_prereq.py new file mode 100644 index 000000000..a152b2936 --- /dev/null +++ b/aiac/demo/agents/github_agent/test/test_prereq.py @@ -0,0 +1,92 @@ +import pytest +from unittest.mock import AsyncMock, MagicMock + +from github_agent.data_types import GithubQueryInfo +from github_agent.main import GithubAgent, _parse_prereq_from_raw +from github_agent.config import Settings + + +def make_agent(prereq_raw="", researcher_raw="done"): + config = Settings() # type: ignore + agent = GithubAgent(config=config) + agent.agents.prereq_identifier_task = MagicMock() + agent.agents.prereq_identifier_task.output = MagicMock(raw=prereq_raw) + agent.agents.prereq_id_crew = MagicMock() + agent.agents.prereq_id_crew.kickoff_async = AsyncMock(return_value=None) + agent.agents.github_query_task = MagicMock() + agent.agents.github_query_task.output = MagicMock(raw=researcher_raw) + agent.agents.crew = MagicMock() + agent.agents.crew.kickoff_async = AsyncMock(return_value=None) + return agent + + +def test_github_query_info_parses_flat_json(): + info = GithubQueryInfo(owner="kagenti", repo="my-repo", ref="main", path="README.md", numbers=[1, 2]) + assert info.owner == "kagenti" + assert info.repo == "my-repo" + assert info.ref == "main" + assert info.path == "README.md" + assert info.numbers == [1, 2] + + +def test_numbers_string_coercion(): + info = GithubQueryInfo(numbers="[1, 2]") + assert info.numbers == [1, 2] + + +def test_parse_prereq_from_raw_valid_json(): + raw = '{"owner": "foo", "repo": "bar", "ref": null, "path": null, "numbers": null}' + result = _parse_prereq_from_raw(raw) + assert result.owner == "foo" + assert result.repo == "bar" + assert result.ref is None + assert result.numbers is None + + +def test_parse_prereq_from_raw_unparseable(): + result = _parse_prereq_from_raw("not json at all") + assert result.owner is None + assert result.repo is None + assert result.ref is None + assert result.path is None + assert result.numbers is None + + +@pytest.mark.anyio +async def test_gate_numbers_without_owner_repo(): + prereq_raw = '{"owner": null, "repo": null, "ref": null, "path": null, "numbers": [42]}' + agent = make_agent(prereq_raw=prereq_raw) + result = await agent.execute([{"role": "User", "content": "Issue 42"}]) + assert "must provide both" in result.lower() + agent.agents.crew.kickoff_async.assert_not_called() + + +@pytest.mark.anyio +async def test_gate_repo_without_owner(): + prereq_raw = '{"owner": null, "repo": "myrepo", "ref": null, "path": null, "numbers": null}' + agent = make_agent(prereq_raw=prereq_raw) + result = await agent.execute([{"role": "User", "content": "list issues in myrepo"}]) + assert "must also provide an owner" in result.lower() + agent.agents.crew.kickoff_async.assert_not_called() + + +@pytest.mark.anyio +async def test_happy_path_researcher_called(): + prereq_raw = '{"owner": "kagenti", "repo": "myrepo", "ref": null, "path": null, "numbers": null}' + agent = make_agent(prereq_raw=prereq_raw, researcher_raw="done") + result = await agent.execute([{"role": "User", "content": "list issues in kagenti/myrepo"}]) + agent.agents.crew.kickoff_async.assert_called_once() + assert result == "done" + + +@pytest.mark.anyio +async def test_happy_path_researcher_inputs(): + prereq_raw = '{"owner": "kagenti", "repo": "myrepo", "ref": null, "path": null, "numbers": null}' + agent = make_agent(prereq_raw=prereq_raw) + await agent.execute([{"role": "User", "content": "list issues in kagenti/myrepo"}]) + call_kwargs = agent.agents.crew.kickoff_async.call_args + inputs = call_kwargs.kwargs.get("inputs") or call_kwargs.args[0] if call_kwargs.args else {} + if not inputs: + inputs = call_kwargs[1].get("inputs", {}) + assert inputs.get("owner") == "kagenti" + assert inputs.get("repo") == "myrepo" diff --git a/aiac/demo/agents/github_agent/test/test_tools.py b/aiac/demo/agents/github_agent/test/test_tools.py new file mode 100644 index 000000000..fc42f2172 --- /dev/null +++ b/aiac/demo/agents/github_agent/test/test_tools.py @@ -0,0 +1,110 @@ +import pytest +from github_agent.tools import ( + DEFAULT_ENABLED_TOOLS, + EXCLUDED, + SOURCE_READ, + SOURCE_WRITE, + ISSUE_READ, + ISSUE_WRITE, + enabled_tool_names, + select_enabled_tools, +) + +# Extra tool names that appear in the full 44-tool github-tool catalog but are not in the +# enabled set — used to build a representative stub list of the expected catalog size. +_EXTRA_CATALOG_TOOLS = [ + "get_me", "get_team_members", "get_teams", "run_secret_scanning", + "create_repository", "fork_repository", "list_tags", "get_tag", "get_label", + "create_release", "list_releases", "get_release", "get_repository", + "list_repositories", "watch_repository", "unwatch_repository", + "get_file_metadata", "list_directory", "get_tree", + "create_gist", "list_gists", + "get_discussion", "list_discussions", +] + + +class FakeTool: + def __init__(self, name): + self.name = name + + +class FakeSettings: + def __init__(self, enabled_tools=None): + self.ENABLED_TOOLS = enabled_tools + + +def test_select_keeps_enabled_tools(): + all_tools = [FakeTool(n) for n in DEFAULT_ENABLED_TOOLS + EXCLUDED] + settings = FakeSettings() + result = select_enabled_tools(all_tools, settings) + result_names = {t.name for t in result} + for name in DEFAULT_ENABLED_TOOLS: + assert name in result_names, f"{name} should be in result" + for name in EXCLUDED: + assert name not in result_names, f"{name} should not be in result" + + +def test_select_with_override(): + all_tools = [FakeTool(n) for n in DEFAULT_ENABLED_TOOLS + EXCLUDED] + settings = FakeSettings(enabled_tools="issue_read,list_issues") + result = select_enabled_tools(all_tools, settings) + result_names = [t.name for t in result] + assert result_names == ["issue_read", "list_issues"] + + +def test_whitespace_tolerance(): + all_tools = [FakeTool(n) for n in DEFAULT_ENABLED_TOOLS + EXCLUDED] + settings = FakeSettings(enabled_tools=" issue_read , list_issues ") + result = select_enabled_tools(all_tools, settings) + result_names = [t.name for t in result] + assert result_names == ["issue_read", "list_issues"] + + +def test_default_tools_no_duplicates(): + assert len(DEFAULT_ENABLED_TOOLS) == len(set(DEFAULT_ENABLED_TOOLS)) + + +def test_default_tools_union_of_groups(): + assert set(DEFAULT_ENABLED_TOOLS) == set(SOURCE_READ + SOURCE_WRITE + ISSUE_READ + ISSUE_WRITE) + + +def test_source_vs_issue_membership(): + default_set = set(DEFAULT_ENABLED_TOOLS) + for name in SOURCE_READ: + assert name in default_set + for name in SOURCE_WRITE: + assert name in default_set + for name in ISSUE_READ: + assert name in default_set + for name in ISSUE_WRITE: + assert name in default_set + + +def test_excluded_tools_absent_by_default(): + default_set = set(DEFAULT_ENABLED_TOOLS) + for name in EXCLUDED: + assert name not in default_set, f"{name} should not appear in DEFAULT_ENABLED_TOOLS" + + +def test_stub_44_tool_catalog_filtered_to_default(): + """Simulate the full github-tool catalog (~44 tools); default set filters it to exactly DEFAULT_ENABLED_TOOLS.""" + catalog = DEFAULT_ENABLED_TOOLS + _EXTRA_CATALOG_TOOLS + # Pad to 44 entries if the catalog is shorter (adds unused dummy names) + while len(catalog) < 44: + catalog = catalog + [f"_dummy_{len(catalog)}"] + tools = [FakeTool(n) for n in catalog[:44]] + result = select_enabled_tools(tools, FakeSettings()) + result_names = {t.name for t in result} + assert result_names == set(DEFAULT_ENABLED_TOOLS) + assert len(result) == len(DEFAULT_ENABLED_TOOLS) + + +def test_executor_raises_runtime_error_on_empty_selection(): + """GithubExecutor raises RuntimeError when select_enabled_tools returns an empty list.""" + from a2a_agent import GithubExecutor + import inspect + + # Verify the RuntimeError guard is present in the execute source + src = inspect.getsource(GithubExecutor.execute) + assert "RuntimeError" in src + assert "tools found" in src.lower() or "enabled tools" in src.lower() diff --git a/aiac/demo/agents/github_agent/test_startup.exp b/aiac/demo/agents/github_agent/test_startup.exp new file mode 100644 index 000000000..881ef4a5c --- /dev/null +++ b/aiac/demo/agents/github_agent/test_startup.exp @@ -0,0 +1,23 @@ +# This script tests if the server has dependencies and reaches the point of starting its server. + +# Run with `expect -f test_startup.exp` + +set timeout 120 + +# This is similar to the Dockerfile CMD +set child_pid [spawn env HOST=localhost PORT=8001 uv run --locked server] + +expect { + "Uvicorn running on" { + # Unlike the other servers, this one opens a child process. + # Without the explicit `pkill -P` it would leave a `uv` process running. + puts "Spawned pid was $child_pid" + exec pkill -P $child_pid + puts "Success"; exit 0 + } + timeout { + exec pkill -P $child_pid + puts "Timed out waiting for startup"; exit 1 + } + eof { puts "Process exited early"; exit 2 } +} diff --git a/aiac/demo/agents/github_agent/uv.lock b/aiac/demo/agents/github_agent/uv.lock new file mode 100644 index 000000000..a333a727b --- /dev/null +++ b/aiac/demo/agents/github_agent/uv.lock @@ -0,0 +1,3123 @@ +version = 1 +revision = 3 +requires-python = ">=3.11, <3.13" +resolution-markers = [ + "python_full_version >= '3.12'", + "python_full_version < '3.12'", +] + +[[package]] +name = "a2a-sdk" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "culsans" }, + { name = "google-api-core" }, + { name = "googleapis-common-protos" }, + { name = "httpx" }, + { name = "httpx-sse" }, + { name = "json-rpc" }, + { name = "packaging" }, + { name = "protobuf" }, + { name = "pydantic" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c7/7e/8ac10bbf8b15b16574355f39b17dbdf617a282c27b41c7ff2116e30336df/a2a_sdk-1.1.0.tar.gz", hash = "sha256:e8102dad1b36709dbdc3d19319e38e6dfa3b3a79c30416030eb2d482576be204", size = 375726, upload-time = "2026-05-29T09:34:43.015Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d4/ea/3a5b160cfd51c67759b08748051094d9365ceff18127633d0021950c9860/a2a_sdk-1.1.0-py3-none-any.whl", hash = "sha256:d7f5846caf18033d8bf3108b11ec827dd8dd32f867c98848ede0e39474be93be", size = 241886, upload-time = "2026-05-29T09:34:41.484Z" }, +] + +[[package]] +name = "aiofiles" +version = "24.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0b/03/a88171e277e8caa88a4c77808c20ebb04ba74cc4681bf1e9416c862de237/aiofiles-24.1.0.tar.gz", hash = "sha256:22a075c9e5a3810f0c2e48f3008c94d68c65d763b9b03857924c99e57355166c", size = 30247, upload-time = "2024-06-24T11:02:03.584Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a5/45/30bb92d442636f570cb5651bc661f52b610e2eec3f891a5dc3a4c3667db0/aiofiles-24.1.0-py3-none-any.whl", hash = "sha256:b4ec55f4195e3eb5d7abd1bf7e061763e864dd4954231fb8539a0ef8bb8260e5", size = 15896, upload-time = "2024-06-24T11:02:01.529Z" }, +] + +[[package]] +name = "aiohappyeyeballs" +version = "2.7.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ce/f4/eec0465c2f67b2664688d0240b3212d5196fd89e741df67ddb81f8d35658/aiohappyeyeballs-2.7.1.tar.gz", hash = "sha256:065665c041c42a5938ed220bdcd7230f22527fbec085e1853d2402c8a3615d9d", size = 24757, upload-time = "2026-07-01T17:11:55.501Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/71/43/1947f06babed6b3f1d7f38b0c767f52df66bfb2bc10b468c4a7de9eceff2/aiohappyeyeballs-2.7.1-py3-none-any.whl", hash = "sha256:9243213661e29250eb41368e5daa826fc017156c3b8a11440826b2e3ed376472", size = 15038, upload-time = "2026-07-01T17:11:54.055Z" }, +] + +[[package]] +name = "aiohttp" +version = "3.14.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiohappyeyeballs" }, + { name = "aiosignal" }, + { name = "attrs" }, + { name = "frozenlist" }, + { name = "multidict" }, + { name = "propcache" }, + { name = "typing-extensions" }, + { name = "yarl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/58/d9/22ce5786ac0c1653ae8b6c23bded02c1686d11f0dbb45b31ce128e0df985/aiohttp-3.14.3.tar.gz", hash = "sha256:9491196535a88924a60afd5b5f434b5b203b6cc616250878dbdb223a8f7844bc", size = 7971213, upload-time = "2026-07-23T01:57:27.037Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f8/5c/b3e4ff8ad43a8afef9602c5e90285936da1beaea8b029016b793891f03c3/aiohttp-3.14.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:e568e14940c09955aa51f4e645b6daa18a581c5dcfcd73744dcc86a856e3ced3", size = 764250, upload-time = "2026-07-23T01:52:48.525Z" }, + { url = "https://files.pythonhosted.org/packages/0e/da/f1b384465e51449d844056b75070461da03a9a23e6c1747003695bf4172a/aiohttp-3.14.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:54cfcdee2770dac994417cbb0ee1f3eb0e7cb6b30c79bf44f2c02ff79ec5124a", size = 516281, upload-time = "2026-07-23T01:52:51.047Z" }, + { url = "https://files.pythonhosted.org/packages/b9/3f/01264f820ee2e3712a827892b1cd6ff80f3300c1fcbffbb45714a915d47a/aiohttp-3.14.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:21c016079415ed3fd676963e9793700a566d85dbbd6bfc564b9b2d209147dcc8", size = 514742, upload-time = "2026-07-23T01:52:53.779Z" }, + { url = "https://files.pythonhosted.org/packages/9e/8d/a71c6f2db52ac1ed142b133f7feddaa6b70539c3f4de24d7e226c95b794c/aiohttp-3.14.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d6088ec9894113802bddb3c09e974929aed2c7b3a8c456219b8aab4481f1a239", size = 1780613, upload-time = "2026-07-23T01:52:56.948Z" }, + { url = "https://files.pythonhosted.org/packages/a5/11/3dd9b3fb3a170f6ec9011b5291d876a6fab4086714c9e158600edf01b4fd/aiohttp-3.14.3-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:16ea7e24c309fb7c0bbd505d149abe4fe4dccfb8db911db7dbec0921bc889a6f", size = 1737688, upload-time = "2026-07-23T01:52:59.294Z" }, + { url = "https://files.pythonhosted.org/packages/6d/3e/834c26918be7d88068822b40e0db30fca50b5f4fe79104aa16a93f1d74e6/aiohttp-3.14.3-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:56f355e79f71aef2a85c80305cc915f894b170dba76de5fe84f6351939b83c06", size = 1845742, upload-time = "2026-07-23T01:53:01.641Z" }, + { url = "https://files.pythonhosted.org/packages/cc/c9/49ab8572df7d66bc13d11e31f781292badb04180dd87ba98733066c6aed7/aiohttp-3.14.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:18c441d0a8fca6de8d1f546849b9f0ab20d435993e2c5b59562b2fae6be2f929", size = 1928412, upload-time = "2026-07-23T01:53:04.018Z" }, + { url = "https://files.pythonhosted.org/packages/a5/b9/2b8f0c0ce09c87a1daf80fd483431b56b1435d3f62789bc86f572e1245de/aiohttp-3.14.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:53e7b4ce82b54a8bcc71b3b67a5cbd177ca1d7f592cbc92cd38b7349f73482db", size = 1786220, upload-time = "2026-07-23T01:53:06.481Z" }, + { url = "https://files.pythonhosted.org/packages/85/00/9c45f81de11710460edfa1dc81317b6e882703b160926c879a9d20da9fcc/aiohttp-3.14.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f55119f7bf25f49ed210f6096090715da24f2943c62102448915fde3c62877ce", size = 1637231, upload-time = "2026-07-23T01:53:10.258Z" }, + { url = "https://files.pythonhosted.org/packages/19/ce/967d628e910756f3539c6107cb7844a1b69440dcb3029a5ee7871b09ab63/aiohttp-3.14.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:9aa6e61fdf20105c4144e755bd586008ff450791d67b1c8146fdc15959c4d51c", size = 1753161, upload-time = "2026-07-23T01:53:13.817Z" }, + { url = "https://files.pythonhosted.org/packages/11/b2/0c3d4114f0aee4f580f5b3b4eb71b24d7a23b834ea506a4dfebe76513f35/aiohttp-3.14.3-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:ccd4893707b3e2a13e39c90d43cf80edf2e4d0457935bcc103bf2346214c3f15", size = 1756356, upload-time = "2026-07-23T01:53:16.211Z" }, + { url = "https://files.pythonhosted.org/packages/63/5d/99e7d91c82f1399d1ae2a854e080bd1493fbc31e5e959dbc4ec33dac3bec/aiohttp-3.14.3-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:b2466434105a4e03113c36ec775cc2ebe6676b62eae326fa670bb607ef788c1c", size = 1819846, upload-time = "2026-07-23T01:53:18.289Z" }, + { url = "https://files.pythonhosted.org/packages/ad/05/d5e1cb6480eeffd3f901d40a2c5e2d1e7effdc797837da3b490272699f13/aiohttp-3.14.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:ba59d59aba08ac02fc03b0c8983ccd5ee39a199d0552ce9e6d2b4845b34d59ae", size = 1628531, upload-time = "2026-07-23T01:53:23.86Z" }, + { url = "https://files.pythonhosted.org/packages/c9/90/b934682bcaefae18a9e04f3dff5b68522ba810906358ae5029b68110ea3b/aiohttp-3.14.3-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:ed099d105449c4f9e84f24af203cd131349d4761d8813fa7e02c32e7128cd910", size = 1832712, upload-time = "2026-07-23T01:53:27.551Z" }, + { url = "https://files.pythonhosted.org/packages/21/df/6061679faaf81fac746e7307c7adb71e858071a5d34c27583afefc64f543/aiohttp-3.14.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:152516815ef926786a0b6ae2b8f1fd2e0c71582dee0b435636865316fd4891b7", size = 1775014, upload-time = "2026-07-23T01:53:30.223Z" }, + { url = "https://files.pythonhosted.org/packages/8a/1d/f854878bbc69b88faefe924b619a34a6f59ec05fd387c77690667eaa75eb/aiohttp-3.14.3-cp311-cp311-win32.whl", hash = "sha256:a4af35c443e0b1a1bd6a8af3f3485d7fda15c142751a00f3ff8090f0b93346fa", size = 456006, upload-time = "2026-07-23T01:53:34.97Z" }, + { url = "https://files.pythonhosted.org/packages/73/0c/2af9d1674baccd1dbd47282a93d660a22e57ef6167c856deb24b4214fbab/aiohttp-3.14.3-cp311-cp311-win_amd64.whl", hash = "sha256:e1e74298bab6ee0d6e749ed4fd1901c7e604bdda32c03d787a2cc71c46d0433d", size = 481069, upload-time = "2026-07-23T01:53:39.673Z" }, + { url = "https://files.pythonhosted.org/packages/8e/76/88401ff3fc95e85c5fc38d588f36f55e61ecb64343b2bc8d69326f453cc0/aiohttp-3.14.3-cp311-cp311-win_arm64.whl", hash = "sha256:03cd2bde3d7f085b64e549c985f4bb928cad7e8ecf5323bfca320db548d81b39", size = 453021, upload-time = "2026-07-23T01:53:43.749Z" }, + { url = "https://files.pythonhosted.org/packages/18/d4/eb96299230e20acf2efae207cb8d69051f1f68e357e5ea5e479bf6fb097a/aiohttp-3.14.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:39aded8c7f3b935b54aab1d8d73c70ec0ee2d3ec3b943e0e86611bc150ba47f5", size = 754690, upload-time = "2026-07-23T01:53:47.332Z" }, + { url = "https://files.pythonhosted.org/packages/88/11/e7a70a209eb9a067c0d3212b518a0134e3484f5178c7533878b6b514d469/aiohttp-3.14.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:5bcb6ff3fdab1258a192679ff1a05d44f59626430aa05cd1a9d2447423599228", size = 509484, upload-time = "2026-07-23T01:53:51.159Z" }, + { url = "https://files.pythonhosted.org/packages/30/07/4bbc222cc8dbe31d4c3e8a5baad2286e4d42026ac0c570027b89afce6344/aiohttp-3.14.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:617105e2c3018ee38d0c8ce5ee3c84f621a6d8b9f723202aacaff28449ca91ee", size = 511949, upload-time = "2026-07-23T01:53:55.083Z" }, + { url = "https://files.pythonhosted.org/packages/54/b9/42e74c46b7b7c794b995bbc1f573fb48950c38b19d8600c62a6804ee2d67/aiohttp-3.14.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f631fe87a6f30df5fbe6d79640b25e4cffb38c31c7fb6f10871517b84b0f8c1a", size = 1765282, upload-time = "2026-07-23T01:53:59.662Z" }, + { url = "https://files.pythonhosted.org/packages/6b/ed/62bc4d74363ad346d518e0720363a949f63e2e23439a79eb5813d4d29bb3/aiohttp-3.14.3-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a94dbaae5ae27bd849c93570669bff91e0510f33a80805738e3de72a7be0447b", size = 1741511, upload-time = "2026-07-23T01:54:04.063Z" }, + { url = "https://files.pythonhosted.org/packages/d0/9f/181e8a8bc79e47d13c7fc4540bd7a3b729d9505609c61f392a8dd2fbfe55/aiohttp-3.14.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8f2f1c4c032c7cedd7d8da6f54c97b70266c6570c3108d3fdffee7188bb70529", size = 1810680, upload-time = "2026-07-23T01:54:09.882Z" }, + { url = "https://files.pythonhosted.org/packages/5c/9a/dec94d6ad694552fe3424e3f1928d7a606a5d9d9433a04e7ecdd9d38ae7f/aiohttp-3.14.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ea05e1f97ceea523942d9b2a7d7c0359d781d683d6b043f5943a602b14da4787", size = 1905646, upload-time = "2026-07-23T01:54:13.475Z" }, + { url = "https://files.pythonhosted.org/packages/52/b7/7cd31f29d6055bd711ae6e669367fba6f5ae9de463910a793e30556a8db7/aiohttp-3.14.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:543906c127fb1d929b95076db19b83fa2d46751006ff1e23b093aa5ac4d8db42", size = 1792122, upload-time = "2026-07-23T01:54:15.752Z" }, + { url = "https://files.pythonhosted.org/packages/66/73/10b1ef93afa61f4963c746257b70ced619cf31a4798671de5fdb2608501d/aiohttp-3.14.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0a5ff2dfbb9ce645fa5b8ef3e02c6c0b9cc3f6030ff863d0c51fffc50cb5541b", size = 1591127, upload-time = "2026-07-23T01:54:19.489Z" }, + { url = "https://files.pythonhosted.org/packages/49/ed/3b203fa6de1b338c14acdc06bf6ca9b043b7944f005966958c2ced932cde/aiohttp-3.14.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:041badb8f84396357c4d3ad26de6afd7a32b112f43d3c63045c0c8278cfd2043", size = 1725210, upload-time = "2026-07-23T01:54:24.129Z" }, + { url = "https://files.pythonhosted.org/packages/28/b7/1c2aab8c706436dcc28598452488ac9cd7c409da815237c28c27d58993e6/aiohttp-3.14.3-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:530125ee1163c4219af35dc3aa1206e541e7b31b6efc1a3f93b70a136f65d427", size = 1764848, upload-time = "2026-07-23T01:54:27.973Z" }, + { url = "https://files.pythonhosted.org/packages/54/50/94c28f08b131c4bf10984ea2c7a536c9920608bb2d6e7f95642c30cc87b7/aiohttp-3.14.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c8653fd547c93a61aadc612007790f5555cdd18946fa48cf45e26d8ea4ea473d", size = 1777102, upload-time = "2026-07-23T01:54:31.775Z" }, + { url = "https://files.pythonhosted.org/packages/13/d4/e7d09ba7d345fb2d74440fd2fa033c5e079fac05552927705986f41a364f/aiohttp-3.14.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:89176250f686cb9853c0fb7ead90e639e915b84a6f43eedc2a4e7ec21f1037f0", size = 1580205, upload-time = "2026-07-23T01:54:34.518Z" }, + { url = "https://files.pythonhosted.org/packages/a3/84/072a91d68e1e1eb587985b54baab94221277f877e8ef274fc213a0ceae28/aiohttp-3.14.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:3a26434dafe408229ff3403458ca58de24fb51936504decac49ce6755f77e59d", size = 1797219, upload-time = "2026-07-23T01:54:36.995Z" }, + { url = "https://files.pythonhosted.org/packages/e0/eb/aad34e897e668424d6e995da5dff8a4a09af93363d3392488772957a63aa/aiohttp-3.14.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:d1558173930a5a8d3069cee5c92fc91c87c4dbcb099debbb3622053717145a19", size = 1768629, upload-time = "2026-07-23T01:54:40.103Z" }, + { url = "https://files.pythonhosted.org/packages/b6/2b/6bb88ddba0fecd9122aa3ebcad25996cf6c083a4a7040dbb3a4f97972af6/aiohttp-3.14.3-cp312-cp312-win32.whl", hash = "sha256:16100ad3ab8d649fdfbee87602d9d2dcdca9df0b9eda8a1b5fdc0d41f96da559", size = 451481, upload-time = "2026-07-23T01:54:42.547Z" }, + { url = "https://files.pythonhosted.org/packages/76/9b/f2f8f108da17ecef2cc3efc424e8b7ad3782b1a8360f7b8eae8ced84f6ea/aiohttp-3.14.3-cp312-cp312-win_amd64.whl", hash = "sha256:33a2d7c28d33797a2e99923dffa63f83d908a19b6bf26cfe80fa790aa5e1a75a", size = 476845, upload-time = "2026-07-23T01:54:44.853Z" }, + { url = "https://files.pythonhosted.org/packages/3e/44/28dac80a8941b604f4da10ce21097614ca1bf905ce93dca28d8d7de9c1e7/aiohttp-3.14.3-cp312-cp312-win_arm64.whl", hash = "sha256:362a3fd481769cac1a824514bcd86fda51c65e8fe6e051099e008fddde6db17c", size = 448050, upload-time = "2026-07-23T01:54:47.087Z" }, +] + +[[package]] +name = "aiologic" +version = "0.17.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "sniffio" }, + { name = "typing-extensions" }, + { name = "wrapt" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f1/7a/d51f2fde1e8ae8a83431f8e97b7a71e9358cdb1d4d2ce6be387fa44d68de/aiologic-0.17.1.tar.gz", hash = "sha256:2e1b93b9e88ced318c2a63ad7b382688f40cbfe40e3d42258d49dc9c5aea179d", size = 252354, upload-time = "2026-06-27T20:41:33.25Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5b/d3/2d310b1b839014034dba0cba685e492df8a5c7ad32c19cab7e979eed6554/aiologic-0.17.1-py3-none-any.whl", hash = "sha256:c66b319830fedb7ca3d2b2125fa6f5b653f89418c2a27ea76f259ec5f00943c0", size = 161331, upload-time = "2026-06-27T20:41:31.877Z" }, +] + +[[package]] +name = "aiosignal" +version = "1.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "frozenlist" }, + { name = "typing-extensions" }, +] +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, upload-time = "2025-07-03T22:54:42.156Z" }, +] + +[[package]] +name = "aiosqlite" +version = "0.21.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/13/7d/8bca2bf9a247c2c5dfeec1d7a5f40db6518f88d314b8bca9da29670d2671/aiosqlite-0.21.0.tar.gz", hash = "sha256:131bb8056daa3bc875608c631c678cda73922a2d4ba8aec373b19f18c17e7aa3", size = 13454, upload-time = "2025-02-03T07:30:16.235Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f5/10/6c25ed6de94c49f88a91fa5018cb4c0f3625f31d5be9f771ebe5cc7cd506/aiosqlite-0.21.0-py3-none-any.whl", hash = "sha256:2549cf4057f95f53dcba16f2b64e8e2791d7e1adedb13197dd8ed77bb226d7d0", size = 15792, upload-time = "2025-02-03T07:30:13.6Z" }, +] + +[[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, 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, 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, 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, upload-time = "2024-05-20T21:33:24.1Z" }, +] + +[[package]] +name = "anyio" +version = "4.14.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/61/cc/a381afa6efea9f496eff839d4a6a1aed3bfafc7b3ab4b0d1b243a12573dd/anyio-4.14.2.tar.gz", hash = "sha256:cfa139f3ed1a23ee8f88a145ddb5ac7605b8bbfd8592baacd7ce3d8bb4313c7f", size = 260176, upload-time = "2026-07-12T20:29:07.082Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/da/35/f2287558c17e29fafc8ef3daf819bb9834061cfa43bff8014f7df7f63bdc/anyio-4.14.2-py3-none-any.whl", hash = "sha256:9f505dda5ac9f0c8309b5e8bd445a8c2bf7246f3ce950121e45ea15bc41d1494", size = 125813, upload-time = "2026-07-12T20:29:05.763Z" }, +] + +[[package]] +name = "appdirs" +version = "1.4.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d7/d8/05696357e0311f5b5c316d7b95f46c669dd9c15aaeecbb48c7d0aeb88c40/appdirs-1.4.4.tar.gz", hash = "sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41", size = 13470, upload-time = "2020-05-11T07:59:51.037Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3b/00/2344469e2084fb287c2e0b57b72910309874c3245463acd6cf5e3db69324/appdirs-1.4.4-py2.py3-none-any.whl", hash = "sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128", size = 9566, upload-time = "2020-05-11T07:59:49.499Z" }, +] + +[[package]] +name = "attrs" +version = "26.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9a/8e/82a0fe20a541c03148528be8cac2408564a6c9a0cc7e9171802bc1d26985/attrs-26.1.0.tar.gz", hash = "sha256:d03ceb89cb322a8fd706d4fb91940737b6642aa36998fe130a9bc96c985eff32", size = 952055, upload-time = "2026-03-19T14:22:25.026Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/64/b4/17d4b0b2a2dc85a6df63d1157e028ed19f90d4cd97c36717afef2bc2f395/attrs-26.1.0-py3-none-any.whl", hash = "sha256:c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309", size = 67548, upload-time = "2026-03-19T14:22:23.645Z" }, +] + +[[package]] +name = "backoff" +version = "2.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/47/d7/5bbeb12c44d7c4f2fb5b56abce497eb5ed9f34d85701de869acedd602619/backoff-2.2.1.tar.gz", hash = "sha256:03f829f5bb1923180821643f8753b0502c3b682293992485b0eef2807afa5cba", size = 17001, upload-time = "2022-10-05T19:19:32.061Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/df/73/b6e24bd22e6720ca8ee9a85a0c4a2971af8497d8f3193fa05390cbd46e09/backoff-2.2.1-py3-none-any.whl", hash = "sha256:63579f9a0628e06278f7e47b7d7d5b6ce20dc65c5e96a6f3ca99a6adca0396e8", size = 15148, upload-time = "2022-10-05T19:19:30.546Z" }, +] + +[[package]] +name = "bcrypt" +version = "5.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d4/36/3329e2518d70ad8e2e5817d5a4cac6bba05a47767ec416c7d020a965f408/bcrypt-5.0.0.tar.gz", hash = "sha256:f748f7c2d6fd375cc93d3fba7ef4a9e3a092421b8dbf34d8d4dc06be9492dfdd", size = 25386, upload-time = "2025-09-25T19:50:47.829Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/84/29/6237f151fbfe295fe3e074ecc6d44228faa1e842a81f6d34a02937ee1736/bcrypt-5.0.0-cp38-abi3-macosx_10_12_universal2.whl", hash = "sha256:fc746432b951e92b58317af8e0ca746efe93e66555f1b40888865ef5bf56446b", size = 494553, upload-time = "2025-09-25T19:49:49.006Z" }, + { url = "https://files.pythonhosted.org/packages/45/b6/4c1205dde5e464ea3bd88e8742e19f899c16fa8916fb8510a851fae985b5/bcrypt-5.0.0-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c2388ca94ffee269b6038d48747f4ce8df0ffbea43f31abfa18ac72f0218effb", size = 275009, upload-time = "2025-09-25T19:49:50.581Z" }, + { url = "https://files.pythonhosted.org/packages/3b/71/427945e6ead72ccffe77894b2655b695ccf14ae1866cd977e185d606dd2f/bcrypt-5.0.0-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:560ddb6ec730386e7b3b26b8b4c88197aaed924430e7b74666a586ac997249ef", size = 278029, upload-time = "2025-09-25T19:49:52.533Z" }, + { url = "https://files.pythonhosted.org/packages/17/72/c344825e3b83c5389a369c8a8e58ffe1480b8a699f46c127c34580c4666b/bcrypt-5.0.0-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:d79e5c65dcc9af213594d6f7f1fa2c98ad3fc10431e7aa53c176b441943efbdd", size = 275907, upload-time = "2025-09-25T19:49:54.709Z" }, + { url = "https://files.pythonhosted.org/packages/0b/7e/d4e47d2df1641a36d1212e5c0514f5291e1a956a7749f1e595c07a972038/bcrypt-5.0.0-cp38-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2b732e7d388fa22d48920baa267ba5d97cca38070b69c0e2d37087b381c681fd", size = 296500, upload-time = "2025-09-25T19:49:56.013Z" }, + { url = "https://files.pythonhosted.org/packages/0f/c3/0ae57a68be2039287ec28bc463b82e4b8dc23f9d12c0be331f4782e19108/bcrypt-5.0.0-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:0c8e093ea2532601a6f686edbc2c6b2ec24131ff5c52f7610dd64fa4553b5464", size = 278412, upload-time = "2025-09-25T19:49:57.356Z" }, + { url = "https://files.pythonhosted.org/packages/45/2b/77424511adb11e6a99e3a00dcc7745034bee89036ad7d7e255a7e47be7d8/bcrypt-5.0.0-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:5b1589f4839a0899c146e8892efe320c0fa096568abd9b95593efac50a87cb75", size = 275486, upload-time = "2025-09-25T19:49:59.116Z" }, + { url = "https://files.pythonhosted.org/packages/43/0a/405c753f6158e0f3f14b00b462d8bca31296f7ecfc8fc8bc7919c0c7d73a/bcrypt-5.0.0-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:89042e61b5e808b67daf24a434d89bab164d4de1746b37a8d173b6b14f3db9ff", size = 277940, upload-time = "2025-09-25T19:50:00.869Z" }, + { url = "https://files.pythonhosted.org/packages/62/83/b3efc285d4aadc1fa83db385ec64dcfa1707e890eb42f03b127d66ac1b7b/bcrypt-5.0.0-cp38-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:e3cf5b2560c7b5a142286f69bde914494b6d8f901aaa71e453078388a50881c4", size = 310776, upload-time = "2025-09-25T19:50:02.393Z" }, + { url = "https://files.pythonhosted.org/packages/95/7d/47ee337dacecde6d234890fe929936cb03ebc4c3a7460854bbd9c97780b8/bcrypt-5.0.0-cp38-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:f632fd56fc4e61564f78b46a2269153122db34988e78b6be8b32d28507b7eaeb", size = 312922, upload-time = "2025-09-25T19:50:04.232Z" }, + { url = "https://files.pythonhosted.org/packages/d6/3a/43d494dfb728f55f4e1cf8fd435d50c16a2d75493225b54c8d06122523c6/bcrypt-5.0.0-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:801cad5ccb6b87d1b430f183269b94c24f248dddbbc5c1f78b6ed231743e001c", size = 341367, upload-time = "2025-09-25T19:50:05.559Z" }, + { url = "https://files.pythonhosted.org/packages/55/ab/a0727a4547e383e2e22a630e0f908113db37904f58719dc48d4622139b5c/bcrypt-5.0.0-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:3cf67a804fc66fc217e6914a5635000259fbbbb12e78a99488e4d5ba445a71eb", size = 359187, upload-time = "2025-09-25T19:50:06.916Z" }, + { url = "https://files.pythonhosted.org/packages/1b/bb/461f352fdca663524b4643d8b09e8435b4990f17fbf4fea6bc2a90aa0cc7/bcrypt-5.0.0-cp38-abi3-win32.whl", hash = "sha256:3abeb543874b2c0524ff40c57a4e14e5d3a66ff33fb423529c88f180fd756538", size = 153752, upload-time = "2025-09-25T19:50:08.515Z" }, + { url = "https://files.pythonhosted.org/packages/41/aa/4190e60921927b7056820291f56fc57d00d04757c8b316b2d3c0d1d6da2c/bcrypt-5.0.0-cp38-abi3-win_amd64.whl", hash = "sha256:35a77ec55b541e5e583eb3436ffbbf53b0ffa1fa16ca6782279daf95d146dcd9", size = 150881, upload-time = "2025-09-25T19:50:09.742Z" }, + { url = "https://files.pythonhosted.org/packages/54/12/cd77221719d0b39ac0b55dbd39358db1cd1246e0282e104366ebbfb8266a/bcrypt-5.0.0-cp38-abi3-win_arm64.whl", hash = "sha256:cde08734f12c6a4e28dc6755cd11d3bdfea608d93d958fffbe95a7026ebe4980", size = 144931, upload-time = "2025-09-25T19:50:11.016Z" }, + { url = "https://files.pythonhosted.org/packages/5d/ba/2af136406e1c3839aea9ecadc2f6be2bcd1eff255bd451dd39bcf302c47a/bcrypt-5.0.0-cp39-abi3-macosx_10_12_universal2.whl", hash = "sha256:0c418ca99fd47e9c59a301744d63328f17798b5947b0f791e9af3c1c499c2d0a", size = 495313, upload-time = "2025-09-25T19:50:12.309Z" }, + { url = "https://files.pythonhosted.org/packages/ac/ee/2f4985dbad090ace5ad1f7dd8ff94477fe089b5fab2040bd784a3d5f187b/bcrypt-5.0.0-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ddb4e1500f6efdd402218ffe34d040a1196c072e07929b9820f363a1fd1f4191", size = 275290, upload-time = "2025-09-25T19:50:13.673Z" }, + { url = "https://files.pythonhosted.org/packages/e4/6e/b77ade812672d15cf50842e167eead80ac3514f3beacac8902915417f8b7/bcrypt-5.0.0-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7aeef54b60ceddb6f30ee3db090351ecf0d40ec6e2abf41430997407a46d2254", size = 278253, upload-time = "2025-09-25T19:50:15.089Z" }, + { url = "https://files.pythonhosted.org/packages/36/c4/ed00ed32f1040f7990dac7115f82273e3c03da1e1a1587a778d8cea496d8/bcrypt-5.0.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:f0ce778135f60799d89c9693b9b398819d15f1921ba15fe719acb3178215a7db", size = 276084, upload-time = "2025-09-25T19:50:16.699Z" }, + { url = "https://files.pythonhosted.org/packages/e7/c4/fa6e16145e145e87f1fa351bbd54b429354fd72145cd3d4e0c5157cf4c70/bcrypt-5.0.0-cp39-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a71f70ee269671460b37a449f5ff26982a6f2ba493b3eabdd687b4bf35f875ac", size = 297185, upload-time = "2025-09-25T19:50:18.525Z" }, + { url = "https://files.pythonhosted.org/packages/24/b4/11f8a31d8b67cca3371e046db49baa7c0594d71eb40ac8121e2fc0888db0/bcrypt-5.0.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:f8429e1c410b4073944f03bd778a9e066e7fad723564a52ff91841d278dfc822", size = 278656, upload-time = "2025-09-25T19:50:19.809Z" }, + { url = "https://files.pythonhosted.org/packages/ac/31/79f11865f8078e192847d2cb526e3fa27c200933c982c5b2869720fa5fce/bcrypt-5.0.0-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:edfcdcedd0d0f05850c52ba3127b1fce70b9f89e0fe5ff16517df7e81fa3cbb8", size = 275662, upload-time = "2025-09-25T19:50:21.567Z" }, + { url = "https://files.pythonhosted.org/packages/d4/8d/5e43d9584b3b3591a6f9b68f755a4da879a59712981ef5ad2a0ac1379f7a/bcrypt-5.0.0-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:611f0a17aa4a25a69362dcc299fda5c8a3d4f160e2abb3831041feb77393a14a", size = 278240, upload-time = "2025-09-25T19:50:23.305Z" }, + { url = "https://files.pythonhosted.org/packages/89/48/44590e3fc158620f680a978aafe8f87a4c4320da81ed11552f0323aa9a57/bcrypt-5.0.0-cp39-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:db99dca3b1fdc3db87d7c57eac0c82281242d1eabf19dcb8a6b10eb29a2e72d1", size = 311152, upload-time = "2025-09-25T19:50:24.597Z" }, + { url = "https://files.pythonhosted.org/packages/5f/85/e4fbfc46f14f47b0d20493669a625da5827d07e8a88ee460af6cd9768b44/bcrypt-5.0.0-cp39-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:5feebf85a9cefda32966d8171f5db7e3ba964b77fdfe31919622256f80f9cf42", size = 313284, upload-time = "2025-09-25T19:50:26.268Z" }, + { url = "https://files.pythonhosted.org/packages/25/ae/479f81d3f4594456a01ea2f05b132a519eff9ab5768a70430fa1132384b1/bcrypt-5.0.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:3ca8a166b1140436e058298a34d88032ab62f15aae1c598580333dc21d27ef10", size = 341643, upload-time = "2025-09-25T19:50:28.02Z" }, + { url = "https://files.pythonhosted.org/packages/df/d2/36a086dee1473b14276cd6ea7f61aef3b2648710b5d7f1c9e032c29b859f/bcrypt-5.0.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:61afc381250c3182d9078551e3ac3a41da14154fbff647ddf52a769f588c4172", size = 359698, upload-time = "2025-09-25T19:50:31.347Z" }, + { url = "https://files.pythonhosted.org/packages/c0/f6/688d2cd64bfd0b14d805ddb8a565e11ca1fb0fd6817175d58b10052b6d88/bcrypt-5.0.0-cp39-abi3-win32.whl", hash = "sha256:64d7ce196203e468c457c37ec22390f1a61c85c6f0b8160fd752940ccfb3a683", size = 153725, upload-time = "2025-09-25T19:50:34.384Z" }, + { url = "https://files.pythonhosted.org/packages/9f/b9/9d9a641194a730bda138b3dfe53f584d61c58cd5230e37566e83ec2ffa0d/bcrypt-5.0.0-cp39-abi3-win_amd64.whl", hash = "sha256:64ee8434b0da054d830fa8e89e1c8bf30061d539044a39524ff7dec90481e5c2", size = 150912, upload-time = "2025-09-25T19:50:35.69Z" }, + { url = "https://files.pythonhosted.org/packages/27/44/d2ef5e87509158ad2187f4dd0852df80695bb1ee0cfe0a684727b01a69e0/bcrypt-5.0.0-cp39-abi3-win_arm64.whl", hash = "sha256:f2347d3534e76bf50bca5500989d6c1d05ed64b440408057a37673282c654927", size = 144953, upload-time = "2025-09-25T19:50:37.32Z" }, + { url = "https://files.pythonhosted.org/packages/8a/75/4aa9f5a4d40d762892066ba1046000b329c7cd58e888a6db878019b282dc/bcrypt-5.0.0-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:7edda91d5ab52b15636d9c30da87d2cc84f426c72b9dba7a9b4fe142ba11f534", size = 271180, upload-time = "2025-09-25T19:50:38.575Z" }, + { url = "https://files.pythonhosted.org/packages/54/79/875f9558179573d40a9cc743038ac2bf67dfb79cecb1e8b5d70e88c94c3d/bcrypt-5.0.0-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:046ad6db88edb3c5ece4369af997938fb1c19d6a699b9c1b27b0db432faae4c4", size = 273791, upload-time = "2025-09-25T19:50:39.913Z" }, + { url = "https://files.pythonhosted.org/packages/bc/fe/975adb8c216174bf70fc17535f75e85ac06ed5252ea077be10d9cff5ce24/bcrypt-5.0.0-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:dcd58e2b3a908b5ecc9b9df2f0085592506ac2d5110786018ee5e160f28e0911", size = 270746, upload-time = "2025-09-25T19:50:43.306Z" }, + { url = "https://files.pythonhosted.org/packages/e4/f8/972c96f5a2b6c4b3deca57009d93e946bbdbe2241dca9806d502f29dd3ee/bcrypt-5.0.0-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:6b8f520b61e8781efee73cba14e3e8c9556ccfb375623f4f97429544734545b4", size = 273375, upload-time = "2025-09-25T19:50:45.43Z" }, +] + +[[package]] +name = "beautifulsoup4" +version = "4.13.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "soupsieve" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/85/2e/3e5079847e653b1f6dc647aa24549d68c6addb4c595cc0d902d1b19308ad/beautifulsoup4-4.13.5.tar.gz", hash = "sha256:5e70131382930e7c3de33450a2f54a63d5e4b19386eab43a5b34d594268f3695", size = 622954, upload-time = "2025-08-24T14:06:13.168Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/eb/f4151e0c7377a6e08a38108609ba5cede57986802757848688aeedd1b9e8/beautifulsoup4-4.13.5-py3-none-any.whl", hash = "sha256:642085eaa22233aceadff9c69651bc51e8bf3f874fb6d7104ece2beb24b47c4a", size = 105113, upload-time = "2025-08-24T14:06:14.884Z" }, +] + +[[package]] +name = "build" +version = "1.5.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "os_name == 'nt'" }, + { name = "packaging" }, + { name = "pyproject-hooks" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2d/21/6ec54248b4d0d51f12f3ca4aa77a128077d747a5db86cb5a2fcd9aedecbd/build-1.5.1.tar.gz", hash = "sha256:94e17f1db803ab22f46049376c44c8437c52090f0dfdf1adc43df56542d644fb", size = 112439, upload-time = "2026-07-09T06:21:59.632Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/f7/2c3f99ff9282f1c1ec9f6298f2c03034658a0901eeacb3b3501cb488574c/build-1.5.1-py3-none-any.whl", hash = "sha256:f1a58fe2e5af5b0238a07b9e70207492c79ddebbdb1ad954fc86d62a56be3e0d", size = 31195, upload-time = "2026-07-09T06:21:58.551Z" }, +] + +[[package]] +name = "cel-python" +version = "0.5.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "google-re2" }, + { name = "jmespath" }, + { name = "lark" }, + { name = "pendulum" }, + { name = "pyyaml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/61/4e/f821948a5bbd7a98a218720f831a62216f79a98e43b13d9ab2f98e37c5f8/cel_python-0.5.0.tar.gz", hash = "sha256:3eb0a619e8df0f338d0430cda01427a742e77e3c433a1c7c3ebd409cd804c45a", size = 13364027, upload-time = "2026-01-31T19:07:13.436Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/f8/38812adc3f787c2c2e8ba56f524185ed379656c10b40347a32796ba61c08/cel_python-0.5.0-py3-none-any.whl", hash = "sha256:d0f85008b89655c2bb18d797d2fa3f96f2ed80f4a3b43b0e8138c6646581e5f6", size = 84950, upload-time = "2026-01-31T19:07:11.821Z" }, +] + +[[package]] +name = "certifi" +version = "2026.6.17" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c9/c7/424b75da314c1045981bd9777432fad05a9e0c69daa4ed7e308bbaffe405/certifi-2026.6.17.tar.gz", hash = "sha256:024c88eeec92ca068db80f02b8b07c9cef7b9fe261d1d535abfd5abd6f6af432", size = 134594, upload-time = "2026-06-17T10:31:07.894Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/2f/c5464532e965badff2f4c4c1a3a83f5697f0d7c407ed0cda44aaa99bb451/certifi-2026.6.17-py3-none-any.whl", hash = "sha256:2227dcbaafe0d2f59279d1762ddddc37783ed4354594f194ffc31d20f41fc3db", size = 133289, upload-time = "2026-06-17T10:31:06.348Z" }, +] + +[[package]] +name = "cffi" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pycparser", marker = "implementation_name != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/57/5f/ff100cae70ebe9d8df1c01a00e510e45d9adb5c1fdda84791b199141de97/cffi-2.1.0.tar.gz", hash = "sha256:efc1cdd798b1aaf39b4610bba7aad28c9bea9b910f25c784ccf9ec1fa719d1f9", size = 531036, upload-time = "2026-07-06T21:34:30.382Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d3/67/85c89a59ba36a671e79638f44d466749f08179266a57e4f2ffdf92174072/cffi-2.1.0-cp311-cp311-macosx_10_15_x86_64.whl", hash = "sha256:02cb7ff33ded4f1532476731f89ede53e2e488a8e6205515a82144246ffa7dcc", size = 183845, upload-time = "2026-07-06T21:32:26.32Z" }, + { url = "https://files.pythonhosted.org/packages/ea/dd/e3b0baa2d3d6a857ac72b7efbf18e32e487c9cdafcc13049ad765495b15e/cffi-2.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f5bce581e6b8c235e566a14768a943b172ada3ed73537bb0c0be1edee312d4e7", size = 184186, upload-time = "2026-07-06T21:32:28.025Z" }, + { url = "https://files.pythonhosted.org/packages/65/68/9f3ef890cf3c6ab97bd531c5677f67613d302165d16f8142b2811782a614/cffi-2.1.0-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:30b65779d598c370374fefabf138d456fd6f3216bfa7bedfab1ba82025b0cd93", size = 211892, upload-time = "2026-07-06T21:32:29.565Z" }, + { url = "https://files.pythonhosted.org/packages/22/d7/1a74539db16d8bfd839ff1515948948efbb162e574650fd3d846896eea95/cffi-2.1.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:88023dfe18799507b73f1dbb0d14326a17465de1bc9c9c7655c22845e9ddc3a2", size = 218793, upload-time = "2026-07-06T21:32:30.951Z" }, + { url = "https://files.pythonhosted.org/packages/ec/d1/9a5b7169499e8e8d8e636de70b97ac7c9447104d2ff1a2cd94790cea5162/cffi-2.1.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:0a96b74cda968eebbad56d973efe5098974f0a9fb323865bf99ea1fd24e3e64c", size = 205737, upload-time = "2026-07-06T21:32:32.216Z" }, + { url = "https://files.pythonhosted.org/packages/ba/b0/e131a9c41f10607926278453d9596163594fe1c4ebc46efe3b5e5b34eb84/cffi-2.1.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:a5781494d4d400a3f47f8f1da94b324f6e6b440a53387774002890a2a2f4b50f", size = 204909, upload-time = "2026-07-06T21:32:33.655Z" }, + { url = "https://files.pythonhosted.org/packages/fb/d2/4398416cd699b35167947c6e22aca52c47e69ad5695073c9f1f2c52e04aa/cffi-2.1.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:aa7a1b53a2a4452ada2d1b5dade9960b2522f1e61293a811a077439e39029565", size = 217883, upload-time = "2026-07-06T21:32:35.173Z" }, + { url = "https://files.pythonhosted.org/packages/a2/a5/d4fe77b589e5e82d43ebc809bf2e6474afe8e48e32ea050b9357645b6471/cffi-2.1.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:9d8272c0e483b024e1b9ad029821470ed8ec65631dbd90217469da0e7cd89f1c", size = 221251, upload-time = "2026-07-06T21:32:36.527Z" }, + { url = "https://files.pythonhosted.org/packages/22/f0/a2fc43084c0433caf7f461bccc013e28f848d04ee1c5ed7fce71423cf4d9/cffi-2.1.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:7762faa47e8ff7eb80bd261d9a7d8eea2d8baa69de5e95b70c1f338bbe712f02", size = 214250, upload-time = "2026-07-06T21:32:37.852Z" }, + { url = "https://files.pythonhosted.org/packages/04/8c/b925975448cf20634a9fbd5efceb807219db452653648d2897c0989cab2d/cffi-2.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:89095c1968b4ba8285840e131bf2891b09ae137fe2146905acae0354fbce1b5e", size = 219441, upload-time = "2026-07-06T21:32:39.146Z" }, + { url = "https://files.pythonhosted.org/packages/eb/da/5c4918a2d61d86fa927d716cb3d8e4626ef8dc8f605a599d32f33897f59a/cffi-2.1.0-cp311-cp311-win32.whl", hash = "sha256:64c753a0f87a256020004f37a1c8c02c480e725f910f0b2a0f3f07debd1b2479", size = 174496, upload-time = "2026-07-06T21:32:40.467Z" }, + { url = "https://files.pythonhosted.org/packages/f9/c8/6c2de1d55cf35ef8b92885d5ef280790f0fb9634d87ea1cc315176aecd61/cffi-2.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:4f26194e3d95e06501b942642855aed4f953d55e95d7d01b7c4483db3ecff458", size = 185113, upload-time = "2026-07-06T21:32:41.761Z" }, + { url = "https://files.pythonhosted.org/packages/9e/4e/e8d7cb5783f1841a3c8fb3a7735838d7484d08ec08c9f984b14cac1ac0e9/cffi-2.1.0-cp311-cp311-win_arm64.whl", hash = "sha256:35aaea0c7ee0e58a5cd8c2fd1a48fdf7ece0d2699b7ecdda08194e9ce5dd9b3d", size = 179927, upload-time = "2026-07-06T21:32:42.961Z" }, + { url = "https://files.pythonhosted.org/packages/1e/85/990925db5df586ec90beb97529c853497e7f85ba0234830447faf41c3057/cffi-2.1.0-cp312-cp312-macosx_10_15_x86_64.whl", hash = "sha256:df2b82571a1b30f58a87bf4e5a9e78d2b1eff6c6ce8fd3aa3757221f93f0863f", size = 184829, upload-time = "2026-07-06T21:32:44.324Z" }, + { url = "https://files.pythonhosted.org/packages/4b/92/e7bb136ad6b5352603732cf907ef862ca103f20f2031c1735a46300c20c9/cffi-2.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:78474632761faa0fb96f30b1c928c84ebcf68713cbb80d15bab09dfe61640fde", size = 184728, upload-time = "2026-07-06T21:32:45.683Z" }, + { url = "https://files.pythonhosted.org/packages/c3/c0/d1ec30ffb370f748f2fb54425972bfef9871e0132e82fb589c46b6676049/cffi-2.1.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:5972433ad71a9e46516584ef60a0fda12d9dc459938d1539c3ddecf9bdc1368d", size = 214815, upload-time = "2026-07-06T21:32:48.557Z" }, + { url = "https://files.pythonhosted.org/packages/1b/dc/5620cf930688be01f2d673804291de757a934c90b946dbdc3d84130c2ea4/cffi-2.1.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b6422532152adf4e59b110cb2808cee7a033800952f5c036b4af047ee43199e7", size = 222429, upload-time = "2026-07-06T21:32:49.848Z" }, + { url = "https://files.pythonhosted.org/packages/4b/a4/77b53abbf7a1e0beb9637edbef2a94d15f9c822f591e85d439ffd91519a6/cffi-2.1.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:46b1c8db8f6122420f32d02fffb924c2fe9bc772d228c7c711748fff56aabb2b", size = 210315, upload-time = "2026-07-06T21:32:51.221Z" }, + { url = "https://files.pythonhosted.org/packages/58/0c/f528df19cc94b675087324d4760d9e6d5bfae97d6217aa4fac43de4f5fcc/cffi-2.1.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9fafc5aa2e2a39aaf7f8cc0c1f044a9b07fca12e558dca53a3cc5c654ad67a7", size = 208859, upload-time = "2026-07-06T21:32:52.512Z" }, + { url = "https://files.pythonhosted.org/packages/62/f2/c9522a81c32132799a1972c39f5c5f8b4c8b9f00488a23feaa6c06f07741/cffi-2.1.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1e9f50d192a3e525b15a75ab5114e442d83d657b7ec29182a991bc9a88fd3a66", size = 221844, upload-time = "2026-07-06T21:32:53.704Z" }, + { url = "https://files.pythonhosted.org/packages/6e/28/bd53988b9833e8f8ad539d26f4c07a6b3f6bcb1e9e02e7ca038250b3428d/cffi-2.1.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:98fff996e983a36d3aa2eca83af40c5821202e7e6f32d13ae94e3d2286f10cfe", size = 225287, upload-time = "2026-07-06T21:32:54.907Z" }, + { url = "https://files.pythonhosted.org/packages/79/99/0d0fd37f055224085f42bbb2c022d002e17dde4a97972822327b07d84101/cffi-2.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:379de10ce1ba048b1448599d1b37b24caee16309d1ac98d3982fc997f768700b", size = 223681, upload-time = "2026-07-06T21:32:56.329Z" }, + { url = "https://files.pythonhosted.org/packages/b0/80/c138990aa2a70b1a269f6e06348729836d733d6f970867943f61d367f8cc/cffi-2.1.0-cp312-cp312-win32.whl", hash = "sha256:9b8f0f26ca4e7513c534d351eca551947d053fac438f2a04ac96d882909b0d3a", size = 175269, upload-time = "2026-07-06T21:32:57.777Z" }, + { url = "https://files.pythonhosted.org/packages/a8/eb/f636456ff21a83fc13c032b58cc5dde061691546ac79efa284b2989b7982/cffi-2.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:c97f080ea627e2863524c5af3836e2270b5f5dfff1f104392b959f8df0c5d384", size = 185881, upload-time = "2026-07-06T21:32:59.253Z" }, + { url = "https://files.pythonhosted.org/packages/dd/2c/400ea43e721727dca8a65c4521390e9196757caba4a45643acb2b63271b8/cffi-2.1.0-cp312-cp312-win_arm64.whl", hash = "sha256:6d194185eabd279f1c05ebe3504265ddfc5ad2b58d0714f7db9f01da592e9eb6", size = 180088, upload-time = "2026-07-06T21:33:02.278Z" }, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.9" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/bd/2a/23f34ec9d04624958e137efdc394888716353190e75f25dd22c7a2c7a8aa/charset_normalizer-3.4.9.tar.gz", hash = "sha256:673611bbd43f0810bec0b0f028ddeaaa501190339cac411f347ac76917c3ae7b", size = 152439, upload-time = "2026-07-07T14:34:58.454Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0b/e3/85ec501f206fb049259288c1f3506e53876937fb00edb47009348e66756b/charset_normalizer-3.4.9-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:0e94703ec9684807f20cfb5eed95c70f67f2a8f21ad620146d7b5a13677b93e5", size = 317075, upload-time = "2026-07-07T14:32:56.021Z" }, + { url = "https://files.pythonhosted.org/packages/c3/69/2a5385192e67175f7d8bd5ce4f57c24bc956439adeae5c13a99aa28a53d1/charset_normalizer-3.4.9-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2a441ea71902098ffe78c5abe6c494f44160b4af614ed16c3d9a3b1d17fd8ee2", size = 213837, upload-time = "2026-07-07T14:32:57.78Z" }, + { url = "https://files.pythonhosted.org/packages/b3/46/03ddc7da576d814fe0a36dd1f0fd3258e95404b4b2e3c026b7923d7e133f/charset_normalizer-3.4.9-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:304b13570067b2547562e308af560b3963857b1fa90bd6afd978130130fe2d6a", size = 235503, upload-time = "2026-07-07T14:32:59.205Z" }, + { url = "https://files.pythonhosted.org/packages/4e/6e/de0229a7ef40f6f9d28a837eebf4ec47bdca5dab4e900c84f22919af636a/charset_normalizer-3.4.9-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4773092f8019072343a7447203308b176e10199920eb02d6195e81bbb3274c29", size = 229944, upload-time = "2026-07-07T14:33:00.803Z" }, + { url = "https://files.pythonhosted.org/packages/a5/34/49b9060e8418b14fb5cba9cf6bfb383111e2538a03a1fb18e66a95aeb3d5/charset_normalizer-3.4.9-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:04ce310cb89c15df659582aee80a0603788732a5e017d5bd5c81158106ce249c", size = 221276, upload-time = "2026-07-07T14:33:02.199Z" }, + { url = "https://files.pythonhosted.org/packages/44/95/80282cce0fae9c3061203d723ee87da996aed79679e65d8935050ee7ca1f/charset_normalizer-3.4.9-cp311-cp311-manylinux_2_31_armv7l.whl", hash = "sha256:c0323c9daef75ef2e5083624b4585018a0c9d5e3b40f607eed81a311270b934b", size = 205260, upload-time = "2026-07-07T14:33:03.698Z" }, + { url = "https://files.pythonhosted.org/packages/0c/74/2f62c8821b969ea3bd67cc2e6976834f48ca5d12664d2559ebcd9bcfbed7/charset_normalizer-3.4.9-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:871ff67ea1aad4dfd91736464934d56b32dac49f9fbe16cddba36198a7b3a0db", size = 217786, upload-time = "2026-07-07T14:33:05.12Z" }, + { url = "https://files.pythonhosted.org/packages/d9/8d/feabb82cb49fcad14515b1d7d1ca4787b0da7fc723a212bf89bc9e0fac52/charset_normalizer-3.4.9-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:67830fc78e67501f47bb950471b2dcb9b35b140084429318e862895a8e89c993", size = 216798, upload-time = "2026-07-07T14:33:06.629Z" }, + { url = "https://files.pythonhosted.org/packages/a5/ff/c946d63bc3786d5b84d960b0f7ab7e25b828486a946b5aa997625bcaf6a6/charset_normalizer-3.4.9-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:3d92613ec25e43b05f042302531ec0f00b8445190e43325880cbd6ab7c2581da", size = 206429, upload-time = "2026-07-07T14:33:08.006Z" }, + { url = "https://files.pythonhosted.org/packages/af/ba/5e5007c370702f85d2ef75791fac7943ed41e080364a673b20142e430e3e/charset_normalizer-3.4.9-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:280081916dc341820640489a66e4696049401ef1cf6dd672f672e70ad915aca3", size = 223066, upload-time = "2026-07-07T14:33:09.783Z" }, + { url = "https://files.pythonhosted.org/packages/83/d5/9096aa3cf532dfad237861544eb47a0f20d5adbf1039760fed8eaae935d9/charset_normalizer-3.4.9-cp311-cp311-win32.whl", hash = "sha256:ac351b3b8014eead140e77e9717e2992c6bbe30b63bc3422422eb84865412e3d", size = 150456, upload-time = "2026-07-07T14:33:11.217Z" }, + { url = "https://files.pythonhosted.org/packages/ed/a1/e29995109e455dc8eff8d0fac6ae509be39561318a7cfeac5d33ad029213/charset_normalizer-3.4.9-cp311-cp311-win_amd64.whl", hash = "sha256:6366a16e1a25018694d6a5d784d09b046edc9eac40ea2b54065c3052672516a1", size = 161410, upload-time = "2026-07-07T14:33:12.743Z" }, + { url = "https://files.pythonhosted.org/packages/4f/8d/1569f4d0032d6ba2a4fe4591c35bf87868c600c41a71eb5c2e1ffa8464c2/charset_normalizer-3.4.9-cp311-cp311-win_arm64.whl", hash = "sha256:1d22856ffbe153a602df38e4a5464f0b748a54002e0d69ac6d2ad0a197cc99ec", size = 152649, upload-time = "2026-07-07T14:33:14.173Z" }, + { url = "https://files.pythonhosted.org/packages/70/4a/ecbd131485c07fcdfad54e28946d513e3da22ef3b4bd854dcafae54ec739/charset_normalizer-3.4.9-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:45b0cc4e3556cd875e09102988d1ab8356c998b596c9fced84547c8138b487a0", size = 319300, upload-time = "2026-07-07T14:33:15.666Z" }, + { url = "https://files.pythonhosted.org/packages/ec/96/5d9364e3342d69f3a045e1777bc47c85c383e6e9466d561b33fdb419d1f9/charset_normalizer-3.4.9-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9b2aff1c7b3884512b9512c3eaadd9bab39fb45042ffaaa1dd08ff2b9f8109d9", size = 215802, upload-time = "2026-07-07T14:33:17.031Z" }, + { url = "https://files.pythonhosted.org/packages/4b/4c/5361f9aa7f2cb58d94f2ab831b3d493f69efb1d239654b4744e3c09527cb/charset_normalizer-3.4.9-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9104ed0bd76a429d46f9ec0dbc9b08ad1d2dcdf2b00a5a0daa1c145329b35b44", size = 237171, upload-time = "2026-07-07T14:33:18.576Z" }, + { url = "https://files.pythonhosted.org/packages/50/78/ce342ca4ff30b2eb49fe6d9578df85974f90c67d294113e94efdd9664cbd/charset_normalizer-3.4.9-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7b86a2b16095d250c6f58b3d9b2eee6f4147754344f3dab0922f7c9bf7d226c9", size = 233075, upload-time = "2026-07-07T14:33:20.084Z" }, + { url = "https://files.pythonhosted.org/packages/01/c4/4fa4c8b3097a11f3c5f09a35b72ed6855fb1d332469504962ab7bafcc702/charset_normalizer-3.4.9-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5e226f6218febc71f6c1fc2fafb91c226f75bdc1d8fb12d66823716e891608fd", size = 224256, upload-time = "2026-07-07T14:33:21.747Z" }, + { url = "https://files.pythonhosted.org/packages/87/3a/ad914516df7e358a81aae018caa5e0470ba827fa6d763b1d2e87d920a5f6/charset_normalizer-3.4.9-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:90c44bc373b7687f6948b693cceaea1348ae0975d7474746559494468e3c1d84", size = 208784, upload-time = "2026-07-07T14:33:23.313Z" }, + { url = "https://files.pythonhosted.org/packages/d7/74/3c12f9755717dfe5c5c87da63f35d765fa0c00382ec26bf23f7fae34f2ba/charset_normalizer-3.4.9-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9cdef90ae47919cae358d8ab15797a800ed41da7aba5d72419fb510729e2ed4b", size = 219928, upload-time = "2026-07-07T14:33:24.814Z" }, + { url = "https://files.pythonhosted.org/packages/33/9a/895095b83e7907abd6d3d99aad3a38ad0d9686cc186cb0c94c24320fe63e/charset_normalizer-3.4.9-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:60f44ade2cf573dad7a277e6f8ca9a51a21dda572b13bd7d8539bb3cd5dbedde", size = 218489, upload-time = "2026-07-07T14:33:26.42Z" }, + { url = "https://files.pythonhosted.org/packages/a1/34/ef5c05f412f42520d7709b7d3784d19640839eb7366ded1755511585429f/charset_normalizer-3.4.9-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:a1786910334ed46ab1dd73222f2cd1e05c2c3bb39f6dddb4f8b36fc382058a39", size = 210267, upload-time = "2026-07-07T14:33:27.952Z" }, + { url = "https://files.pythonhosted.org/packages/83/dc/9b29fa4412b318bf3bfea985c35d67eb55e04b59a7c3f2237168b0e0be6f/charset_normalizer-3.4.9-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:03d07803992c6c7bbc976327f34b18b6160327fc81cb82c9d504720ac0be3b62", size = 226030, upload-time = "2026-07-07T14:33:29.397Z" }, + { url = "https://files.pythonhosted.org/packages/0e/42/6dbc00b8cd16011691203e33570fa42ed5746599a2e878112d16eab403a3/charset_normalizer-3.4.9-cp312-cp312-win32.whl", hash = "sha256:78841cccf1af7b40f6f716338d50c0902dbe88d9f800b3c973b7a9a0a693a642", size = 151185, upload-time = "2026-07-07T14:33:30.781Z" }, + { url = "https://files.pythonhosted.org/packages/80/cc/f920afd1a23c58ccd53c1d36085a71893a4737ff5e66e0371efab6809850/charset_normalizer-3.4.9-cp312-cp312-win_amd64.whl", hash = "sha256:4b3dac63058cc36820b0dd072f89898604e2d39686fe05321729d00d8ac185a0", size = 162557, upload-time = "2026-07-07T14:33:32.176Z" }, + { url = "https://files.pythonhosted.org/packages/f0/e6/0386d43a261ff4e4b30c5857af7df877254b46bec7b9d1b74b6bf969a90b/charset_normalizer-3.4.9-cp312-cp312-win_arm64.whl", hash = "sha256:78fa18e436a1a0e58dbd7e02fc4473f3f32cceb12df9dfca542d075961c307d2", size = 152665, upload-time = "2026-07-07T14:33:33.711Z" }, + { url = "https://files.pythonhosted.org/packages/98/2b/f97f1c193fb855c345d678f5077d6926034db0722df74c8f057020e05a25/charset_normalizer-3.4.9-py3-none-any.whl", hash = "sha256:68e5f26a1ad57ded6d1cfb85331d1c1a195314756471d97758c48498bb4dcdf5", size = 64538, upload-time = "2026-07-07T14:34:56.993Z" }, +] + +[[package]] +name = "chromadb" +version = "1.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "bcrypt" }, + { name = "build" }, + { name = "grpcio" }, + { name = "httpx" }, + { name = "importlib-resources" }, + { name = "jsonschema" }, + { name = "kubernetes" }, + { name = "mmh3" }, + { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, + { name = "numpy", version = "2.5.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, + { name = "onnxruntime" }, + { name = "opentelemetry-api" }, + { name = "opentelemetry-exporter-otlp-proto-grpc" }, + { name = "opentelemetry-sdk" }, + { name = "orjson" }, + { name = "overrides" }, + { name = "posthog" }, + { name = "pybase64" }, + { name = "pydantic" }, + { name = "pypika" }, + { name = "pyyaml" }, + { name = "rich" }, + { name = "tenacity" }, + { name = "tokenizers" }, + { name = "tqdm" }, + { name = "typer" }, + { name = "typing-extensions" }, + { name = "uvicorn", extra = ["standard"] }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7f/48/11851dddeadad6abe36ee071fedc99b5bdd2c324df3afa8cb952ae02798b/chromadb-1.1.1.tar.gz", hash = "sha256:ebfce0122753e306a76f1e291d4ddaebe5f01b5979b97ae0bc80b1d4024ff223", size = 1338109, upload-time = "2025-10-05T02:49:14.834Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/39/59/0d881a9b7eb63d8d2446cf67fcbb53fb8ae34991759d2b6024a067e90a9a/chromadb-1.1.1-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:27fe0e25ef0f83fb09c30355ab084fe6f246808a7ea29e8c19e85cf45785b90d", size = 19175479, upload-time = "2025-10-05T02:49:12.525Z" }, + { url = "https://files.pythonhosted.org/packages/94/4f/5a9fa317c84c98e70af48f74b00aa25589626c03a0428b4381b2095f3d73/chromadb-1.1.1-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:95aed58869683f12e7dcbf68b039fe5f576dbe9d1b86b8f4d014c9d077ccafd2", size = 18267188, upload-time = "2025-10-05T02:49:09.236Z" }, + { url = "https://files.pythonhosted.org/packages/45/1a/02defe2f1c8d1daedb084bbe85f5b6083510a3ba192ed57797a3649a4310/chromadb-1.1.1-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:06776dad41389a00e7d63d936c3a15c179d502becaf99f75745ee11b062c9b6a", size = 18855754, upload-time = "2025-10-05T02:49:03.299Z" }, + { url = "https://files.pythonhosted.org/packages/5a/0d/80be82717e5dc19839af24558494811b6f2af2b261a8f21c51b872193b09/chromadb-1.1.1-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bba0096a7f5e975875ead23a91c0d41d977fbd3767f60d3305a011b0ace7afd3", size = 19893681, upload-time = "2025-10-05T02:49:06.481Z" }, + { url = "https://files.pythonhosted.org/packages/2d/6e/956e62975305a4e31daf6114a73b3b0683a8f36f8d70b20aabd466770edb/chromadb-1.1.1-cp39-abi3-win_amd64.whl", hash = "sha256:a77aa026a73a18181fd89bbbdb86191c9a82fd42aa0b549ff18d8cae56394c8b", size = 19844042, upload-time = "2025-10-05T02:49:16.925Z" }, +] + +[[package]] +name = "click" +version = "8.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/76/d4/81420972a676e8ffea40450d8c8c92943e7218a78fe9b64359836cc9876b/click-8.4.2.tar.gz", hash = "sha256:9a6cea6e60b17ebe0a44c5cc636d94f09bd66142c1cd7d8b4cd731c4917a15f6", size = 338000, upload-time = "2026-06-24T17:45:15.148Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fb/e2/79c688af8b210d232694e31e59da9f6ec747bae31c3f5946e4e9b98860d5/click-8.4.2-py3-none-any.whl", hash = "sha256:e6f9f66136c816745b9d65817da91d61d957fb16e02e4dcd0552553c5a197b76", size = 119243, upload-time = "2026-06-24T17:45:13.73Z" }, +] + +[[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, 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, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "crewai" +version = "1.15.8" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiofiles" }, + { name = "aiosqlite" }, + { name = "appdirs" }, + { name = "cel-python" }, + { name = "chromadb" }, + { name = "click" }, + { name = "crewai-cli" }, + { name = "crewai-core" }, + { name = "httpx" }, + { name = "instructor" }, + { name = "json-repair" }, + { name = "json5" }, + { name = "jsonref" }, + { name = "lancedb" }, + { name = "mcp" }, + { name = "openai" }, + { name = "openpyxl" }, + { name = "opentelemetry-api" }, + { name = "opentelemetry-exporter-otlp-proto-http" }, + { name = "opentelemetry-sdk" }, + { name = "pdfplumber" }, + { name = "portalocker" }, + { name = "pydantic" }, + { name = "pydantic-settings" }, + { name = "pyjwt" }, + { name = "python-dotenv" }, + { name = "pyyaml" }, + { name = "regex" }, + { name = "tokenizers" }, + { name = "tomli" }, + { name = "tomli-w" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e9/f3/7bc19e31e203d979329da261677bc4f1c2fa4808a5878cb3f62e87f33093/crewai-1.15.8.tar.gz", hash = "sha256:f99532849e77db9af237ffd836fec7e368495d1bb86aec3eca192a9dc404666f", size = 7818988, upload-time = "2026-07-28T15:07:04.108Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/e3/bbf060f477e21618d0fd6fedb8be29dc630a16aa21a87ccb6a9ed5cabcda/crewai-1.15.8-py3-none-any.whl", hash = "sha256:81d32fcacfeecadf2be4adb33ca844b39bba2797a39f88754c8ca51c2a21e3c6", size = 1090246, upload-time = "2026-07-28T15:07:01.879Z" }, +] + +[package.optional-dependencies] +litellm = [ + { name = "litellm" }, +] + +[[package]] +name = "crewai-cli" +version = "1.15.8" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "appdirs" }, + { name = "certifi" }, + { name = "click" }, + { name = "crewai-core" }, + { name = "cryptography" }, + { name = "httpx" }, + { name = "packaging" }, + { name = "pydantic" }, + { name = "pydantic-settings" }, + { name = "pyjwt" }, + { name = "python-dotenv" }, + { name = "rich" }, + { name = "textual" }, + { name = "tomli" }, + { name = "tomli-w" }, + { name = "uv" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/00/9e/e81aeef487e9eee1d7011d0033b1532e270bd68cb62816f33e6a7e3c793b/crewai_cli-1.15.8.tar.gz", hash = "sha256:716eb189d29bf92c36570c9d9cf6221a80b907f65578714c8b5be3e31f4d537b", size = 220296, upload-time = "2026-07-28T15:07:08.068Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8a/df/c5887b7d69ea5cff8721544f331c4c075d4f5cd6ff0790093aac2a399549/crewai_cli-1.15.8-py3-none-any.whl", hash = "sha256:1d8144d035d6457d18a45ee1448c158b18c4ebd8992486fb706331f310ce1ceb", size = 189838, upload-time = "2026-07-28T15:07:06.421Z" }, +] + +[[package]] +name = "crewai-core" +version = "1.15.8" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "appdirs" }, + { name = "cryptography" }, + { name = "httpx" }, + { name = "opentelemetry-api" }, + { name = "opentelemetry-exporter-otlp-proto-http" }, + { name = "opentelemetry-sdk" }, + { name = "packaging" }, + { name = "portalocker" }, + { name = "pydantic" }, + { name = "pyjwt" }, + { name = "rich" }, + { name = "tomli" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/4d/20/597bf2a16004dd0dd8e25a99dc0cc38b3caa1e23000bef803e0e8e50a573/crewai_core-1.15.8.tar.gz", hash = "sha256:64e570b5e4d80caba439f2c87f9370cb9e8014a4824c62543bd33c3807f53b1b", size = 23434, upload-time = "2026-07-28T15:07:10.176Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/61/7e/82401b9ae7e881c3a983737dba61d65b85f47764116fe655eda2820f2638/crewai_core-1.15.8-py3-none-any.whl", hash = "sha256:ba2de39170470fe2579c11e8909471169b23c16a221d1bae5e1afdc195682baa", size = 30975, upload-time = "2026-07-28T15:07:09.226Z" }, +] + +[[package]] +name = "crewai-tools" +version = "1.15.8" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "beautifulsoup4" }, + { name = "crewai" }, + { name = "pymupdf" }, + { name = "python-docx" }, + { name = "pytube" }, + { name = "requests" }, + { name = "tiktoken" }, + { name = "youtube-transcript-api" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/74/9e/41f5c4ba57376dd6482d34836cab0bd788f98985b0631c161ea2fda82648/crewai_tools-1.15.8.tar.gz", hash = "sha256:bb500e45af3a97ed3e92efaa0ebe900cfecbcdf608c453b646265c36db8aea1a", size = 910843, upload-time = "2026-07-28T15:07:15.568Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6c/5d/761bf4ee2d36ca4c11bc9ecff9615f6bb4c070be68c3b8d3ea54f2edc0c5/crewai_tools-1.15.8-py3-none-any.whl", hash = "sha256:b22a0af459f6a5dbd0cc80a122720bdd970e6290284d039870c7b1950fd08730", size = 820630, upload-time = "2026-07-28T15:07:13.913Z" }, +] + +[package.optional-dependencies] +mcp = [ + { name = "mcp" }, + { name = "mcpadapt", version = "0.1.19", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, + { name = "mcpadapt", version = "0.1.20", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, +] + +[[package]] +name = "cryptography" +version = "50.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/de/41/6cbdcf9142d00fe82836fbb51e503e58088575cf7a0fe1dbff6695bf0840/cryptography-50.0.0.tar.gz", hash = "sha256:eeac2acb5a20ed25e0ad6d1df9891a520b78b404266b6d11778f25d5d691a6c9", size = 880201, upload-time = "2026-07-31T14:25:10.11Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c5/5c/59086b4aac5e879d38ddbcf74e4be7ade89cebc3eb199a55da998c3bb46a/cryptography-50.0.0-cp311-abi3-macosx_11_0_arm64.whl", hash = "sha256:031e2d5dd4bb9caa3ca9c82e5a197fd8ae680232cee62603d1a813f3f07e3d03", size = 4001252, upload-time = "2026-07-31T14:23:33.331Z" }, + { url = "https://files.pythonhosted.org/packages/57/ef/8f2df13c7216bcad3e1c74e07f6e193d93e998e114f524a53877c9af27ad/cryptography-50.0.0-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fd9192b7b70c573d7f214eb1ae35e00d359f6f5e4b27c7e21e30de1fc6204645", size = 4719554, upload-time = "2026-07-31T14:23:35.611Z" }, + { url = "https://files.pythonhosted.org/packages/d9/41/029086c34d91052fc3b88bcc8056f709a7c915c7a23b235a54eb800b1c97/cryptography-50.0.0-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:06a32a980526a6ab9a4b9bf8f7385800791e2bb960903cb6b530e4817509a3b7", size = 4702130, upload-time = "2026-07-31T14:23:37.635Z" }, + { url = "https://files.pythonhosted.org/packages/7d/ff/b6ce0954962e7f7b969f850a883744197bb3910bdfd7b6da162eab7d9f68/cryptography-50.0.0-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:a1b30560f2acc95aa8b2e06e716a13dbfc97314747b80d9707e307f77b40d6b3", size = 4725244, upload-time = "2026-07-31T14:23:39.471Z" }, + { url = "https://files.pythonhosted.org/packages/06/1e/63a1027cb7fec360a182208e1b7767d5aa1fe57be3d6aa856e69a321edc0/cryptography-50.0.0-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:8d89f3976b10b4ce31118de72329025f70d2c6ead14a8217c5514dd2c6d5a78f", size = 5342265, upload-time = "2026-07-31T14:23:41.286Z" }, + { url = "https://files.pythonhosted.org/packages/6b/72/a1116d683a6d7ece94590013882515de087edf9ef0e6292aae615a44df73/cryptography-50.0.0-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:b42a28c1844fd9de8f3f7d540e36b66f3a9c83fceac7170ebc7a6a19edd9dcae", size = 4734609, upload-time = "2026-07-31T14:23:43.139Z" }, + { url = "https://files.pythonhosted.org/packages/15/37/36a9c479bbe49acea2636c7fd3360d20f7b7e079c300352011c44850b181/cryptography-50.0.0-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:900131fafd8aead39ac7dd3a7e833be754c17a95cfd91221636949fe4eb0aa8a", size = 4356517, upload-time = "2026-07-31T14:23:44.939Z" }, + { url = "https://files.pythonhosted.org/packages/32/98/8a151d64367204cbc63ec65d37502f1d9c53cf4bfc6ec3c532614dbec60d/cryptography-50.0.0-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:07949c449a1abcf60d1ee6e88956d89404c7df3c8258f46589e912988e551987", size = 4724529, upload-time = "2026-07-31T14:23:46.93Z" }, + { url = "https://files.pythonhosted.org/packages/22/f6/ec13b470172126464a86bf54d2294a46d29837fc51ba3e45d4047946fb5e/cryptography-50.0.0-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:f89831ef99dd7dd169ab06d63a831adb9e20a87aac6d380266bbda5823349169", size = 5299852, upload-time = "2026-07-31T14:23:48.851Z" }, + { url = "https://files.pythonhosted.org/packages/da/3a/f05e32c99d440c9bb891ea0e36c9091891e36be5a9a87ab2ee6ea20729f6/cryptography-50.0.0-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:82148ec5bddac30b51a5b3c1945075f896fa022cb93f8e4a01e9f6ee95292c5f", size = 4734462, upload-time = "2026-07-31T14:23:50.861Z" }, + { url = "https://files.pythonhosted.org/packages/ca/dc/bd72b26be8953f80625f63151efd38eee71c76ca6cf591c08ff34615a79e/cryptography-50.0.0-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:1489e263a8048bb8b6a8bac662eb2d402ea5d2b7b4699b72f385f1e2772db105", size = 4852708, upload-time = "2026-07-31T14:23:52.715Z" }, + { url = "https://files.pythonhosted.org/packages/27/20/c930314a2ab476d15dec966ec87e2e9637bb02b06106b12c0396c57bb603/cryptography-50.0.0-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:7cec5b856506da6defb290f30c9ee687d5f5e8cb0bd3f6459dde43b0b4fa40ef", size = 5004179, upload-time = "2026-07-31T14:23:54.887Z" }, + { url = "https://files.pythonhosted.org/packages/32/2e/c9db68a0c4bfa28e310707527c0ee3a2bd254104d2e02e68f368e197aa4c/cryptography-50.0.0-cp311-abi3-win_amd64.whl", hash = "sha256:bd1c592e4d5974f0d08d4888e432157adba757c66da0246918e43677fafa2d30", size = 3840395, upload-time = "2026-07-31T14:23:56.677Z" }, + { url = "https://files.pythonhosted.org/packages/03/37/73d005be173aff344af30e9fd2a576575cb2391a7101d9cd3842e1fa8cce/cryptography-50.0.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:ccdc4a71a4dabae05de219404f9f4abc38e3b58422177ff93d0da05967dafa07", size = 4036009, upload-time = "2026-07-31T14:24:24.122Z" }, + { url = "https://files.pythonhosted.org/packages/ff/c6/7a6202a534e32103a285b7834a120869557fe198d51d7cfe59754c8bda9c/cryptography-50.0.0-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:910e1d2668e7de9648f2bcee30e180db2a6b15c30f887d7c4c93ddf96e3992e3", size = 4745252, upload-time = "2026-07-31T14:24:26.118Z" }, + { url = "https://files.pythonhosted.org/packages/85/4f/0fa8c2f4428198f15d9ff8d63400e27afbf94ce833f6108da1eb3753f945/cryptography-50.0.0-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a91296cb61e8df6f86d0c19cc4068228da256bf59bf86049fbd821084565327f", size = 4728939, upload-time = "2026-07-31T14:24:27.994Z" }, + { url = "https://files.pythonhosted.org/packages/d1/63/54dd723490ba2dc09b299682c10b38db38f159728bcaae8c591b8af2f22d/cryptography-50.0.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:e722f16708d854fe924790e051061f6704a472c3bac347b6fd88033ea8dd0dc5", size = 4748483, upload-time = "2026-07-31T14:24:30.254Z" }, + { url = "https://files.pythonhosted.org/packages/1d/dd/7c77d26285cc7f6991efce64a0f5b4f9383bfa5dd8c5033003eaf7db4cdb/cryptography-50.0.0-cp39-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:d764dcf130c428ef66786f866dd750f53182bc608813489915e9fc106bb0c82f", size = 5367599, upload-time = "2026-07-31T14:24:32.457Z" }, + { url = "https://files.pythonhosted.org/packages/46/c9/f60aed34c013f317f92817b6c171c2d22a78270fa41109bd4b08af26b194/cryptography-50.0.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:105110f43a471dbd0060b9c9516cb8a6a79233631a04cc2ba16f28323ac6e025", size = 4762647, upload-time = "2026-07-31T14:24:34.599Z" }, + { url = "https://files.pythonhosted.org/packages/be/f3/f9a0173b139372c3a48ed98154b45cc6b9de17c789d5ab552e621c293609/cryptography-50.0.0-cp39-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:828743d939e9629bc267b8e2d08d8bb67cd4319c771a33d4b18b22dd8fb7440a", size = 4385197, upload-time = "2026-07-31T14:24:36.647Z" }, + { url = "https://files.pythonhosted.org/packages/d8/36/83bb81f6e569bc38e1e4a7bc80f29b46bb9601920bc455fc8e888f5d5742/cryptography-50.0.0-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:2a8183b489dc1f7f80f135780fadc1108f14b31b8a40411c7a5b17425f65f28b", size = 4748095, upload-time = "2026-07-31T14:24:39.493Z" }, + { url = "https://files.pythonhosted.org/packages/6b/16/d3008eff98c764979865834c3d386d4fd041b5f52e7f34fc29ac1a5eb515/cryptography-50.0.0-cp39-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:6e7d61120573a7f2cd94cc095f9e81f6967c61ccdf194285aa143ecec8e0b708", size = 5325948, upload-time = "2026-07-31T14:24:41.556Z" }, + { url = "https://files.pythonhosted.org/packages/9c/f8/d97f9603efda3888187bfdb893f26c41be4735c10631d05d284ee6b047c4/cryptography-50.0.0-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:37fdb0d0111f1e2ff07139dfb79f1b49531f8e213c46f1163dd7642979b58c47", size = 4762400, upload-time = "2026-07-31T14:24:43.636Z" }, + { url = "https://files.pythonhosted.org/packages/64/a2/4615c8f7d81a00b1d6e6afe19f694e1543582349fb5f4076f6cb5dc36485/cryptography-50.0.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:c87f62a3d3b9888ed0fdde100ec06aa61ca9cd44bad9057d1dff9a516b5f5bb9", size = 4878208, upload-time = "2026-07-31T14:24:45.522Z" }, + { url = "https://files.pythonhosted.org/packages/d2/1a/efcfb02f91407149a0dacffffab791f7e19bf6385f63b3666dc8b5e5c9c8/cryptography-50.0.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:65c2c3add92b45fd0709db8594536aea39c2a67af0e27ffcf049c498501140b7", size = 5037050, upload-time = "2026-07-31T14:24:47.697Z" }, + { url = "https://files.pythonhosted.org/packages/57/30/4a22984d4f1bdfb8c054f07a92bc176b97a3134cc1d6c4b3bffb1f3688b4/cryptography-50.0.0-cp39-abi3-win_amd64.whl", hash = "sha256:d24fead1d4d076e1bfb006dcec392074a3cd8d7b4fc8a595aa64073b2b7a96ba", size = 3874135, upload-time = "2026-07-31T14:24:50.085Z" }, + { url = "https://files.pythonhosted.org/packages/9d/3e/e54cde8c01631a5a8226ccd617eab9e57fd5cfdad90f1a9e6bb570794631/cryptography-50.0.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:5e34edd123674534acd70147f0ca331eaa2c74e6325fb2028c886aa26ba0b68c", size = 3963170, upload-time = "2026-07-31T14:24:51.968Z" }, + { url = "https://files.pythonhosted.org/packages/01/b6/0b9e125e90f3d2dcf599a218a899cda7326a3158cfa258723f0b398b08f6/cryptography-50.0.0-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:8eb5e1172eb569ea8a872796576e6a67c276351728b6455d5beb01242b027c6a", size = 4692441, upload-time = "2026-07-31T14:24:53.743Z" }, + { url = "https://files.pythonhosted.org/packages/53/c9/a5151588710785a96d7bc4de27d4cd62f263bbbcb203cfe29df537eb6505/cryptography-50.0.0-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:910d11e1a385c654bf738bf3e6b8e6ed5de0f5610fcae2be9e5b398d8081d20e", size = 4699810, upload-time = "2026-07-31T14:24:55.746Z" }, + { url = "https://files.pythonhosted.org/packages/c7/1a/15b92b25eb6ce3089cd49377ae990a0f3ad485a510f968aed1f19dbdcdf2/cryptography-50.0.0-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:62598a8a57f815db4c6259a4e97d857dab56697e7de8e8ab02352ab74da1995d", size = 4691924, upload-time = "2026-07-31T14:24:58.082Z" }, + { url = "https://files.pythonhosted.org/packages/62/15/219075012ab13e8905f3cd572204f4acb4b111df787104346b9bc0cea789/cryptography-50.0.0-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:07479a1cb08219ab719147e742e76090c9c773321959bb94946fffdd397a6437", size = 4699593, upload-time = "2026-07-31T14:24:59.951Z" }, + { url = "https://files.pythonhosted.org/packages/8e/b5/c2c5fce26f0ee40d21bafe7f191d29a34b35a65ac4fe8a1191d1983612e9/cryptography-50.0.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:c99c003e088647b8a5b7c145d6f78c335f6348332b62e142d411c4b63d1460b9", size = 3813796, upload-time = "2026-07-31T14:25:02.298Z" }, +] + +[[package]] +name = "culsans" +version = "0.11.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiologic" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d9/e3/49afa1bc180e0d28008ec6bcdf82a4072d1c7a41032b5b759b60814ca4b0/culsans-0.11.0.tar.gz", hash = "sha256:0b43d0d05dce6106293d114c86e3fb4bfc63088cfe8ff08ed3fe36891447fe33", size = 107546, upload-time = "2025-12-31T23:15:38.196Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e0/5d/9fb19fb38f6d6120422064279ea5532e22b84aa2be8831d49607194feda3/culsans-0.11.0-py3-none-any.whl", hash = "sha256:278d118f63fc75b9db11b664b436a1b83cc30d9577127848ba41420e66eb5a47", size = 21811, upload-time = "2025-12-31T23:15:37.189Z" }, +] + +[[package]] +name = "defusedxml" +version = "0.7.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0f/d5/c66da9b79e5bdb124974bfe172b4daf3c984ebd9c2a06e2b8a4dc7331c72/defusedxml-0.7.1.tar.gz", hash = "sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69", size = 75520, upload-time = "2021-03-08T10:59:26.269Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/07/6c/aa3f2f849e01cb6a001cd8554a88d4c77c5c1a31c95bdf1cf9301e6d9ef4/defusedxml-0.7.1-py2.py3-none-any.whl", hash = "sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61", size = 25604, upload-time = "2021-03-08T10:59:24.45Z" }, +] + +[[package]] +name = "deprecation" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "packaging" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5a/d3/8ae2869247df154b64c1884d7346d412fed0c49df84db635aab2d1c40e62/deprecation-2.1.0.tar.gz", hash = "sha256:72b3bde64e5d778694b0cf68178aed03d15e15477116add3fb773e581f9518ff", size = 173788, upload-time = "2020-04-20T14:23:38.738Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/02/c3/253a89ee03fc9b9682f1541728eb66db7db22148cd94f89ab22528cd1e1b/deprecation-2.1.0-py2.py3-none-any.whl", hash = "sha256:a10811591210e1fb0e768a8c25517cabeabcba6f0bf96564f8ff45189f90b14a", size = 11178, upload-time = "2020-04-20T14:23:36.581Z" }, +] + +[[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, 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, upload-time = "2023-12-24T09:54:30.421Z" }, +] + +[[package]] +name = "docstring-parser" +version = "0.18.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e0/4d/f332313098c1de1b2d2ff91cf2674415cc7cddab2ca1b01ae29774bd5fdf/docstring_parser-0.18.0.tar.gz", hash = "sha256:292510982205c12b1248696f44959db3cdd1740237a968ea1e2e7a900eeb2015", size = 29341, upload-time = "2026-04-14T04:09:19.867Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a7/5f/ed01f9a3cdffbd5a008556fc7b2a08ddb1cc6ace7effa7340604b1d16699/docstring_parser-0.18.0-py3-none-any.whl", hash = "sha256:b3fcbed555c47d8479be0796ef7e19c2670d428d72e96da63f3a40122860374b", size = 22484, upload-time = "2026-04-14T04:09:18.638Z" }, +] + +[[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, 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, upload-time = "2025-05-17T13:52:36.463Z" }, +] + +[[package]] +name = "et-xmlfile" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d3/38/af70d7ab1ae9d4da450eeec1fa3918940a5fafb9055e934af8d6eb0c2313/et_xmlfile-2.0.0.tar.gz", hash = "sha256:dab3f4764309081ce75662649be815c4c9081e88f0837825f90fd28317d4da54", size = 17234, upload-time = "2024-10-25T17:25:40.039Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c1/8b/5fe2cc11fee489817272089c4203e679c63b570a5aaeb18d852ae3cbba6a/et_xmlfile-2.0.0-py3-none-any.whl", hash = "sha256:7a91720bc756843502c3b7504c77b8fe44217c85c537d85037f0f536151b2caa", size = 18059, upload-time = "2024-10-25T17:25:39.051Z" }, +] + +[[package]] +name = "fastuuid" +version = "0.14.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c3/7d/d9daedf0f2ebcacd20d599928f8913e9d2aea1d56d2d355a93bfa2b611d7/fastuuid-0.14.0.tar.gz", hash = "sha256:178947fc2f995b38497a74172adee64fdeb8b7ec18f2a5934d037641ba265d26", size = 18232, upload-time = "2025-10-19T22:19:22.402Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/98/f3/12481bda4e5b6d3e698fbf525df4443cc7dce746f246b86b6fcb2fba1844/fastuuid-0.14.0-cp311-cp311-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:73946cb950c8caf65127d4e9a325e2b6be0442a224fd51ba3b6ac44e1912ce34", size = 516386, upload-time = "2025-10-19T22:42:40.176Z" }, + { url = "https://files.pythonhosted.org/packages/59/19/2fc58a1446e4d72b655648eb0879b04e88ed6fa70d474efcf550f640f6ec/fastuuid-0.14.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:12ac85024637586a5b69645e7ed986f7535106ed3013640a393a03e461740cb7", size = 264569, upload-time = "2025-10-19T22:25:50.977Z" }, + { url = "https://files.pythonhosted.org/packages/78/29/3c74756e5b02c40cfcc8b1d8b5bac4edbd532b55917a6bcc9113550e99d1/fastuuid-0.14.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:05a8dde1f395e0c9b4be515b7a521403d1e8349443e7641761af07c7ad1624b1", size = 254366, upload-time = "2025-10-19T22:29:49.166Z" }, + { url = "https://files.pythonhosted.org/packages/52/96/d761da3fccfa84f0f353ce6e3eb8b7f76b3aa21fd25e1b00a19f9c80a063/fastuuid-0.14.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:09378a05020e3e4883dfdab438926f31fea15fd17604908f3d39cbeb22a0b4dc", size = 278978, upload-time = "2025-10-19T22:35:41.306Z" }, + { url = "https://files.pythonhosted.org/packages/fc/c2/f84c90167cc7765cb82b3ff7808057608b21c14a38531845d933a4637307/fastuuid-0.14.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bbb0c4b15d66b435d2538f3827f05e44e2baafcc003dd7d8472dc67807ab8fd8", size = 279692, upload-time = "2025-10-19T22:25:36.997Z" }, + { url = "https://files.pythonhosted.org/packages/af/7b/4bacd03897b88c12348e7bd77943bac32ccf80ff98100598fcff74f75f2e/fastuuid-0.14.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:cd5a7f648d4365b41dbf0e38fe8da4884e57bed4e77c83598e076ac0c93995e7", size = 303384, upload-time = "2025-10-19T22:29:46.578Z" }, + { url = "https://files.pythonhosted.org/packages/c0/a2/584f2c29641df8bd810d00c1f21d408c12e9ad0c0dafdb8b7b29e5ddf787/fastuuid-0.14.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:c0a94245afae4d7af8c43b3159d5e3934c53f47140be0be624b96acd672ceb73", size = 460921, upload-time = "2025-10-19T22:36:42.006Z" }, + { url = "https://files.pythonhosted.org/packages/24/68/c6b77443bb7764c760e211002c8638c0c7cce11cb584927e723215ba1398/fastuuid-0.14.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:2b29e23c97e77c3a9514d70ce343571e469098ac7f5a269320a0f0b3e193ab36", size = 480575, upload-time = "2025-10-19T22:28:18.975Z" }, + { url = "https://files.pythonhosted.org/packages/5a/87/93f553111b33f9bb83145be12868c3c475bf8ea87c107063d01377cc0e8e/fastuuid-0.14.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:1e690d48f923c253f28151b3a6b4e335f2b06bf669c68a02665bc150b7839e94", size = 452317, upload-time = "2025-10-19T22:25:32.75Z" }, + { url = "https://files.pythonhosted.org/packages/9e/8c/a04d486ca55b5abb7eaa65b39df8d891b7b1635b22db2163734dc273579a/fastuuid-0.14.0-cp311-cp311-win32.whl", hash = "sha256:a6f46790d59ab38c6aa0e35c681c0484b50dc0acf9e2679c005d61e019313c24", size = 154804, upload-time = "2025-10-19T22:24:15.615Z" }, + { url = "https://files.pythonhosted.org/packages/9c/b2/2d40bf00820de94b9280366a122cbaa60090c8cf59e89ac3938cf5d75895/fastuuid-0.14.0-cp311-cp311-win_amd64.whl", hash = "sha256:e150eab56c95dc9e3fefc234a0eedb342fac433dacc273cd4d150a5b0871e1fa", size = 156099, upload-time = "2025-10-19T22:24:31.646Z" }, + { url = "https://files.pythonhosted.org/packages/02/a2/e78fcc5df65467f0d207661b7ef86c5b7ac62eea337c0c0fcedbeee6fb13/fastuuid-0.14.0-cp312-cp312-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:77e94728324b63660ebf8adb27055e92d2e4611645bf12ed9d88d30486471d0a", size = 510164, upload-time = "2025-10-19T22:31:45.635Z" }, + { url = "https://files.pythonhosted.org/packages/2b/b3/c846f933f22f581f558ee63f81f29fa924acd971ce903dab1a9b6701816e/fastuuid-0.14.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:caa1f14d2102cb8d353096bc6ef6c13b2c81f347e6ab9d6fbd48b9dea41c153d", size = 261837, upload-time = "2025-10-19T22:38:38.53Z" }, + { url = "https://files.pythonhosted.org/packages/54/ea/682551030f8c4fa9a769d9825570ad28c0c71e30cf34020b85c1f7ee7382/fastuuid-0.14.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d23ef06f9e67163be38cece704170486715b177f6baae338110983f99a72c070", size = 251370, upload-time = "2025-10-19T22:40:26.07Z" }, + { url = "https://files.pythonhosted.org/packages/14/dd/5927f0a523d8e6a76b70968e6004966ee7df30322f5fc9b6cdfb0276646a/fastuuid-0.14.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0c9ec605ace243b6dbe3bd27ebdd5d33b00d8d1d3f580b39fdd15cd96fd71796", size = 277766, upload-time = "2025-10-19T22:37:23.779Z" }, + { url = "https://files.pythonhosted.org/packages/16/6e/c0fb547eef61293153348f12e0f75a06abb322664b34a1573a7760501336/fastuuid-0.14.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:808527f2407f58a76c916d6aa15d58692a4a019fdf8d4c32ac7ff303b7d7af09", size = 278105, upload-time = "2025-10-19T22:26:56.821Z" }, + { url = "https://files.pythonhosted.org/packages/2d/b1/b9c75e03b768f61cf2e84ee193dc18601aeaf89a4684b20f2f0e9f52b62c/fastuuid-0.14.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2fb3c0d7fef6674bbeacdd6dbd386924a7b60b26de849266d1ff6602937675c8", size = 301564, upload-time = "2025-10-19T22:30:31.604Z" }, + { url = "https://files.pythonhosted.org/packages/fc/fa/f7395fdac07c7a54f18f801744573707321ca0cee082e638e36452355a9d/fastuuid-0.14.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:ab3f5d36e4393e628a4df337c2c039069344db5f4b9d2a3c9cea48284f1dd741", size = 459659, upload-time = "2025-10-19T22:31:32.341Z" }, + { url = "https://files.pythonhosted.org/packages/66/49/c9fd06a4a0b1f0f048aacb6599e7d96e5d6bc6fa680ed0d46bf111929d1b/fastuuid-0.14.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:b9a0ca4f03b7e0b01425281ffd44e99d360e15c895f1907ca105854ed85e2057", size = 478430, upload-time = "2025-10-19T22:26:22.962Z" }, + { url = "https://files.pythonhosted.org/packages/be/9c/909e8c95b494e8e140e8be6165d5fc3f61fdc46198c1554df7b3e1764471/fastuuid-0.14.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:3acdf655684cc09e60fb7e4cf524e8f42ea760031945aa8086c7eae2eeeabeb8", size = 450894, upload-time = "2025-10-19T22:27:01.647Z" }, + { url = "https://files.pythonhosted.org/packages/90/eb/d29d17521976e673c55ef7f210d4cdd72091a9ec6755d0fd4710d9b3c871/fastuuid-0.14.0-cp312-cp312-win32.whl", hash = "sha256:9579618be6280700ae36ac42c3efd157049fe4dd40ca49b021280481c78c3176", size = 154374, upload-time = "2025-10-19T22:29:19.879Z" }, + { url = "https://files.pythonhosted.org/packages/cc/fc/f5c799a6ea6d877faec0472d0b27c079b47c86b1cdc577720a5386483b36/fastuuid-0.14.0-cp312-cp312-win_amd64.whl", hash = "sha256:d9e4332dc4ba054434a9594cbfaf7823b57993d7d8e7267831c3e059857cf397", size = 156550, upload-time = "2025-10-19T22:27:49.658Z" }, +] + +[[package]] +name = "filelock" +version = "3.29.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/35/94/00f2059e4835eace3ae8fde680b932c496f8ec7bdc99168dfa53fb2e6b79/filelock-3.29.7.tar.gz", hash = "sha256:5b481979797ae69e72f0b389d89a80bdd585c260c5b3f1fb9c0a5ba9bb3f195d", size = 71521, upload-time = "2026-07-08T05:46:58.716Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/60/02/be4a57b60c7149b55b9e3b3c13f609cd8eb5307c751f22bd8fb8d262e75b/filelock-3.29.7-py3-none-any.whl", hash = "sha256:987db6f789a3a2a59f55081801b2b3697cb97e2a736b5f1a9e99b559285fbc51", size = 46036, upload-time = "2026-07-08T05:46:57.53Z" }, +] + +[[package]] +name = "flatbuffers" +version = "25.12.19" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e8/2d/d2a548598be01649e2d46231d151a6c56d10b964d94043a335ae56ea2d92/flatbuffers-25.12.19-py2.py3-none-any.whl", hash = "sha256:7634f50c427838bb021c2d66a3d1168e9d199b0607e6329399f04846d42e20b4", size = 26661, upload-time = "2025-12-19T23:16:13.622Z" }, +] + +[[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, upload-time = "2025-10-06T05:38:17.865Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bc/03/077f869d540370db12165c0aa51640a873fb661d8b315d1d4d67b284d7ac/frozenlist-1.8.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:09474e9831bc2b2199fad6da3c14c7b0fbdd377cce9d3d77131be28906cb7d84", size = 86912, upload-time = "2025-10-06T05:35:45.98Z" }, + { url = "https://files.pythonhosted.org/packages/df/b5/7610b6bd13e4ae77b96ba85abea1c8cb249683217ef09ac9e0ae93f25a91/frozenlist-1.8.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:17c883ab0ab67200b5f964d2b9ed6b00971917d5d8a92df149dc2c9779208ee9", size = 50046, upload-time = "2025-10-06T05:35:47.009Z" }, + { url = "https://files.pythonhosted.org/packages/6e/ef/0e8f1fe32f8a53dd26bdd1f9347efe0778b0fddf62789ea683f4cc7d787d/frozenlist-1.8.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:fa47e444b8ba08fffd1c18e8cdb9a75db1b6a27f17507522834ad13ed5922b93", size = 50119, upload-time = "2025-10-06T05:35:48.38Z" }, + { url = "https://files.pythonhosted.org/packages/11/b1/71a477adc7c36e5fb628245dfbdea2166feae310757dea848d02bd0689fd/frozenlist-1.8.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2552f44204b744fba866e573be4c1f9048d6a324dfe14475103fd51613eb1d1f", size = 231067, upload-time = "2025-10-06T05:35:49.97Z" }, + { url = "https://files.pythonhosted.org/packages/45/7e/afe40eca3a2dc19b9904c0f5d7edfe82b5304cb831391edec0ac04af94c2/frozenlist-1.8.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:957e7c38f250991e48a9a73e6423db1bb9dd14e722a10f6b8bb8e16a0f55f695", size = 233160, upload-time = "2025-10-06T05:35:51.729Z" }, + { url = "https://files.pythonhosted.org/packages/a6/aa/7416eac95603ce428679d273255ffc7c998d4132cfae200103f164b108aa/frozenlist-1.8.0-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:8585e3bb2cdea02fc88ffa245069c36555557ad3609e83be0ec71f54fd4abb52", size = 228544, upload-time = "2025-10-06T05:35:53.246Z" }, + { url = "https://files.pythonhosted.org/packages/8b/3d/2a2d1f683d55ac7e3875e4263d28410063e738384d3adc294f5ff3d7105e/frozenlist-1.8.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:edee74874ce20a373d62dc28b0b18b93f645633c2943fd90ee9d898550770581", size = 243797, upload-time = "2025-10-06T05:35:54.497Z" }, + { url = "https://files.pythonhosted.org/packages/78/1e/2d5565b589e580c296d3bb54da08d206e797d941a83a6fdea42af23be79c/frozenlist-1.8.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c9a63152fe95756b85f31186bddf42e4c02c6321207fd6601a1c89ebac4fe567", size = 247923, upload-time = "2025-10-06T05:35:55.861Z" }, + { url = "https://files.pythonhosted.org/packages/aa/c3/65872fcf1d326a7f101ad4d86285c403c87be7d832b7470b77f6d2ed5ddc/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b6db2185db9be0a04fecf2f241c70b63b1a242e2805be291855078f2b404dd6b", size = 230886, upload-time = "2025-10-06T05:35:57.399Z" }, + { url = "https://files.pythonhosted.org/packages/a0/76/ac9ced601d62f6956f03cc794f9e04c81719509f85255abf96e2510f4265/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:f4be2e3d8bc8aabd566f8d5b8ba7ecc09249d74ba3c9ed52e54dc23a293f0b92", size = 245731, upload-time = "2025-10-06T05:35:58.563Z" }, + { url = "https://files.pythonhosted.org/packages/b9/49/ecccb5f2598daf0b4a1415497eba4c33c1e8ce07495eb07d2860c731b8d5/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:c8d1634419f39ea6f5c427ea2f90ca85126b54b50837f31497f3bf38266e853d", size = 241544, upload-time = "2025-10-06T05:35:59.719Z" }, + { url = "https://files.pythonhosted.org/packages/53/4b/ddf24113323c0bbcc54cb38c8b8916f1da7165e07b8e24a717b4a12cbf10/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:1a7fa382a4a223773ed64242dbe1c9c326ec09457e6b8428efb4118c685c3dfd", size = 241806, upload-time = "2025-10-06T05:36:00.959Z" }, + { url = "https://files.pythonhosted.org/packages/a7/fb/9b9a084d73c67175484ba2789a59f8eebebd0827d186a8102005ce41e1ba/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:11847b53d722050808926e785df837353bd4d75f1d494377e59b23594d834967", size = 229382, upload-time = "2025-10-06T05:36:02.22Z" }, + { url = "https://files.pythonhosted.org/packages/95/a3/c8fb25aac55bf5e12dae5c5aa6a98f85d436c1dc658f21c3ac73f9fa95e5/frozenlist-1.8.0-cp311-cp311-win32.whl", hash = "sha256:27c6e8077956cf73eadd514be8fb04d77fc946a7fe9f7fe167648b0b9085cc25", size = 39647, upload-time = "2025-10-06T05:36:03.409Z" }, + { url = "https://files.pythonhosted.org/packages/0a/f5/603d0d6a02cfd4c8f2a095a54672b3cf967ad688a60fb9faf04fc4887f65/frozenlist-1.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:ac913f8403b36a2c8610bbfd25b8013488533e71e62b4b4adce9c86c8cea905b", size = 44064, upload-time = "2025-10-06T05:36:04.368Z" }, + { url = "https://files.pythonhosted.org/packages/5d/16/c2c9ab44e181f043a86f9a8f84d5124b62dbcb3a02c0977ec72b9ac1d3e0/frozenlist-1.8.0-cp311-cp311-win_arm64.whl", hash = "sha256:d4d3214a0f8394edfa3e303136d0575eece0745ff2b47bd2cb2e66dd92d4351a", size = 39937, upload-time = "2025-10-06T05:36:05.669Z" }, + { url = "https://files.pythonhosted.org/packages/69/29/948b9aa87e75820a38650af445d2ef2b6b8a6fab1a23b6bb9e4ef0be2d59/frozenlist-1.8.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:78f7b9e5d6f2fdb88cdde9440dc147259b62b9d3b019924def9f6478be254ac1", size = 87782, upload-time = "2025-10-06T05:36:06.649Z" }, + { url = "https://files.pythonhosted.org/packages/64/80/4f6e318ee2a7c0750ed724fa33a4bdf1eacdc5a39a7a24e818a773cd91af/frozenlist-1.8.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:229bf37d2e4acdaf808fd3f06e854a4a7a3661e871b10dc1f8f1896a3b05f18b", size = 50594, upload-time = "2025-10-06T05:36:07.69Z" }, + { url = "https://files.pythonhosted.org/packages/2b/94/5c8a2b50a496b11dd519f4a24cb5496cf125681dd99e94c604ccdea9419a/frozenlist-1.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f833670942247a14eafbb675458b4e61c82e002a148f49e68257b79296e865c4", size = 50448, upload-time = "2025-10-06T05:36:08.78Z" }, + { url = "https://files.pythonhosted.org/packages/6a/bd/d91c5e39f490a49df14320f4e8c80161cfcce09f1e2cde1edd16a551abb3/frozenlist-1.8.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:494a5952b1c597ba44e0e78113a7266e656b9794eec897b19ead706bd7074383", size = 242411, upload-time = "2025-10-06T05:36:09.801Z" }, + { url = "https://files.pythonhosted.org/packages/8f/83/f61505a05109ef3293dfb1ff594d13d64a2324ac3482be2cedc2be818256/frozenlist-1.8.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96f423a119f4777a4a056b66ce11527366a8bb92f54e541ade21f2374433f6d4", size = 243014, upload-time = "2025-10-06T05:36:11.394Z" }, + { url = "https://files.pythonhosted.org/packages/d8/cb/cb6c7b0f7d4023ddda30cf56b8b17494eb3a79e3fda666bf735f63118b35/frozenlist-1.8.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3462dd9475af2025c31cc61be6652dfa25cbfb56cbbf52f4ccfe029f38decaf8", size = 234909, upload-time = "2025-10-06T05:36:12.598Z" }, + { url = "https://files.pythonhosted.org/packages/31/c5/cd7a1f3b8b34af009fb17d4123c5a778b44ae2804e3ad6b86204255f9ec5/frozenlist-1.8.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c4c800524c9cd9bac5166cd6f55285957fcfc907db323e193f2afcd4d9abd69b", size = 250049, upload-time = "2025-10-06T05:36:14.065Z" }, + { url = "https://files.pythonhosted.org/packages/c0/01/2f95d3b416c584a1e7f0e1d6d31998c4a795f7544069ee2e0962a4b60740/frozenlist-1.8.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d6a5df73acd3399d893dafc71663ad22534b5aa4f94e8a2fabfe856c3c1b6a52", size = 256485, upload-time = "2025-10-06T05:36:15.39Z" }, + { url = "https://files.pythonhosted.org/packages/ce/03/024bf7720b3abaebcff6d0793d73c154237b85bdf67b7ed55e5e9596dc9a/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:405e8fe955c2280ce66428b3ca55e12b3c4e9c336fb2103a4937e891c69a4a29", size = 237619, upload-time = "2025-10-06T05:36:16.558Z" }, + { url = "https://files.pythonhosted.org/packages/69/fa/f8abdfe7d76b731f5d8bd217827cf6764d4f1d9763407e42717b4bed50a0/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:908bd3f6439f2fef9e85031b59fd4f1297af54415fb60e4254a95f75b3cab3f3", size = 250320, upload-time = "2025-10-06T05:36:17.821Z" }, + { url = "https://files.pythonhosted.org/packages/f5/3c/b051329f718b463b22613e269ad72138cc256c540f78a6de89452803a47d/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:294e487f9ec720bd8ffcebc99d575f7eff3568a08a253d1ee1a0378754b74143", size = 246820, upload-time = "2025-10-06T05:36:19.046Z" }, + { url = "https://files.pythonhosted.org/packages/0f/ae/58282e8f98e444b3f4dd42448ff36fa38bef29e40d40f330b22e7108f565/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:74c51543498289c0c43656701be6b077f4b265868fa7f8a8859c197006efb608", size = 250518, upload-time = "2025-10-06T05:36:20.763Z" }, + { url = "https://files.pythonhosted.org/packages/8f/96/007e5944694d66123183845a106547a15944fbbb7154788cbf7272789536/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:776f352e8329135506a1d6bf16ac3f87bc25b28e765949282dcc627af36123aa", size = 239096, upload-time = "2025-10-06T05:36:22.129Z" }, + { url = "https://files.pythonhosted.org/packages/66/bb/852b9d6db2fa40be96f29c0d1205c306288f0684df8fd26ca1951d461a56/frozenlist-1.8.0-cp312-cp312-win32.whl", hash = "sha256:433403ae80709741ce34038da08511d4a77062aa924baf411ef73d1146e74faf", size = 39985, upload-time = "2025-10-06T05:36:23.661Z" }, + { url = "https://files.pythonhosted.org/packages/b8/af/38e51a553dd66eb064cdf193841f16f077585d4d28394c2fa6235cb41765/frozenlist-1.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:34187385b08f866104f0c0617404c8eb08165ab1272e884abc89c112e9c00746", size = 44591, upload-time = "2025-10-06T05:36:24.958Z" }, + { url = "https://files.pythonhosted.org/packages/a7/06/1dc65480ab147339fecc70797e9c2f69d9cea9cf38934ce08df070fdb9cb/frozenlist-1.8.0-cp312-cp312-win_arm64.whl", hash = "sha256:fe3c58d2f5db5fbd18c2987cba06d51b0529f52bc3a6cdc33d3f4eab725104bd", size = 40102, upload-time = "2025-10-06T05:36:26.333Z" }, + { 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 = "2026.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/10/a1/ae4e3e5003468d6391d2c77b6fa1cd73bd5d13511d81c642d7b28ac90ed4/fsspec-2026.6.0.tar.gz", hash = "sha256:f5bac145310fe30e16e1471bd6840b2d990d609e872251d7e674241822abf01a", size = 313646, upload-time = "2026-06-16T01:57:28.105Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e5/22/4222d7ddf3da30f363edaa98e329c2bce6c65497c9cb2810931c8b2c0fbc/fsspec-2026.6.0-py3-none-any.whl", hash = "sha256:02e0b71817df9b2169dc30a16832045764def1191b43dcff5bb85bdee212d2a1", size = 203949, upload-time = "2026-06-16T01:57:26.358Z" }, +] + +[[package]] +name = "github-agent" +version = "0.1.0" +source = { editable = "." } +dependencies = [ + { name = "a2a-sdk" }, + { name = "aiohttp" }, + { name = "crewai", extra = ["litellm"] }, + { name = "crewai-tools", extra = ["mcp"] }, + { name = "cryptography" }, + { name = "json-repair" }, + { name = "litellm" }, + { name = "lxml" }, + { name = "orjson" }, + { name = "pillow" }, + { name = "pyasn1" }, + { name = "python-dotenv" }, + { name = "python-multipart" }, + { name = "starlette" }, + { name = "urllib3" }, +] + +[package.dev-dependencies] +dev = [ + { name = "pytest" }, +] + +[package.metadata] +requires-dist = [ + { name = "a2a-sdk", specifier = ">=1.1.0,<2" }, + { name = "aiohttp", specifier = ">=3.14.3" }, + { name = "crewai", extras = ["litellm"], specifier = ">=1.6.1" }, + { name = "crewai-tools", extras = ["mcp"], specifier = ">=1.6.1" }, + { name = "cryptography", specifier = ">=50.0.0" }, + { name = "json-repair", specifier = ">=0.60.1" }, + { name = "litellm", specifier = ">=1.90.2" }, + { name = "lxml", specifier = ">=6.1.1" }, + { name = "orjson", specifier = ">=3.11.6" }, + { name = "pillow", specifier = ">=12.3.0" }, + { name = "pyasn1", specifier = ">=0.6.3" }, + { name = "python-dotenv", specifier = ">=1.2.2" }, + { name = "python-multipart", specifier = ">=0.0.32" }, + { name = "starlette", specifier = ">=1.3.1" }, + { name = "urllib3", specifier = ">=2.7.0" }, +] + +[package.metadata.requires-dev] +dev = [{ name = "pytest", specifier = ">=9.1.1" }] + +[[package]] +name = "google-api-core" +version = "2.31.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "google-auth" }, + { name = "googleapis-common-protos" }, + { name = "proto-plus" }, + { name = "protobuf" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c6/22/155cadf1d49272a9cf48f3168c0f3874fa13397297e611a5ea00cd093880/google_api_core-2.31.0.tar.gz", hash = "sha256:2be84ee0f584c48e6bde1b36766e23348b361fb7e55e56135fc76ce1c397f9c2", size = 176492, upload-time = "2026-06-03T14:52:17.257Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/86/40/9bdbb60b03a332bd45acb8703da08bbc27d991d35286b62e42acc86d243a/google_api_core-2.31.0-py3-none-any.whl", hash = "sha256:ef79fb3784c71cbac89cbd03301ba0c8fb8ad2aa95d7f9204dd9628f7adf59ab", size = 173102, upload-time = "2026-06-03T14:51:26.729Z" }, +] + +[[package]] +name = "google-auth" +version = "2.55.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cryptography" }, + { name = "pyasn1-modules" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/79/b9/e370d86fea3da13ec0256df30323dd26c0cb9c8c85f0c6ec42ac9df0106b/google_auth-2.55.2.tar.gz", hash = "sha256:97ae7790ff740f2bc9db60eb864a7804f4ac19f5f02c38b3d942f2fea6e9b9ae", size = 361414, upload-time = "2026-07-07T18:43:21.227Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/c6/02eb5a337ac316a4c30c012e747bad5cea36e1a876efecdf80865541f7d8/google_auth-2.55.2-py3-none-any.whl", hash = "sha256:d715f265f2cafc6a5f1bf0dc19870d20e3119f6f6682785a250bce3d03d38a3b", size = 256778, upload-time = "2026-07-07T18:43:19.52Z" }, +] + +[[package]] +name = "google-re2" +version = "1.1.20251105" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6b/60/805c654ba53d685513df955ee745f71920fe8e6a284faf0f9b9dc19b659c/google_re2-1.1.20251105.tar.gz", hash = "sha256:1db14a292ee8303b91e91e7c37e05ac17d3c467f29416c79ac70a78be3e65bda", size = 11676, upload-time = "2025-11-05T14:58:07.324Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8d/4d/203a08dab1bdb5c83b46dd424c01a789ecb5a37dbc80f33d016bd116a9d7/google_re2-1.1.20251105-1-cp311-cp311-macosx_13_0_arm64.whl", hash = "sha256:329efa209ea7baa44f0facf0402fa34e655dc97fdeb10d0b83fc06354f5575fd", size = 483717, upload-time = "2025-11-05T14:57:04.808Z" }, + { url = "https://files.pythonhosted.org/packages/78/88/466026b43ff5c7d740f5ede090992ec63b60d1810ab14fe35dfc00677e0a/google_re2-1.1.20251105-1-cp311-cp311-macosx_13_0_x86_64.whl", hash = "sha256:aa2ad5f6f48921ec137a7b7f1b1da903ddef8627a2dc30bc878a9a69d9925719", size = 515547, upload-time = "2025-11-05T14:57:06.013Z" }, + { url = "https://files.pythonhosted.org/packages/f3/6a/c6c9fdb00c98990e4f7a6cd650e209d7b5d2754ca0404b72c69ac9909a69/google_re2-1.1.20251105-1-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:ac1cb2526cc88f050a0661fc7245ad009ee454bddc541b2e653f1d007585000d", size = 485396, upload-time = "2025-11-05T14:57:07.592Z" }, + { url = "https://files.pythonhosted.org/packages/a2/f6/529c44f607c47f96cfa29c1fe3a690fe75b2fdb48e9b0d6b54e5f0a75e59/google_re2-1.1.20251105-1-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:50c7205182ad66c23c07abe8072f720ca2f7d595b61e28fd9b63623614f9afd6", size = 517150, upload-time = "2025-11-05T14:57:09.376Z" }, + { url = "https://files.pythonhosted.org/packages/df/d2/ccc07860e31ab81965c63f9ed4eb69ea0d3449a9b4e1610f71883694bbe8/google_re2-1.1.20251105-1-cp311-cp311-macosx_15_0_arm64.whl", hash = "sha256:4cb5acee61e35772503b8b1db3c592a46b8e6a9bc0ab54d7d6233654ea2bf93d", size = 482807, upload-time = "2025-11-05T14:57:11.057Z" }, + { url = "https://files.pythonhosted.org/packages/bd/43/5fb20d16664457f61670bdd95f39039d43ee8b7732511c688e2f322a4317/google_re2-1.1.20251105-1-cp311-cp311-macosx_15_0_x86_64.whl", hash = "sha256:1617097d63620c2d46bdfc0e48f24f66cd341664fc75718636d234f67473fe7f", size = 508839, upload-time = "2025-11-05T14:57:12.338Z" }, + { url = "https://files.pythonhosted.org/packages/0e/f2/6e470338271e164dd3c5e508876f99aec3ed23bf419c7d54a5672fd5b05f/google_re2-1.1.20251105-1-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:18a5610b26742b90cb1d64ead2b16fe0e3bd7e67add03fd3779cd1b85e401661", size = 573718, upload-time = "2025-11-05T14:57:13.635Z" }, + { url = "https://files.pythonhosted.org/packages/91/21/4566fc344c21cf3c49082d13ddab785994b5e3b8b7fd4631242538f698a2/google_re2-1.1.20251105-1-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:03156291269f145eccddff63118f2df02d395792f51fc039f09955818943815a", size = 590749, upload-time = "2025-11-05T14:57:14.864Z" }, + { url = "https://files.pythonhosted.org/packages/94/19/5981fb798bb8d08933b815b1fd9e55d179c380b9d8c21a49197b9b7c5967/google_re2-1.1.20251105-1-cp311-cp311-win32.whl", hash = "sha256:54f51762b51dc238eceddf49b56cc2b64594fe72d9328c1c39d615aa990e1f87", size = 434066, upload-time = "2025-11-05T14:57:16.22Z" }, + { url = "https://files.pythonhosted.org/packages/49/e5/f83053a36cfc4762d843748e4f7a9c1141937dcf74cd6fc3f4598292dda3/google_re2-1.1.20251105-1-cp311-cp311-win_amd64.whl", hash = "sha256:f5f856ff5036a8f22b3bad57f376d4e3b97b59b64f311bdb1f83c8dabded2492", size = 491025, upload-time = "2025-11-05T14:57:17.746Z" }, + { url = "https://files.pythonhosted.org/packages/56/be/4315c3b38f42f9a2888fa76260545c98547502f1c35aa63a672d39011b2e/google_re2-1.1.20251105-1-cp311-cp311-win_arm64.whl", hash = "sha256:913864f97de4151eaa8bb7746ca230fd193656501e07fb658ce2cd46d4f6efcc", size = 642194, upload-time = "2025-11-05T14:57:19.374Z" }, + { url = "https://files.pythonhosted.org/packages/67/20/73b487538e9107c2fd96aed737e3f3890dfce3e292622e4ffb2f9c810ee5/google_re2-1.1.20251105-1-cp312-cp312-macosx_13_0_arm64.whl", hash = "sha256:b30f09b4d63249c72e65ccae4cbf6b331b48c22fc7cb439f1d85f347b9d07ceb", size = 485591, upload-time = "2025-11-05T14:57:20.961Z" }, + { url = "https://files.pythonhosted.org/packages/b9/9a/ca3a993bdb5dc6d5b2616b9657b2872a83d1827f8bd3ab50cd629eb751c7/google_re2-1.1.20251105-1-cp312-cp312-macosx_13_0_x86_64.whl", hash = "sha256:9a77892c524b8bdf3d47d7cad1cc2ac3a0108bdd65007ef4c02888fa46baf8ee", size = 518780, upload-time = "2025-11-05T14:57:22.18Z" }, + { url = "https://files.pythonhosted.org/packages/df/37/b2e367987371514253ec9e514637f457deaacb7acc1c900814f3a6421e0f/google_re2-1.1.20251105-1-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:a3ac51b28cbf25c100dfd8849212d878d7005d1d4a7e129a10789043c56b6021", size = 486966, upload-time = "2025-11-05T14:57:24.575Z" }, + { url = "https://files.pythonhosted.org/packages/d9/69/1db6742943c0ac254bfb7d8a37a5d3f73f016a65cfa1f84fe3a0451820f6/google_re2-1.1.20251105-1-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:9f7158afc9825ac2654c6561aea94a1f7edb5b5b88e6e3639bb80bb817d102ac", size = 520225, upload-time = "2025-11-05T14:57:26.039Z" }, + { url = "https://files.pythonhosted.org/packages/f4/0a/0747c92dbebe2c09a26bd7386d372b5c5a9926236b4f3d69bb8f15db05cb/google_re2-1.1.20251105-1-cp312-cp312-macosx_15_0_arm64.whl", hash = "sha256:5320da07dc3b7ac7f407514f42ac17d67e771ac7c7562d449571185e6fb601b2", size = 482943, upload-time = "2025-11-05T14:57:27.353Z" }, + { url = "https://files.pythonhosted.org/packages/7f/14/6bfc6838bb6cb561824ac03deeab2bd11d5d9a93505f536c8fa2f6bd46c4/google_re2-1.1.20251105-1-cp312-cp312-macosx_15_0_x86_64.whl", hash = "sha256:5a4e5785bc30d52ce655d805b07ad2d8a4905429a5f690ae9c2f1caa76665709", size = 510384, upload-time = "2025-11-05T14:57:29.139Z" }, + { url = "https://files.pythonhosted.org/packages/8a/0a/6add090c917ee39f6f0be753037cafceb3bad904b424efc155fb38082635/google_re2-1.1.20251105-1-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2b7a3b90f747130310d4b3b8e19ebb845d0d97c1deb63b36f76c7242dacbd736", size = 572446, upload-time = "2025-11-05T14:57:30.495Z" }, + { url = "https://files.pythonhosted.org/packages/0d/1c/8b1ccbeade96a21435d55b5185cd6d9b2ceab5a9af998a4d9099e0540759/google_re2-1.1.20251105-1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:809c5fa5d08279413b29c2e2c5c528e85cd94a0e0fd897db595a0c09eeee2782", size = 591348, upload-time = "2025-11-05T14:57:31.808Z" }, + { url = "https://files.pythonhosted.org/packages/62/cf/7bdd7a1ae7828b613011da808eafec4da3132f43c3be6af5e0bd670ebe8b/google_re2-1.1.20251105-1-cp312-cp312-win32.whl", hash = "sha256:d8424e63a9ec0fe5bde03d97876b2431f8a746af33eb475fa1ae39144bd05b2a", size = 433787, upload-time = "2025-11-05T14:57:33.071Z" }, + { url = "https://files.pythonhosted.org/packages/31/e9/5dd951c35acaabfe87c67228b9af2cdcd7779d9167edbe6b9094b8a8e529/google_re2-1.1.20251105-1-cp312-cp312-win_amd64.whl", hash = "sha256:062313c309f93dfeb6966372f4c446580e98879133ec155522eea8aaf568a5cd", size = 491726, upload-time = "2025-11-05T14:57:34.39Z" }, + { url = "https://files.pythonhosted.org/packages/60/8d/c1afd29fc2cb475fd4c634f3d3c8099c0efb662362c10b27a9eaf11c9357/google_re2-1.1.20251105-1-cp312-cp312-win_arm64.whl", hash = "sha256:558f144b26a9555ae4e9467cc3aa3299a8ce13217f328b21ae326ca0633be19b", size = 642673, upload-time = "2025-11-05T14:57:35.693Z" }, +] + +[[package]] +name = "googleapis-common-protos" +version = "1.75.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "protobuf" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b5/c8/f439cffde755cffa462bfbb156278fa6f9d09119719af9814b858fd4f81f/googleapis_common_protos-1.75.0.tar.gz", hash = "sha256:53a062ff3c32552fbd62c11fe23768b78e4ddf0494d5e5fd97d3f4689c75fbbd", size = 151035, upload-time = "2026-05-07T08:04:49.423Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e7/c8/e2645aa8ed02fd4c7a2f59d68783b65b1f3cbdfe39a6308e156509d1fee8/googleapis_common_protos-1.75.0-py3-none-any.whl", hash = "sha256:961ed60399c457ceb0ee8f285a84c870aabc9c6a832b9d37bb281b5bebde43ed", size = 300631, upload-time = "2026-05-07T08:03:30.345Z" }, +] + +[[package]] +name = "grpcio" +version = "1.82.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/90/bc/656b89387d6f4ed7e0686c7b64c2ae7e554a759aa58122c8e5fb99392c32/grpcio-1.82.1.tar.gz", hash = "sha256:707b24abd90fcb1e45bcc080577da1dbf9971d107490589b9539af8e1e77b4b5", size = 13187300, upload-time = "2026-07-08T12:36:16.588Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/26/5b/e5092af97fa671ca279b3e373251af4bf87d5fbda7dc85f6a616899562a7/grpcio-1.82.1-cp311-cp311-linux_armv7l.whl", hash = "sha256:0ddb18a9a9e1f46692b3567ae4abb3f8d117ce6afea48650f8eca06d8ab5d06f", size = 6181472, upload-time = "2026-07-08T12:34:31.009Z" }, + { url = "https://files.pythonhosted.org/packages/c1/8f/18053a3a2ca03d0c2a1b8cc7271e705007a16aa5dae84bac00935c5b1a7f/grpcio-1.82.1-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:cf855b1af246720f567b0ce5d0724d45dfa4188eecc3296a2a69257b11b9e94b", size = 11970995, upload-time = "2026-07-08T12:34:33.603Z" }, + { url = "https://files.pythonhosted.org/packages/5b/7e/21b1acb052876ad00959ec4d1b05fe08607d650bcfa282073bb164c2703c/grpcio-1.82.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ddb30cb13e25bc13cea70ffc69d6d90c49d36ea6c1d4549e6912f70177834cac", size = 6760127, upload-time = "2026-07-08T12:34:36.122Z" }, + { url = "https://files.pythonhosted.org/packages/3e/12/25eef9c245c54f0061317d13a302357fe8ea03bac240b2b02ececcf54da4/grpcio-1.82.1-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:1e822b2774f719c017cbe700b6e47173b6ae290fb84906f52a5a3c2c60b62e1e", size = 7484377, upload-time = "2026-07-08T12:34:38.368Z" }, + { url = "https://files.pythonhosted.org/packages/a0/41/1a348767eb9d9bd7765dc4fa8a01723d3bb386d67f981ee5c6f9c02b8b1c/grpcio-1.82.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5dafb1ece8ed45dee7c738f166ec82e19673221ed5ab8967f72858a4685345b2", size = 6924269, upload-time = "2026-07-08T12:34:40.583Z" }, + { url = "https://files.pythonhosted.org/packages/e4/b9/3aae7a03d34c86ea27988db859a6087c186f6c3f53f9b551e07afd989bfa/grpcio-1.82.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:e06503106e7271e0a49fd5a1ac04747f1e47e87d900476db6fe45bc87ee411f4", size = 7531848, upload-time = "2026-07-08T12:34:43.277Z" }, + { url = "https://files.pythonhosted.org/packages/3c/2e/3c4afa625d0dac9090707966916284c035fc5b2fb3e2c51e156accee6735/grpcio-1.82.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:ff99bc8cafb6a952201c37b995f425e641c93ffa6e072258525feab57290141d", size = 8568217, upload-time = "2026-07-08T12:34:45.502Z" }, + { url = "https://files.pythonhosted.org/packages/20/9c/d8489c628e73e20a3d034e7f66912de7b1acb405f01d388f056a88e47924/grpcio-1.82.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:644ae1b94266ac785330f4590a69e52b6a7eb73029043a02209db81c81397d69", size = 7938771, upload-time = "2026-07-08T12:34:48.323Z" }, + { url = "https://files.pythonhosted.org/packages/4b/b7/0a92cfd1658f3a896d4aa12d4efeb7dd4ddfc723725ae22741a5241ea710/grpcio-1.82.1-cp311-cp311-win32.whl", hash = "sha256:e203d2e19d471630084a16c815616f8211dff21c268ab3c5f5bf38417832e074", size = 4256432, upload-time = "2026-07-08T12:34:50.432Z" }, + { url = "https://files.pythonhosted.org/packages/c7/6a/2872c761b025d9ec74386f22a4a7d59c5a5b00ebf718761b33739ffc45de/grpcio-1.82.1-cp311-cp311-win_amd64.whl", hash = "sha256:0d8299c285fe6cc6a1f56badf8d3bc5078c8d20273ee64bafa3783b4bc29a769", size = 5009633, upload-time = "2026-07-08T12:34:52.67Z" }, + { url = "https://files.pythonhosted.org/packages/dc/88/d1350bf3343a2ed87d801584e40609f6c6bd3087926eeca03de50348cf4a/grpcio-1.82.1-cp312-cp312-linux_armv7l.whl", hash = "sha256:c09bd5fa0d5b1fbd773ec349fe61441c3e4ebf168c229aa7538a820bdfad6a58", size = 6144689, upload-time = "2026-07-08T12:34:55.567Z" }, + { url = "https://files.pythonhosted.org/packages/e6/33/71875cdecd27c24ac1385d4783a09853f01b84a825a36aec2a2bc7d0d080/grpcio-1.82.1-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:1eae24810720734598e3e6a1a528d5de0f265fe3fc86575e9ecce424b9ec7379", size = 11952034, upload-time = "2026-07-08T12:34:58.128Z" }, + { url = "https://files.pythonhosted.org/packages/82/b2/d9125df3d8a140dec12cc82c05b7deafedeababcff6496f28b2fd5634d10/grpcio-1.82.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:a6bd5daf5bde7b24d7ad2cbaf8bf9eac620d96222016bb5e7ddde930dec0673f", size = 6710772, upload-time = "2026-07-08T12:35:01.33Z" }, + { url = "https://files.pythonhosted.org/packages/88/9b/69e2d1627398b964f34437dc476a5aff5a2cc8e7f247d26272b5674b5faf/grpcio-1.82.1-cp312-cp312-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:1ecfde669cb687ac020d31ff76debe5dc7a62213335f02262eb6625628da1c03", size = 7450677, upload-time = "2026-07-08T12:35:03.926Z" }, + { url = "https://files.pythonhosted.org/packages/e4/e7/8f855ca29c294956122a2a73023655b9b02602d5111dad2b9b00e7631c68/grpcio-1.82.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:011c8badee95734dee8bf05ce3464756a0ac3ebb8d443afd20c0e2b5e4640ad9", size = 6886855, upload-time = "2026-07-08T12:35:06.174Z" }, + { url = "https://files.pythonhosted.org/packages/5f/f0/fa87e85f49925f44c479d07e58b051e69bcfef6b6d5fbc6749d140f6730a/grpcio-1.82.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b85f4564926fb23114d239392bdcae200db1e6179629edd7d7ab0ab89c96a197", size = 7501323, upload-time = "2026-07-08T12:35:08.49Z" }, + { url = "https://files.pythonhosted.org/packages/67/55/2e0b10ae1d3ef9dcc480b91dc2158f4931fc4675d3af0a2836e39b2a744f/grpcio-1.82.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:2c0c8270833395644c3fe6b6a806397955a2bc0538000a19a78b90c05a6c16e0", size = 8536899, upload-time = "2026-07-08T12:35:10.975Z" }, + { url = "https://files.pythonhosted.org/packages/bd/95/a3d8b0431fa221efc51ee39d73595ede74ba82a43b7c4313192e580face2/grpcio-1.82.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2ba199205ff46c7778290fe1673c91ac8e7e45678dd5c86e9e56fa33ec8788f6", size = 7913892, upload-time = "2026-07-08T12:35:13.944Z" }, + { url = "https://files.pythonhosted.org/packages/b8/92/f2651ec704d9852a56faef394775038afba435b50ce82ab2404d119c3355/grpcio-1.82.1-cp312-cp312-win32.whl", hash = "sha256:06127691866e295c14e84a1fb86356dd962254f6abd0da4ca4b001eea9e89438", size = 4240985, upload-time = "2026-07-08T12:35:16.048Z" }, + { url = "https://files.pythonhosted.org/packages/96/4f/a5fe8bf0d0a1b24855f370293075c931f27de4eb55f0f158786095bf3c11/grpcio-1.82.1-cp312-cp312-win_amd64.whl", hash = "sha256:1fa3223a3a2e1db74f4c2b255189eb7ea875dfba56e221d252ee3fc7b204778e", size = 5001580, upload-time = "2026-07-08T12:35:18.689Z" }, +] + +[[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, 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, upload-time = "2025-04-24T03:35:24.344Z" }, +] + +[[package]] +name = "hf-xet" +version = "1.5.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4b/2d/57fd21d84d93efb4bd0b962383790e19dd1bc053501b4264c97903b4e83e/hf_xet-1.5.1.tar.gz", hash = "sha256:51ef4500dab3764b41135ee1381a4b62ce56fc54d4c92b719b59e597d6df5bf6", size = 876636, upload-time = "2026-06-08T23:02:53.897Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7a/d8/5e54cf37434759d1f4f2ba9b66077ff9d4c4e1f37b6bd7975da5c40d94ab/hf_xet-1.5.1-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:6abd35c3221eff63836618ddfb954dcf84798603f71d8e33e3ed7b04acfdbe6e", size = 4077794, upload-time = "2026-06-08T23:02:40.656Z" }, + { url = "https://files.pythonhosted.org/packages/35/94/4b2ecfbad8f8b04701a23aefb62f540b9137d058b7e1dbef16a32676f0e9/hf_xet-1.5.1-cp37-abi3-macosx_11_0_arm64.whl", hash = "sha256:94e761bbd266bf4c03cee73753916062665ce8365aa40ed321f45afcb934b41e", size = 3845354, upload-time = "2026-06-08T23:02:42.702Z" }, + { url = "https://files.pythonhosted.org/packages/de/cc/f99f4bc7295023d7bd9ebbfd51f75cc530ca262c1227666268b8208f4b77/hf_xet-1.5.1-cp37-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:892e3a3a3aecc12aded8b93cf4f9cd059282c7de0732f7d55026f3abdf474350", size = 4514864, upload-time = "2026-06-08T23:02:44.497Z" }, + { url = "https://files.pythonhosted.org/packages/cd/6e/21f7e5a2381278bd3b7b7a5a4d90038518bb6308a0c1daf5d9f8268bb178/hf_xet-1.5.1-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:a93df2039190502835b1db8cd7e178b0b7b889fe9ab51299d5ced26e0dd879a4", size = 4303784, upload-time = "2026-06-08T23:02:46.203Z" }, + { url = "https://files.pythonhosted.org/packages/35/0e/f992bb6927ac1cb30ef74e62268f551f338bc32b2191f7c96a44c6f7283e/hf_xet-1.5.1-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:0c97106032ef70467b4f6bc2d0ccc266d7613ee076afc56516c502f87ce1c4a6", size = 4500703, upload-time = "2026-06-08T23:02:47.628Z" }, + { url = "https://files.pythonhosted.org/packages/fb/d1/90a498d05447980b977b1669246eeeeae4cfb0ea3e7a286eaba627f91bf9/hf_xet-1.5.1-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:6208adb15d192b90e4c2ad2a27ed864359b2cb0f2494eb6d7c7f3699ac02e2bf", size = 4719498, upload-time = "2026-06-08T23:02:49.268Z" }, + { url = "https://files.pythonhosted.org/packages/6d/b6/20f99cfe97cc663a711f7b33cc21d4793e51968e9a26125b4afcd77315ba/hf_xet-1.5.1-cp37-abi3-win_amd64.whl", hash = "sha256:f7b3002f95d1c13e24bcb4537baa8f0eb3838957067c91bb4959bc004a6435f5", size = 4026419, upload-time = "2026-06-08T23:02:50.829Z" }, + { url = "https://files.pythonhosted.org/packages/f9/fa/77453694888f03e5a8c8852d1514a0894d8e81c622d39edbaf308ea0dcf4/hf_xet-1.5.1-cp37-abi3-win_arm64.whl", hash = "sha256:93d090b57b211133f6c0dab0205ef5cb6d89162979ba75a74845045cc3063b8e", size = 3855178, upload-time = "2026-06-08T23:02:52.452Z" }, +] + +[[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, 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, upload-time = "2025-04-24T22:06:20.566Z" }, +] + +[[package]] +name = "httptools" +version = "0.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/43/e5/d471fcb0e14523fe1c3f4ba58ca52480e7bd70ad7109a3846bc75892f7fb/httptools-0.8.0.tar.gz", hash = "sha256:6b2a32f18d97e16e90827d7a819ffa8dbd8cc245fc4e1fa9d1095b54ef4bd999", size = 271342, upload-time = "2026-05-25T22:17:48.841Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f8/d2/c3eedaef57de65c3cc5f8dc244cf12d09c84ad258a479055aad6db23206c/httptools-0.8.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:ed377e64805bdba4943c82717333f8f8603a13b09aff9cead2717c6c817fb168", size = 208428, upload-time = "2026-05-25T22:16:59.717Z" }, + { url = "https://files.pythonhosted.org/packages/f1/94/dfe435d90d0ef61ec0f2cc3d480eef78c59727c6c2ce039f433882f6131a/httptools-0.8.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9518c406d7b310f05adb1a37f80acabac40504a575d7c0da6d3e365c695ac20d", size = 113366, upload-time = "2026-05-25T22:17:00.795Z" }, + { url = "https://files.pythonhosted.org/packages/cc/d4/13025f1a56e615dcb331e0bbe2d9a1143212b58c263385fc5d2e558f5bac/httptools-0.8.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:57278e6fa0424c42a8a3e454828ab4f0aff27b40cddf9679579b98c6dce6a376", size = 464676, upload-time = "2026-05-25T22:17:02.014Z" }, + { url = "https://files.pythonhosted.org/packages/bf/95/4c1c26c0b985f8a3331682d802598f14e32dc41bf7509266eb2c04ad4801/httptools-0.8.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bbb8caadb2b742d293169d2b458b5c001ef70e3158704aa3d3ef9597624c5d1d", size = 464235, upload-time = "2026-05-25T22:17:03.109Z" }, + { url = "https://files.pythonhosted.org/packages/a2/82/6735be2b0ca527718c431cdb8e5f70c3862c0844a687df0f572c51e11497/httptools-0.8.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:52dd695b865fe96d9d2b16b64a895f3f57bf3cb064e8383cd3b5713a069e8085", size = 449809, upload-time = "2026-05-25T22:17:04.443Z" }, + { url = "https://files.pythonhosted.org/packages/b5/f9/5811c74f37a758c8a4aa3dc430375119d335947e883efc4664d8f3559a41/httptools-0.8.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:20b4aac66ff65f7db06a375808b78f42a94970aa22e826b3cb2b43eb09174124", size = 452174, upload-time = "2026-05-25T22:17:05.476Z" }, + { url = "https://files.pythonhosted.org/packages/cc/94/97b75870dea07b71e3ec535cebe525b08d723152e4c7d13fa887e51f4de2/httptools-0.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:a1b4c8e7a489a0d750d91894e9a8cdc295838f1924c0ca903ae993456fddec07", size = 90991, upload-time = "2026-05-25T22:17:06.75Z" }, + { url = "https://files.pythonhosted.org/packages/14/88/1d21a36da8f5cb0fa49eafd4b169eba5608d57e75bbcf61845cbc6243216/httptools-0.8.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:880490234c10f70a9830743097e8958d6e4b9f5a0ffc24515023afeef984054d", size = 208247, upload-time = "2026-05-25T22:17:07.843Z" }, + { url = "https://files.pythonhosted.org/packages/a5/42/cc4feea2945cb3051038f090c9b36bd5b8a9d7f5a894a506a8983e33fd1c/httptools-0.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5931891fb7b441b8a3853cf1b85c82c903defce084dd5f6771ca46e31bf862c5", size = 113064, upload-time = "2026-05-25T22:17:09.136Z" }, + { url = "https://files.pythonhosted.org/packages/e3/a6/febbb8b8db0f58b38e44ad6cb946e6a255ae49b55f2e8543408fb7501ccd/httptools-0.8.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b15fc622b0f869d19207c4089a501d9bcc63ca5e071ffdd2f03f922df882dcb2", size = 523851, upload-time = "2026-05-25T22:17:10.106Z" }, + { url = "https://files.pythonhosted.org/packages/b7/e4/f90a0df0b83beff265b7e3b65f2a4cefd95792d4be0ac3e16049f2acd3c2/httptools-0.8.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:425f83884fd6343828d8c565f046cb72b6d19063f6924093e11bcd8e1548cd09", size = 518842, upload-time = "2026-05-25T22:17:11.218Z" }, + { url = "https://files.pythonhosted.org/packages/9e/2d/0c9ac76dd2c893841fbf6498d6acec4f2442e1b7067f6e3e316a80e494e8/httptools-0.8.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ef7c3c97f4311c7be57e2986629df89d49cb434dbff78eafcd48c2bff986b15a", size = 501238, upload-time = "2026-05-25T22:17:12.728Z" }, + { url = "https://files.pythonhosted.org/packages/ca/42/906adc91ae3a5fa9c59c0a2f21c139725bd7e5b41ae6acd485cd14123ebf/httptools-0.8.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a1afd7c9fbff0d9f5d489c4ce2768bd09c84a46ddefc7161e6aa82ae35c85745", size = 509567, upload-time = "2026-05-25T22:17:13.842Z" }, + { url = "https://files.pythonhosted.org/packages/05/0b/4240efeb672751ee5b9b380cb0e3fdc050bc05f68adc7a8aefc4fcd9a69a/httptools-0.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:cd96f29b4bab1d42fa6e3d008711c75e0f79e94e06827330160e3a304227f150", size = 90918, upload-time = "2026-05-25T22:17:15.155Z" }, +] + +[[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, 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, 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, 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, upload-time = "2025-10-10T21:48:21.158Z" }, +] + +[[package]] +name = "huggingface-hub" +version = "1.23.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "filelock" }, + { name = "fsspec" }, + { name = "hf-xet", marker = "platform_machine == 'AMD64' or platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'arm64' or platform_machine == 'x86_64'" }, + { name = "httpx" }, + { name = "packaging" }, + { name = "pyyaml" }, + { name = "tqdm" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1a/8f/999e4dda11c6187c78f090eac00895a47e11a0049308f07579bcb7aa3aa2/huggingface_hub-1.23.0.tar.gz", hash = "sha256:c04997fb8bbdace1e57b7703d30ed7678af51f70d00d241819ff411b92ae9a88", size = 919163, upload-time = "2026-07-09T14:49:32.315Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f1/ce/13b2ba57838b8db1e6bd033c1b21ce0b9f6153b87d4e4939f77074e41eb0/huggingface_hub-1.23.0-py3-none-any.whl", hash = "sha256:b1d604788f5adc7f0eb246e03e0ec19011ca06e38400218c347dccc3dffa64a2", size = 770336, upload-time = "2026-07-09T14:49:30.597Z" }, +] + +[[package]] +name = "idna" +version = "3.18" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cd/63/9496c57188a2ee585e0f1db071d75089a11e98aa86eb99d9d7618fc1edce/idna-3.18.tar.gz", hash = "sha256:ffb385a7e039654cef1ab9ef32c6fafe283c0c0467bba1d9029738ce4a14a848", size = 196711, upload-time = "2026-06-02T14:34:07.794Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/5e/d4e9f1a599fb8e573b7b87160658329fbf28d19eac2718f51fc3def3aa5a/idna-3.18-py3-none-any.whl", hash = "sha256:7f952cbe720b688055e3f87de14f5c3e5fdaa8bc3928985c4077ca689de849a2", size = 65455, upload-time = "2026-06-02T14:34:06.319Z" }, +] + +[[package]] +name = "importlib-metadata" +version = "8.9.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "zipp" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e7/72/c600ae4f68c28fc19f9c31b9403053e5dbb8cace2e6842c7b7c3e4d42fe9/importlib_metadata-8.9.0.tar.gz", hash = "sha256:58850626cef4bd2df100378b0f2aea9724a7b92f10770d547725b047078f99ee", size = 56140, upload-time = "2026-03-20T16:56:26.362Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7d/f9/97f2ca8bb3ec6e4b1d64f983ebe98b9a192faddff67fac3d6303a537e670/importlib_metadata-8.9.0-py3-none-any.whl", hash = "sha256:e0f761b6ea91ced3b0844c14c9d955224d538105921f8e6754c00f6ca79fba7f", size = 27220, upload-time = "2026-03-20T16:56:25.07Z" }, +] + +[[package]] +name = "importlib-resources" +version = "7.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e4/06/b56dfa750b44e86157093bc8fca0ab81dccbf5260510de4eaf1cb69b5b99/importlib_resources-7.1.0.tar.gz", hash = "sha256:0722d4c6212489c530f2a145a34c0a7a3b4721bc96a15fada5930e2a0b760708", size = 44985, upload-time = "2026-04-12T16:36:09.232Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8a/db/55a262f3606bebcae07cc14095338471ad7c0bbcaa37707e6f0ee49725b7/importlib_resources-7.1.0-py3-none-any.whl", hash = "sha256:1bd7b48b4088eddb2cd16382150bb515af0bd2c70128194392725f82ad2c96a1", size = 37232, upload-time = "2026-04-12T16:36:08.219Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, +] + +[[package]] +name = "instructor" +version = "1.15.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiohttp" }, + { name = "docstring-parser" }, + { name = "jinja2" }, + { name = "jiter" }, + { name = "openai" }, + { name = "pydantic" }, + { name = "pydantic-core" }, + { name = "requests" }, + { name = "rich" }, + { name = "tenacity" }, + { name = "typer" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9f/24/f6b28e83b3194c6223ed7c6eed5724687f6ecd378ec2ff24044f0cbf1f09/instructor-1.15.4.tar.gz", hash = "sha256:ea2280c3678d0f6891c4d826104f95624b680e69877113a6345b1d7c9027ba0f", size = 70049678, upload-time = "2026-06-28T07:36:43.458Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/80/8d/f668a30fff4d25b36533355e23aeb0b5724df4628eb974124ed64b7bcf8d/instructor-1.15.4-py3-none-any.whl", hash = "sha256:00e0ecda80fd9746fb6d082d3f9641e193adb1d8849f0775f91519a82aeff968", size = 252522, upload-time = "2026-06-28T07:36:36.863Z" }, +] + +[[package]] +name = "jinja2" +version = "3.1.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, +] + +[[package]] +name = "jiter" +version = "0.14.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6e/c1/0cddc6eb17d4c53a99840953f95dd3accdc5cfc7a337b0e9b26476276be9/jiter-0.14.0.tar.gz", hash = "sha256:e8a39e66dac7153cf3f964a12aad515afa8d74938ec5cc0018adcdae5367c79e", size = 165725, upload-time = "2026-04-10T14:28:42.01Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8a/1f/198ae537fccb7080a0ed655eb56abf64a92f79489dfbf79f40fa34225bcd/jiter-0.14.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:7e791e247b8044512e070bd1f3633dc08350d32776d2d6e7473309d0edf256a2", size = 316896, upload-time = "2026-04-10T14:26:01.986Z" }, + { url = "https://files.pythonhosted.org/packages/cf/34/da67cff3fce964a36d03c3e365fb0f8726ade2a6cfd4d3c70107e216ead6/jiter-0.14.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:71527ce13fd5a0c4e40ad37331f8c547177dbb2dd0a93e5278b6a5eecf748804", size = 321085, upload-time = "2026-04-10T14:26:03.364Z" }, + { url = "https://files.pythonhosted.org/packages/ed/36/4c72e67180d4e71a4f5dcf7886d0840e83c49ab11788172177a77570326e/jiter-0.14.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:02c4a7ab56f746014874f2c525584c0daca1dec37f66fd707ecef3b7e5c2228c", size = 347393, upload-time = "2026-04-10T14:26:05.314Z" }, + { url = "https://files.pythonhosted.org/packages/bc/db/9b39e09ceafa9878235c0fc29e3e3f9b12a4c6a98ea3085b998cadf3accc/jiter-0.14.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:376e9dafff914253bb9d46cdc5f7965607fbe7feb0a491c34e35f92b2770702e", size = 372937, upload-time = "2026-04-10T14:26:06.884Z" }, + { url = "https://files.pythonhosted.org/packages/b0/96/0dcba1d7a82c1b720774b48ef239376addbaf30df24c34742ac4a57b67b2/jiter-0.14.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:23ad2a7a9da1935575c820428dd8d2490ce4d23189691ce33da1fc0a58e14e1c", size = 463646, upload-time = "2026-04-10T14:26:08.345Z" }, + { url = "https://files.pythonhosted.org/packages/f1/e3/f61b71543e746e6b8b805e7755814fc242715c16f1dba58e1cbccb8032c2/jiter-0.14.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:54b3ddf5786bc7732d293bba3411ac637ecfa200a39983166d1df86a59a43c9f", size = 380225, upload-time = "2026-04-10T14:26:10.161Z" }, + { url = "https://files.pythonhosted.org/packages/ad/5e/0ddeb7096aca099114abe36c4921016e8d251e6f35f5890240b31f1f60ae/jiter-0.14.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5c001d5a646c2a50dc055dd526dad5d5245969e8234d2b1131d0451e81f3a373", size = 358682, upload-time = "2026-04-10T14:26:11.574Z" }, + { url = "https://files.pythonhosted.org/packages/e9/d1/fe0c46cd7fda9cad8f1ff9ad217dc61f1e4280b21052ec6dfe88c1446ef2/jiter-0.14.0-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:834bb5bdabca2e91592a03d373838a8d0a1b8bbde7077ae6913fd2fc51812d00", size = 359973, upload-time = "2026-04-10T14:26:13.316Z" }, + { url = "https://files.pythonhosted.org/packages/ac/21/f5317f91729b501019184771c80d60abd89907009e7bfa6c7e348c5bdd44/jiter-0.14.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4e9178be60e229b1b2b0710f61b9e24d1f4f8556985a83ff4c4f95920eea7314", size = 397568, upload-time = "2026-04-10T14:26:15.212Z" }, + { url = "https://files.pythonhosted.org/packages/e9/05/79d8f33fb2bf168db0df5c9cd16fe440a8ada57e929d3677b22712c2568f/jiter-0.14.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a7e4ccff04ec03614e62c613e976a3a5860dc9714ce8266f44328bdc8b1cab2c", size = 522535, upload-time = "2026-04-10T14:26:16.956Z" }, + { url = "https://files.pythonhosted.org/packages/5c/00/d1e3ff3d2a465e67f08507d74bafb2dcd29eba91dc939820e39e8dea38b8/jiter-0.14.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:69539d936fb5d55caf6ecd33e2e884de083ff0ea28579780d56c4403094bb8d9", size = 556709, upload-time = "2026-04-10T14:26:18.5Z" }, + { url = "https://files.pythonhosted.org/packages/60/5b/bbb2189f62ace8d95e869aa4c84c9946616f301e2d02895a6f20dcc3bba3/jiter-0.14.0-cp311-cp311-win32.whl", hash = "sha256:4927d09b3e572787cc5e0a5318601448e1ab9391bcef95677f5840c2d00eaa6d", size = 208660, upload-time = "2026-04-10T14:26:20.511Z" }, + { url = "https://files.pythonhosted.org/packages/b8/86/c500b53dcbf08575f5963e536ebd757a1f7c568272ba5d180b212c9a87fb/jiter-0.14.0-cp311-cp311-win_amd64.whl", hash = "sha256:42d6ed359ac49eb922fdd565f209c57340aa06d589c84c8413e42a0f9ae1b842", size = 204659, upload-time = "2026-04-10T14:26:22.152Z" }, + { url = "https://files.pythonhosted.org/packages/75/4a/a676249049d42cb29bef82233e4fe0524d414cbe3606c7a4b311193c2f77/jiter-0.14.0-cp311-cp311-win_arm64.whl", hash = "sha256:6dd689f5f4a5a33747b28686e051095beb214fe28cfda5e9fe58a295a788f593", size = 194772, upload-time = "2026-04-10T14:26:23.458Z" }, + { url = "https://files.pythonhosted.org/packages/5a/68/7390a418f10897da93b158f2d5a8bd0bcd73a0f9ec3bb36917085bb759ef/jiter-0.14.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:2fb2ce3a7bc331256dfb14cefc34832366bb28a9aca81deaf43bbf2a5659e607", size = 316295, upload-time = "2026-04-10T14:26:24.887Z" }, + { url = "https://files.pythonhosted.org/packages/60/a0/5854ac00ff63551c52c6c89534ec6aba4b93474e7924d64e860b1c94165b/jiter-0.14.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5252a7ca23785cef5d02d4ece6077a1b556a410c591b379f82091c3001e14844", size = 315898, upload-time = "2026-04-10T14:26:26.601Z" }, + { url = "https://files.pythonhosted.org/packages/41/a1/4f44832650a16b18e8391f1bf1d6ca4909bc738351826bcc198bba4357f4/jiter-0.14.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c409578cbd77c338975670ada777add4efd53379667edf0aceea730cabede6fb", size = 343730, upload-time = "2026-04-10T14:26:28.326Z" }, + { url = "https://files.pythonhosted.org/packages/48/64/a329e9d469f86307203594b1707e11ae51c3348d03bfd514a5f997870012/jiter-0.14.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7ede4331a1899d604463369c730dbb961ffdc5312bc7f16c41c2896415b1304a", size = 370102, upload-time = "2026-04-10T14:26:30.089Z" }, + { url = "https://files.pythonhosted.org/packages/94/c1/5e3dfc59635aa4d4c7bd20a820ac1d09b8ed851568356802cf1c08edb3cf/jiter-0.14.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:92cd8b6025981a041f5310430310b55b25ca593972c16407af8837d3d7d2ca01", size = 461335, upload-time = "2026-04-10T14:26:31.911Z" }, + { url = "https://files.pythonhosted.org/packages/e3/1b/dd157009dbc058f7b00108f545ccb72a2d56461395c4fc7b9cfdccb00af4/jiter-0.14.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:351bf6eda4e3a7ceb876377840c702e9a3e4ecc4624dbfb2d6463c67ae52637d", size = 378536, upload-time = "2026-04-10T14:26:33.595Z" }, + { url = "https://files.pythonhosted.org/packages/91/78/256013667b7c10b8834f8e6e54cd3e562d4c6e34227a1596addccc05e38c/jiter-0.14.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c1dcfbeb93d9ecd9ca128bbf8910120367777973fa193fb9a39c31237d8df165", size = 353859, upload-time = "2026-04-10T14:26:35.098Z" }, + { url = "https://files.pythonhosted.org/packages/de/d9/137d65ade9093a409fe80955ce60b12bb753722c986467aeda47faf450ad/jiter-0.14.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:ae039aaef8de3f8157ecc1fdd4d85043ac4f57538c245a0afaecb8321ec951c3", size = 357626, upload-time = "2026-04-10T14:26:36.685Z" }, + { url = "https://files.pythonhosted.org/packages/2e/48/76750835b87029342727c1a268bea8878ab988caf81ee4e7b880900eeb5a/jiter-0.14.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7d9d51eb96c82a9652933bd769fe6de66877d6eb2b2440e281f2938c51b5643e", size = 393172, upload-time = "2026-04-10T14:26:38.097Z" }, + { url = "https://files.pythonhosted.org/packages/a6/60/456c4e81d5c8045279aefe60e9e483be08793828800a4e64add8fdde7f2a/jiter-0.14.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d824ca4148b705970bf4e120924a212fdfca9859a73e42bd7889a63a4ea6bb98", size = 520300, upload-time = "2026-04-10T14:26:39.532Z" }, + { url = "https://files.pythonhosted.org/packages/a8/9f/2020e0984c235f678dced38fe4eec3058cf528e6af36ebf969b410305941/jiter-0.14.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:ff3a6465b3a0f54b1a430f45c3c0ba7d61ceb45cbc3e33f9e1a7f638d690baf3", size = 553059, upload-time = "2026-04-10T14:26:40.991Z" }, + { url = "https://files.pythonhosted.org/packages/ef/32/e2d298e1a22a4bbe6062136d1c7192db7dba003a6975e51d9a9eecabc4c2/jiter-0.14.0-cp312-cp312-win32.whl", hash = "sha256:5dec7c0a3e98d2a3f8a2e67382d0d7c3ac60c69103a4b271da889b4e8bb1e129", size = 206030, upload-time = "2026-04-10T14:26:42.517Z" }, + { url = "https://files.pythonhosted.org/packages/36/ac/96369141b3d8a4a8e4590e983085efe1c436f35c0cda940dd76d942e3e40/jiter-0.14.0-cp312-cp312-win_amd64.whl", hash = "sha256:fc7e37b4b8bc7e80a63ad6cfa5fc11fab27dbfea4cc4ae644b1ab3f273dc348f", size = 201603, upload-time = "2026-04-10T14:26:44.328Z" }, + { url = "https://files.pythonhosted.org/packages/01/c3/75d847f264647017d7e3052bbcc8b1e24b95fa139c320c5f5066fa7a0bdd/jiter-0.14.0-cp312-cp312-win_arm64.whl", hash = "sha256:ee4a72f12847ef29b072aee9ad5474041ab2924106bdca9fcf5d7d965853e057", size = 191525, upload-time = "2026-04-10T14:26:46Z" }, + { url = "https://files.pythonhosted.org/packages/32/a1/ef34ca2cab2962598591636a1804b93645821201cc0095d4a93a9a329c9d/jiter-0.14.0-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:a25ffa2dbbdf8721855612f6dca15c108224b12d0c4024d0ac3d7902132b4211", size = 311366, upload-time = "2026-04-10T14:28:27.943Z" }, + { url = "https://files.pythonhosted.org/packages/60/bb/520576a532a6b8a6f42747afed289c8448c879a34d7802fe2c832d4fd38f/jiter-0.14.0-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:0ac9cbaa86c10996b92bd12c91659b60f939f8e28fcfa6bc11a0e90a774ce95b", size = 309873, upload-time = "2026-04-10T14:28:29.688Z" }, + { url = "https://files.pythonhosted.org/packages/b2/7c/c16db114ea1f2f532f198aa8dc39585026af45af362c69a0492f31bc4821/jiter-0.14.0-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:844e73b6c56b505e9e169234ea3bdea2ea43f769f847f47ac559ba1d2361ebea", size = 344816, upload-time = "2026-04-10T14:28:31.348Z" }, + { url = "https://files.pythonhosted.org/packages/99/8f/15e7741ff19e9bcd4d753f7ff22f988fd54592f134ca13701c13ea8c20e0/jiter-0.14.0-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e52c076f187405fc21523c746c04399c9af8ece566077ed147b2126f2bcba577", size = 351445, upload-time = "2026-04-10T14:28:33.093Z" }, + { url = "https://files.pythonhosted.org/packages/21/42/9042c3f3019de4adcb8c16591c325ec7255beea9fcd33a42a43f3b0b1000/jiter-0.14.0-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:fbd9e482663ca9d005d051330e4d2d8150bb208a209409c10f7e7dfdf7c49da9", size = 308810, upload-time = "2026-04-10T14:28:34.673Z" }, + { url = "https://files.pythonhosted.org/packages/60/cf/a7e19b308bd86bb04776803b1f01a5f9a287a4c55205f4708827ee487fbf/jiter-0.14.0-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:33a20d838b91ef376b3a56896d5b04e725c7df5bc4864cc6569cf046a8d73b6d", size = 308443, upload-time = "2026-04-10T14:28:36.658Z" }, + { url = "https://files.pythonhosted.org/packages/ca/44/e26ede3f0caeff93f222559cb0cc4ca68579f07d009d7b6010c5b586f9b1/jiter-0.14.0-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:432c4db5255d86a259efde91e55cb4c8d18c0521d844c9e2e7efcce3899fb016", size = 343039, upload-time = "2026-04-10T14:28:38.356Z" }, + { url = "https://files.pythonhosted.org/packages/da/e9/1f9ada30cef7b05e74bb06f52127e7a724976c225f46adb65c37b1dadfb6/jiter-0.14.0-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:67f00d94b281174144d6532a04b66a12cb866cbdc47c3af3bfe2973677f9861a", size = 349613, upload-time = "2026-04-10T14:28:40.066Z" }, +] + +[[package]] +name = "jmespath" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d3/59/322338183ecda247fb5d1763a6cbe46eff7222eaeebafd9fa65d4bf5cb11/jmespath-1.1.0.tar.gz", hash = "sha256:472c87d80f36026ae83c6ddd0f1d05d4e510134ed462851fd5f754c8c3cbb88d", size = 27377, upload-time = "2026-01-22T16:35:26.279Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/14/2f/967ba146e6d58cf6a652da73885f52fc68001525b4197effc174321d70b4/jmespath-1.1.0-py3-none-any.whl", hash = "sha256:a5663118de4908c91729bea0acadca56526eb2698e83de10cd116ae0f4e97c64", size = 20419, upload-time = "2026-01-22T16:35:24.919Z" }, +] + +[[package]] +name = "json-repair" +version = "0.60.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5e/a6/d69888cb4ffde30e80db1e6c32caaadd2f984a80067d5ea72c2cb3f61c3f/json_repair-0.60.1.tar.gz", hash = "sha256:841661cdd2df507c9a4e189097f38ca6bc372e06d4b4e36d72e590f68176c290", size = 49451, upload-time = "2026-06-03T17:28:44.451Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/32/1f/2a2b5eea8ef5762a86ad3f8fddddaaba2c0d76dd44e644b9158900868bec/json_repair-0.60.1-py3-none-any.whl", hash = "sha256:ba6ff974f2a8bef2f7768144a7f03f870a816443f03da27a49cdd0ec31a78049", size = 48045, upload-time = "2026-06-03T17:28:43.038Z" }, +] + +[[package]] +name = "json-rpc" +version = "1.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6d/9e/59f4a5b7855ced7346ebf40a2e9a8942863f644378d956f68bcef2c88b90/json-rpc-1.15.0.tar.gz", hash = "sha256:e6441d56c1dcd54241c937d0a2dcd193bdf0bdc539b5316524713f554b7f85b9", size = 28854, upload-time = "2023-06-11T09:45:49.078Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/94/9e/820c4b086ad01ba7d77369fb8b11470a01fac9b4977f02e18659cf378b6b/json_rpc-1.15.0-py2.py3-none-any.whl", hash = "sha256:4a4668bbbe7116feb4abbd0f54e64a4adcf4b8f648f19ffa0848ad0f6606a9bf", size = 39450, upload-time = "2023-06-11T09:45:47.136Z" }, +] + +[[package]] +name = "json5" +version = "0.10.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/85/3d/bbe62f3d0c05a689c711cff57b2e3ac3d3e526380adb7c781989f075115c/json5-0.10.0.tar.gz", hash = "sha256:e66941c8f0a02026943c52c2eb34ebeb2a6f819a0be05920a6f5243cd30fd559", size = 48202, upload-time = "2024-11-26T19:56:37.823Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/aa/42/797895b952b682c3dafe23b1834507ee7f02f4d6299b65aaa61425763278/json5-0.10.0-py3-none-any.whl", hash = "sha256:19b23410220a7271e8377f81ba8aacba2fdd56947fbb137ee5977cbe1f5e8dfa", size = 34049, upload-time = "2024-11-26T19:56:36.649Z" }, +] + +[[package]] +name = "jsonref" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/aa/0d/c1f3277e90ccdb50d33ed5ba1ec5b3f0a242ed8c1b1a85d3afeb68464dca/jsonref-1.1.0.tar.gz", hash = "sha256:32fe8e1d85af0fdefbebce950af85590b22b60f9e95443176adbde4e1ecea552", size = 8814, upload-time = "2023-01-16T16:10:04.455Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0c/ec/e1db9922bceb168197a558a2b8c03a7963f1afe93517ddd3cf99f202f996/jsonref-1.1.0-py3-none-any.whl", hash = "sha256:590dc7773df6c21cbf948b5dac07a72a251db28b0238ceecce0a2abfa8ec30a9", size = 9425, upload-time = "2023-01-16T16:10:02.255Z" }, +] + +[[package]] +name = "jsonschema" +version = "4.26.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "jsonschema-specifications" }, + { name = "referencing" }, + { name = "rpds-py" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b3/fc/e067678238fa451312d4c62bf6e6cf5ec56375422aee02f9cb5f909b3047/jsonschema-4.26.0.tar.gz", hash = "sha256:0c26707e2efad8aa1bfc5b7ce170f3fccc2e4918ff85989ba9ffa9facb2be326", size = 366583, upload-time = "2026-01-07T13:41:07.246Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/69/90/f63fb5873511e014207a475e2bb4e8b2e570d655b00ac19a9a0ca0a385ee/jsonschema-4.26.0-py3-none-any.whl", hash = "sha256:d489f15263b8d200f8387e64b4c3a75f06629559fb73deb8fdfb525f2dab50ce", size = 90630, upload-time = "2026-01-07T13:41:05.306Z" }, +] + +[[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, 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, upload-time = "2025-09-08T01:34:57.871Z" }, +] + +[[package]] +name = "kubernetes" +version = "36.0.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiohttp" }, + { name = "certifi" }, + { name = "durationpy" }, + { name = "python-dateutil" }, + { name = "pyyaml" }, + { name = "requests" }, + { name = "requests-oauthlib" }, + { name = "six" }, + { name = "urllib3" }, + { name = "websocket-client" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2f/57/8b538af5076bc3372949d76f70ba3449bdfe52f9e6488170fa5d4f7cbe70/kubernetes-36.0.2.tar.gz", hash = "sha256:03551fcb49cae1f708f63624041e37403545b7aaed10cbf54e2b01a37a5438e3", size = 2336738, upload-time = "2026-06-01T18:20:30.785Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/46/2c/5c160dbdef7123f8cc97fd8ece7e0198627a426a2a49614845e9086feb8d/kubernetes-36.0.2-py2.py3-none-any.whl", hash = "sha256:faf9b5241b58de0c4a5069f2a0ffc8ac06fece7215156cd3d3ba081a78a858b6", size = 4617568, upload-time = "2026-06-01T18:20:28.737Z" }, +] + +[[package]] +name = "lance-namespace" +version = "0.9.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "lance-namespace-urllib3-client" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/00/81/4cf8d0412e1f37b2bfa70d0aeb9c7ae4ab73607534e44d60b55efb485306/lance_namespace-0.9.0.tar.gz", hash = "sha256:f738b641cc615b17323baa4eb47900f184688739ee3d2ea9fe39396b9588e53d", size = 11637, upload-time = "2026-07-01T07:42:41.78Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e1/fe/f38747c9610ade83dd9a99a0470b9432b6f21ce4e2bb5524edbe66f626fd/lance_namespace-0.9.0-py3-none-any.whl", hash = "sha256:f785ff10927e4ce0db69986576670fedd37f8a33521e8a4630c6be22db8061b2", size = 13501, upload-time = "2026-07-01T07:42:39.372Z" }, +] + +[[package]] +name = "lance-namespace-urllib3-client" +version = "0.9.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic" }, + { name = "python-dateutil" }, + { name = "typing-extensions" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d3/c3/32d0e2618549ace857c80a457e5915ef3e1145661baff876c8a5ec27be5b/lance_namespace_urllib3_client-0.9.0.tar.gz", hash = "sha256:cf796fa5307fa4dde91fe4bec2af28b90ba79191852d4394e8fe44276538e40f", size = 235805, upload-time = "2026-07-01T07:42:42.563Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cc/ab/c8754da0a1efc817f8480100cfe12b7e04034df834759de8ecf02beff3cc/lance_namespace_urllib3_client-0.9.0-py3-none-any.whl", hash = "sha256:be819c8cffb1e460a3a504dbf52d1ca009560a48e7202b8c4279998e4adf9fe4", size = 405586, upload-time = "2026-07-01T07:42:40.503Z" }, +] + +[[package]] +name = "lancedb" +version = "0.30.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "deprecation" }, + { name = "lance-namespace" }, + { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, + { name = "numpy", version = "2.5.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, + { name = "overrides", marker = "python_full_version < '3.12'" }, + { name = "packaging" }, + { name = "pyarrow" }, + { name = "pydantic" }, + { name = "tqdm" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/13/2f/1577778ad57dba0c55dc13d87230583e14541c82562483ecf8bb2f8e8a00/lancedb-0.30.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:be2a9a43a65c330ccfd08115afb26106cd8d16788522fe7693d3a1f4e01ad321", size = 41959907, upload-time = "2026-03-16T23:03:04.551Z" }, + { url = "https://files.pythonhosted.org/packages/f1/ca/8c2a04ce499a2a97d1a0de2b7e84fa8166f988a9a495e1ada860110489c2/lancedb-0.30.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:be6a4ba2a1799a426cbf2ba5ea2559a7389a569e9a31f2409d531ceb59d42f35", size = 43873070, upload-time = "2026-03-16T23:11:01.352Z" }, + { url = "https://files.pythonhosted.org/packages/16/68/e01bf7837454a5ce9e2f6773905e07b09a949bc88136c0773c8166ed7729/lancedb-0.30.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0a967ec05f9930770aeb077bc5579769b1bedf559fcd03a592d9644084625918", size = 46891197, upload-time = "2026-03-16T23:14:39.18Z" }, + { url = "https://files.pythonhosted.org/packages/43/d1/9085ad17abd98f3a180d7860df3190b2d76f99f533c76d7c7494cec4139d/lancedb-0.30.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:05c66f40f7d4f6f24208e786c40f84b87b1b8e55505305849dd3fed3b78431a3", size = 43877660, upload-time = "2026-03-16T23:11:00.837Z" }, + { url = "https://files.pythonhosted.org/packages/ea/69/504ee25c57c3f23c80276b5b7b5e4c0f98a5197a7e9e51d3c50500d2b53a/lancedb-0.30.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:bdcd27d98554ed11b6f345b14d1307b0e2332d5654767e9ee2e23d9b2d6513d1", size = 46932144, upload-time = "2026-03-16T23:15:00.474Z" }, + { url = "https://files.pythonhosted.org/packages/2c/85/d5550f22023e672af1945394f7a06a578fcab2980ecc6666acef3428a771/lancedb-0.30.0-cp39-abi3-win_amd64.whl", hash = "sha256:4751ff0446b90be4d4dccfe05f6c105f403a05f3b8531ab99eedc1c656aca950", size = 51121310, upload-time = "2026-03-16T23:43:23.89Z" }, +] + +[[package]] +name = "lark" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/da/34/28fff3ab31ccff1fd4f6c7c7b0ceb2b6968d8ea4950663eadcb5720591a0/lark-1.3.1.tar.gz", hash = "sha256:b426a7a6d6d53189d318f2b6236ab5d6429eaf09259f1ca33eb716eed10d2905", size = 382732, upload-time = "2025-10-27T18:25:56.653Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/82/3d/14ce75ef66813643812f3093ab17e46d3a206942ce7376d31ec2d36229e7/lark-1.3.1-py3-none-any.whl", hash = "sha256:c629b661023a014c37da873b4ff58a817398d12635d3bbb2c5a03be7fe5d1e12", size = 113151, upload-time = "2025-10-27T18:25:54.882Z" }, +] + +[[package]] +name = "linkify-it-py" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "uc-micro-py" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2e/c9/06ea13676ef354f0af6169587ae292d3e2406e212876a413bf9eece4eb23/linkify_it_py-2.1.0.tar.gz", hash = "sha256:43360231720999c10e9328dc3691160e27a718e280673d444c38d7d3aaa3b98b", size = 29158, upload-time = "2026-03-01T07:48:47.683Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b4/de/88b3be5c31b22333b3ca2f6ff1de4e863d8fe45aaea7485f591970ec1d3e/linkify_it_py-2.1.0-py3-none-any.whl", hash = "sha256:0d252c1594ecba2ecedc444053db5d3a9b7ec1b0dd929c8f1d74dce89f86c05e", size = 19878, upload-time = "2026-03-01T07:48:46.098Z" }, +] + +[[package]] +name = "litellm" +version = "1.92.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiohttp" }, + { name = "click" }, + { name = "fastuuid" }, + { name = "httpx" }, + { name = "importlib-metadata" }, + { name = "jinja2" }, + { name = "jsonschema" }, + { name = "openai" }, + { name = "pydantic" }, + { name = "python-dotenv" }, + { name = "tiktoken" }, + { name = "tokenizers" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/46/ea/5522be7e0dbcaa7c3fddfd2834f387c6e8eab47c0cc65f2a74657c815af7/litellm-1.92.0.tar.gz", hash = "sha256:773adf5503ee1793289689c899394a83df8122993760d9acd782e32aa798db9d", size = 15098143, upload-time = "2026-07-12T01:15:59.828Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ed/40/401dfb16c88f22fcb285eafc074cb995047feb24b98bcf47e9552207837c/litellm-1.92.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:f0ec8327aacc7914cc16310a1a3cc14340f1140b0a761403c5a4a98b01684373", size = 19292358, upload-time = "2026-07-12T01:15:43.628Z" }, + { url = "https://files.pythonhosted.org/packages/51/29/f4d671762611a88ff7818e891094984202c7502e2984eed0e440a11332bd/litellm-1.92.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:b4b65395c5d7b15fa24e08a13871c0617f6d156c46cee0eb920f3a5954e50adf", size = 19290890, upload-time = "2026-07-12T01:15:46.237Z" }, + { url = "https://files.pythonhosted.org/packages/cf/26/8061907b67aa04b7cdf2a41cbc4f8aebfbc722c909a7e4b2aea25def7053/litellm-1.92.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:9c7b2849591a35f7039dc7386a81a8e68fccb5ac2fecaa5122192c1172556b29", size = 19291180, upload-time = "2026-07-12T01:15:48.728Z" }, + { url = "https://files.pythonhosted.org/packages/ba/6c/dc9b0b580ee8af9117abd729d1de25d74fae487a0029ca77049a09f3ad33/litellm-1.92.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:f17499155366afd1eaaeb6fd0c7b55cbd2239dcd72f2fe1f8071a969cc402fae", size = 19289559, upload-time = "2026-07-12T01:15:51.376Z" }, +] + +[[package]] +name = "lxml" +version = "6.1.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/05/3b/aab6728cae887456f409b4d75e8a01856e4f04bd510de38052a47768b680/lxml-6.1.1.tar.gz", hash = "sha256:ba96ae44888e0185281e937633a743ea90d5a196c6000f82565ebb0580012d40", size = 4197430, upload-time = "2026-05-18T19:19:06.424Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/b0/83f481780d1548750b8ce2ec824073deef2f452d9cd1a6faff8507e3d16d/lxml-6.1.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:53b7d2b7a10b1c35c0a5e21e9224accf60c1bbfba523990732e521b2b73adef2", size = 8526461, upload-time = "2026-05-18T19:17:25.862Z" }, + { url = "https://files.pythonhosted.org/packages/b9/d5/30fa0f808002c7329397bfbb24e306789c0b29f04aa5842c07b174b4216f/lxml-6.1.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ff3f333630ab480244a1bff72043e511a91eb22e7595dead8653ee5612dd8f3d", size = 4595375, upload-time = "2026-05-18T19:17:34.555Z" }, + { url = "https://files.pythonhosted.org/packages/4f/d2/edb71cf0e561581a7c5eb2626244320eb04e9f8ce6d563184fd668b45073/lxml-6.1.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:a4bbea04c97f6d78a48e3fbc1cb9116d2780b1b39e03a23f6eb9b603fd61f510", size = 4923654, upload-time = "2026-05-18T19:17:42.917Z" }, + { url = "https://files.pythonhosted.org/packages/4c/77/1bc7eeb0de4577d783fb625aa092cc9357883bba35845a3666bf1259f3dc/lxml-6.1.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:db1d75f6617a49c1c01bc7023713e0ff59ab32c9579ae62a7674c0e34f3b0b0a", size = 5067921, upload-time = "2026-05-18T19:17:49.175Z" }, + { url = "https://files.pythonhosted.org/packages/1b/3c/c0690d74bd2bc17bc03b5b0d093569ead597dd0bfa088bf99eef8c24e19c/lxml-6.1.1-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a12689be69a28ddaa0ab99a5a1137da2afd5f8f16df7b5680b66f616d3eda1d", size = 5002456, upload-time = "2026-05-18T19:17:59.715Z" }, + { url = "https://files.pythonhosted.org/packages/66/8d/d1b3271af0c0f1e27e8472a849e4d2c65bc7766884b9ad2da9e76e145c88/lxml-6.1.1-cp311-cp311-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:18b73c339ae29b90fd2d06e58ebd555a751bde9cd6bbd36cc0281b9a2c94e9d8", size = 5202776, upload-time = "2026-05-18T19:18:08.924Z" }, + { url = "https://files.pythonhosted.org/packages/7a/45/689824ffb237fd10125ad273f32b28ff04dc6203c2822c85ff65a93df65e/lxml-6.1.1-cp311-cp311-manylinux_2_28_i686.whl", hash = "sha256:752d3bbfe874715ccd0aec7f88d7fc623c0f1fd7aa7b3238a084e017bad2a009", size = 5329945, upload-time = "2026-05-18T19:18:13.673Z" }, + { url = "https://files.pythonhosted.org/packages/5d/c0/ef73af53767e958fd87d437c170f272e2f6e6c0f854939f133a895f1e711/lxml-6.1.1-cp311-cp311-manylinux_2_31_armv7l.whl", hash = "sha256:6b1761fbf9ec984e2e9d9c589ef5f5fd684b7c19f92aadd567a26c5224958db6", size = 4659237, upload-time = "2026-05-18T19:18:18.657Z" }, + { url = "https://files.pythonhosted.org/packages/a0/5e/e1158e40397585e91cb0472374a1f63d0926a1ddeaa92f13d1a1ffe306d5/lxml-6.1.1-cp311-cp311-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d680fbcb768404c601ecb43519ecd8461f6954cb11c06a78962f666832ccfca8", size = 5265904, upload-time = "2026-05-18T19:18:24.883Z" }, + { url = "https://files.pythonhosted.org/packages/a0/16/8687e5d1400ed1c0bc41dace232ebb7553952b618ea1f2e5fb6e2cfbbe23/lxml-6.1.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:162af1091cd785f2f27e62d3547ae9bc58ec5c86dd314d67021fd02463708d83", size = 5045225, upload-time = "2026-05-18T19:17:20.073Z" }, + { url = "https://files.pythonhosted.org/packages/ca/18/d877bd1ae2e5ffdfd4836565aba350db31feb2f2656d6ce70316ed66a05e/lxml-6.1.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:e9308ff8241c532df3f3e570f9a5aeed6c853f888512ba4b75638d7c11c95ef6", size = 4712721, upload-time = "2026-05-18T19:17:40.512Z" }, + { url = "https://files.pythonhosted.org/packages/44/4d/1f44fd1d770b10dacbf6b5c6e520f4d6e0708744930f719dc04e67cab981/lxml-6.1.1-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:5f6994074ebae6ffb04447268e37dc16edc304f9859cf91acb86e0af6c1b395c", size = 5252549, upload-time = "2026-05-18T19:17:51.236Z" }, + { url = "https://files.pythonhosted.org/packages/64/5d/1d66b84f850089254c230ef6ea6b267a5a54e2e179a5d960036a05d501d7/lxml-6.1.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:80c2dfadb855da477cf73373ad29a333535dedb9b12bad02c9814c8e2b43bf08", size = 5226877, upload-time = "2026-05-18T19:18:00.875Z" }, + { url = "https://files.pythonhosted.org/packages/ad/00/84c4b5302d42a2d0184f38d538c8a197f33b52a50bd4f7bcfe990bce3036/lxml-6.1.1-cp311-cp311-win32.whl", hash = "sha256:30a89d3ac8faec007453fb541f3f46807eeec88edd5826f6e3fe001752a2c621", size = 3594072, upload-time = "2026-05-18T19:17:12.714Z" }, + { url = "https://files.pythonhosted.org/packages/61/9d/2e2f7d876349f45e0f3e29f72da311668853d59b58d473a2dea4f0160135/lxml-6.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:abbefa31eee84842140f67acef1c828e28bba8bbf0c3bc6e5492a9af88152c28", size = 4025469, upload-time = "2026-05-18T19:17:50.566Z" }, + { url = "https://files.pythonhosted.org/packages/b0/d5/570e6390e4110331e6208b2ba83d1482cc9146808ee118b22824a34c1070/lxml-6.1.1-cp311-cp311-win_arm64.whl", hash = "sha256:dcb292aa7fe485ceff7af4f92e46c5af397daec5dff64871a528f0fc47a3cc5b", size = 3667640, upload-time = "2026-05-19T19:22:48.293Z" }, + { url = "https://files.pythonhosted.org/packages/6a/6e/c4add832b6fc1e887125b96f880d7b9b70aae5248718e046b1704bcac4b9/lxml-6.1.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:104c09bda8d2a562824c0e319d0768ce26a779b7601e0931d33b09b53c392ef7", size = 8570821, upload-time = "2026-05-18T19:17:42.068Z" }, + { url = "https://files.pythonhosted.org/packages/22/00/ff3009c88e65de8011630acf8ab5a09cb2becd2aaf47fba2f3449f6224e9/lxml-6.1.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:25c6997a9a534e016695a0ba06b2f07945de682731ff01065b6d5a4474179da1", size = 4624252, upload-time = "2026-05-18T19:17:47.897Z" }, + { url = "https://files.pythonhosted.org/packages/42/95/bb63f0fd62e554fe078e1fb3c8fe9083c14ddc7ad7fa178d10e57e071ac7/lxml-6.1.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c921ba5c51e4e9f63b8b00267d06566e1f63407408a0496da2d1d0bfc819c7fc", size = 4930746, upload-time = "2026-05-18T19:18:29.637Z" }, + { url = "https://files.pythonhosted.org/packages/eb/99/0013e8d9b5960f4f041cf0b73e2f80c23eb5205b1f7bfb20203243651359/lxml-6.1.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:54a7f95e4de5fb94e2f9f4b9055c6ba33bf3d628fd77a1d647c5923caa2cdcdc", size = 5093723, upload-time = "2026-05-18T19:18:34.168Z" }, + { url = "https://files.pythonhosted.org/packages/29/91/317b332636bfc7bddcff828d41b3307f50043f4b237e40849c333d80fa1a/lxml-6.1.1-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96f2ec43df44b1f76249ee0a615334f9b5b060e1c8bd90e706dad2d14d02f383", size = 5005557, upload-time = "2026-05-18T19:18:39.798Z" }, + { url = "https://files.pythonhosted.org/packages/42/2f/cc9bf06afe70f9c9093ae60855d9759da9db601ec4080f7473319666ffd7/lxml-6.1.1-cp312-cp312-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:70ef8a7e102a1508f8121aae5b0867abd663f72c14f0a9c937e6554cb4587b7b", size = 5631036, upload-time = "2026-05-18T19:18:44.858Z" }, + { url = "https://files.pythonhosted.org/packages/08/f6/af32e23e563971ffb0fb86be52bc5be5c2c118858ffc119bf6a9039b173d/lxml-6.1.1-cp312-cp312-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ebe6af670449830d6d9b752c256a983291c766a1365ba5d5460048f9e33a7818", size = 5240367, upload-time = "2026-05-18T19:18:49.217Z" }, + { url = "https://files.pythonhosted.org/packages/78/83/8555d40948b09ce86f1bd0c68a7ac31d07b1929f92cc1b074006c97ef2d2/lxml-6.1.1-cp312-cp312-manylinux_2_28_i686.whl", hash = "sha256:27acc820660aaffa4f7c087f29120e12980f7779d56d8492d263170111284740", size = 5350171, upload-time = "2026-05-18T19:18:52.779Z" }, + { url = "https://files.pythonhosted.org/packages/63/75/5d92da93729b7bad783689e6496049fa40927b45bec7bf183c981de3ca70/lxml-6.1.1-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:1db753c9115ec7100d073b744d17e25e88a8f90f5c39b2f5dd878149af59671f", size = 4694874, upload-time = "2026-05-18T19:18:55.139Z" }, + { url = "https://files.pythonhosted.org/packages/c5/b5/3aad415a9a25b822e783f15deeb4dffccf5113030f1afa2222dd929313d9/lxml-6.1.1-cp312-cp312-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c4f469aebd783bb741c2ecb2a681008fd26bfe5c16a9a72ed5467f834e810df2", size = 5244492, upload-time = "2026-05-18T19:19:01.28Z" }, + { url = "https://files.pythonhosted.org/packages/f1/a1/5fcf7eb9904b80086aa47dcf0027de07b1bb990afad2e6823144c368ae04/lxml-6.1.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:766b010012d59470072c1816b5b6c69f1d243e5db36ea5968e94accf430a4635", size = 5048232, upload-time = "2026-05-18T19:18:12.67Z" }, + { url = "https://files.pythonhosted.org/packages/77/74/1f601b63c7a69fcdf10fa9b148c81da8442204194f6c55509cc485c786b9/lxml-6.1.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:b8d812c6011c08b8111a15e54dd990b8923692d80adf35488bee34026c35accf", size = 4777023, upload-time = "2026-05-18T19:18:15.928Z" }, + { url = "https://files.pythonhosted.org/packages/a2/b9/7a78f51aec95b1bf780d78e12705a9f6533284f8693dc5c0e6724fa53d3f/lxml-6.1.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:fe0306bd29505a9177aac19f1877174b0e7422c222a59f70b2cd41633448c3dc", size = 5645773, upload-time = "2026-05-18T19:18:23.223Z" }, + { url = "https://files.pythonhosted.org/packages/a5/6e/98a7b7ad54e4e74fa1f20fff776913980619d0ebe5558232d7da6580bdd8/lxml-6.1.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:5ba186ad207446c65d3bb3d3e0412b032b1d9f595e59861e2354798c5703d955", size = 5233088, upload-time = "2026-05-18T19:18:31.433Z" }, + { url = "https://files.pythonhosted.org/packages/65/d1/bc0ed2427bf609f2ee10da303a6a226f9c8bce94f945dc29a32ce55de6e4/lxml-6.1.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:aa366a1e55b8ebfe8ca8ddc3cfe75c8ebade181aeb0f661d0cb05986b647f72a", size = 5260995, upload-time = "2026-05-18T19:18:37.091Z" }, + { url = "https://files.pythonhosted.org/packages/69/8b/6772e1a4b513fc50a8d931f19edde0e13ae6918510a1e13ff67864f3e5ed/lxml-6.1.1-cp312-cp312-win32.whl", hash = "sha256:126c93f7f56f0eda92f6d8c619edc463a4f23d9252f1c9d0405a76f25fa9f11a", size = 3596382, upload-time = "2026-05-18T19:17:18.37Z" }, + { url = "https://files.pythonhosted.org/packages/1b/89/45198e9624762af2dfd2cb8782598477ceb29f6e59caab560388ae1f4ec1/lxml-6.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:26e6eda8d38c1fcab1090dd196ee87cbd13788e531937610e2589085de074e77", size = 3997255, upload-time = "2026-05-18T19:17:56.781Z" }, + { url = "https://files.pythonhosted.org/packages/90/a9/7a54b6834088d9ae528a7b780584ba6a39a9457b0ac330479f20ffbc9449/lxml-6.1.1-cp312-cp312-win_arm64.whl", hash = "sha256:6540377fbd53fe1b629172288c464fb18db11ce1fa7dc15891da10aa9dcc3e7f", size = 3659610, upload-time = "2026-05-19T19:22:50.843Z" }, + { url = "https://files.pythonhosted.org/packages/b5/32/86a3f0f724a3a402d4627937a7fc27b160e45e7012b4adf47f6e1e844511/lxml-6.1.1-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:31033dc34636ea6b7d5cc11b1ddbda78a14de858ba9d3e1ed4b69a3085bc521e", size = 3930127, upload-time = "2026-05-18T19:19:02.27Z" }, + { url = "https://files.pythonhosted.org/packages/40/44/d832e82af08723761556d004b1d04d281c09f9a8cecd7d3148548c9941a3/lxml-6.1.1-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3893c14c4b6ac5b2d54ba8cf03e99fe5104e592de491f19bd6b82756c09f8004", size = 4210769, upload-time = "2026-05-18T19:20:41.427Z" }, + { url = "https://files.pythonhosted.org/packages/6d/39/0dc5949f759ed7d951e0bb8c2f2d9d7aca1908d22352fa84a8afd2ea54af/lxml-6.1.1-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c07da4cebf6889f03ebac8d238f62318e29f495de0aa18a51ea14e61ae907e2e", size = 4318163, upload-time = "2026-05-18T19:20:44.702Z" }, + { url = "https://files.pythonhosted.org/packages/e6/fb/8ab3845fe046ba4cbf74536bcf6801a774b7caf4350de1c5d37f1f0a9e90/lxml-6.1.1-pp311-pypy311_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f6f0ce10945fab9c4c06ce14e22af9059d1a87493a9af4501a5b0b9187e21cf2", size = 4250945, upload-time = "2026-05-18T19:20:47.385Z" }, + { url = "https://files.pythonhosted.org/packages/68/1b/7553ab136894374ffae8851ec06f98f511cd8e66246e41b6be059d0a7289/lxml-6.1.1-pp311-pypy311_pp73-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f8844cd288697c6425c9beba919302241e3278871dc6519515e72b04e987abcf", size = 4401664, upload-time = "2026-05-18T19:20:50.489Z" }, + { url = "https://files.pythonhosted.org/packages/db/a4/441aee36c6f6b249823d20fd91f9be9ab89d7c5a8ae542a4a4ca6d342d56/lxml-6.1.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:ed21202aec73cda4d55d1ce57b389aadb90ffb044e6cd1080b8347efe1b1ec84", size = 3508989, upload-time = "2026-05-18T19:18:38.158Z" }, +] + +[[package]] +name = "markdown-it-py" +version = "4.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mdurl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/ff/7841249c247aa650a76b9ee4bbaeae59370dc8bfd2f6c01f3630c35eb134/markdown_it_py-4.2.0.tar.gz", hash = "sha256:04a21681d6fbb623de53f6f364d352309d4094dd4194040a10fd51833e418d49", size = 82454, upload-time = "2026-05-07T12:08:28.36Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/81/4da04ced5a082363ecfa159c010d200ecbd959ae410c10c0264a38cac0f5/markdown_it_py-4.2.0-py3-none-any.whl", hash = "sha256:9f7ebbcd14fe59494226453aed97c1070d83f8d24b6fc3a3bcf9a38092641c4a", size = 91687, upload-time = "2026-05-07T12:08:27.182Z" }, +] + +[package.optional-dependencies] +linkify = [ + { name = "linkify-it-py" }, +] + +[[package]] +name = "markupsafe" +version = "3.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/08/db/fefacb2136439fc8dd20e797950e749aa1f4997ed584c62cfb8ef7c2be0e/markupsafe-3.0.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1cc7ea17a6824959616c525620e387f6dd30fec8cb44f649e31712db02123dad", size = 11631, upload-time = "2025-09-27T18:36:18.185Z" }, + { url = "https://files.pythonhosted.org/packages/e1/2e/5898933336b61975ce9dc04decbc0a7f2fee78c30353c5efba7f2d6ff27a/markupsafe-3.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4bd4cd07944443f5a265608cc6aab442e4f74dff8088b0dfc8238647b8f6ae9a", size = 12058, upload-time = "2025-09-27T18:36:19.444Z" }, + { url = "https://files.pythonhosted.org/packages/1d/09/adf2df3699d87d1d8184038df46a9c80d78c0148492323f4693df54e17bb/markupsafe-3.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b5420a1d9450023228968e7e6a9ce57f65d148ab56d2313fcd589eee96a7a50", size = 24287, upload-time = "2025-09-27T18:36:20.768Z" }, + { url = "https://files.pythonhosted.org/packages/30/ac/0273f6fcb5f42e314c6d8cd99effae6a5354604d461b8d392b5ec9530a54/markupsafe-3.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0bf2a864d67e76e5c9a34dc26ec616a66b9888e25e7b9460e1c76d3293bd9dbf", size = 22940, upload-time = "2025-09-27T18:36:22.249Z" }, + { url = "https://files.pythonhosted.org/packages/19/ae/31c1be199ef767124c042c6c3e904da327a2f7f0cd63a0337e1eca2967a8/markupsafe-3.0.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc51efed119bc9cfdf792cdeaa4d67e8f6fcccab66ed4bfdd6bde3e59bfcbb2f", size = 21887, upload-time = "2025-09-27T18:36:23.535Z" }, + { url = "https://files.pythonhosted.org/packages/b2/76/7edcab99d5349a4532a459e1fe64f0b0467a3365056ae550d3bcf3f79e1e/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:068f375c472b3e7acbe2d5318dea141359e6900156b5b2ba06a30b169086b91a", size = 23692, upload-time = "2025-09-27T18:36:24.823Z" }, + { url = "https://files.pythonhosted.org/packages/a4/28/6e74cdd26d7514849143d69f0bf2399f929c37dc2b31e6829fd2045b2765/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:7be7b61bb172e1ed687f1754f8e7484f1c8019780f6f6b0786e76bb01c2ae115", size = 21471, upload-time = "2025-09-27T18:36:25.95Z" }, + { url = "https://files.pythonhosted.org/packages/62/7e/a145f36a5c2945673e590850a6f8014318d5577ed7e5920a4b3448e0865d/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f9e130248f4462aaa8e2552d547f36ddadbeaa573879158d721bbd33dfe4743a", size = 22923, upload-time = "2025-09-27T18:36:27.109Z" }, + { url = "https://files.pythonhosted.org/packages/0f/62/d9c46a7f5c9adbeeeda52f5b8d802e1094e9717705a645efc71b0913a0a8/markupsafe-3.0.3-cp311-cp311-win32.whl", hash = "sha256:0db14f5dafddbb6d9208827849fad01f1a2609380add406671a26386cdf15a19", size = 14572, upload-time = "2025-09-27T18:36:28.045Z" }, + { url = "https://files.pythonhosted.org/packages/83/8a/4414c03d3f891739326e1783338e48fb49781cc915b2e0ee052aa490d586/markupsafe-3.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:de8a88e63464af587c950061a5e6a67d3632e36df62b986892331d4620a35c01", size = 15077, upload-time = "2025-09-27T18:36:29.025Z" }, + { url = "https://files.pythonhosted.org/packages/35/73/893072b42e6862f319b5207adc9ae06070f095b358655f077f69a35601f0/markupsafe-3.0.3-cp311-cp311-win_arm64.whl", hash = "sha256:3b562dd9e9ea93f13d53989d23a7e775fdfd1066c33494ff43f5418bc8c58a5c", size = 13876, upload-time = "2025-09-27T18:36:29.954Z" }, + { url = "https://files.pythonhosted.org/packages/5a/72/147da192e38635ada20e0a2e1a51cf8823d2119ce8883f7053879c2199b5/markupsafe-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e", size = 11615, upload-time = "2025-09-27T18:36:30.854Z" }, + { url = "https://files.pythonhosted.org/packages/9a/81/7e4e08678a1f98521201c3079f77db69fb552acd56067661f8c2f534a718/markupsafe-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce", size = 12020, upload-time = "2025-09-27T18:36:31.971Z" }, + { url = "https://files.pythonhosted.org/packages/1e/2c/799f4742efc39633a1b54a92eec4082e4f815314869865d876824c257c1e/markupsafe-3.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d", size = 24332, upload-time = "2025-09-27T18:36:32.813Z" }, + { url = "https://files.pythonhosted.org/packages/3c/2e/8d0c2ab90a8c1d9a24f0399058ab8519a3279d1bd4289511d74e909f060e/markupsafe-3.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d", size = 22947, upload-time = "2025-09-27T18:36:33.86Z" }, + { url = "https://files.pythonhosted.org/packages/2c/54/887f3092a85238093a0b2154bd629c89444f395618842e8b0c41783898ea/markupsafe-3.0.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a", size = 21962, upload-time = "2025-09-27T18:36:35.099Z" }, + { url = "https://files.pythonhosted.org/packages/c9/2f/336b8c7b6f4a4d95e91119dc8521402461b74a485558d8f238a68312f11c/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b", size = 23760, upload-time = "2025-09-27T18:36:36.001Z" }, + { url = "https://files.pythonhosted.org/packages/32/43/67935f2b7e4982ffb50a4d169b724d74b62a3964bc1a9a527f5ac4f1ee2b/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f", size = 21529, upload-time = "2025-09-27T18:36:36.906Z" }, + { url = "https://files.pythonhosted.org/packages/89/e0/4486f11e51bbba8b0c041098859e869e304d1c261e59244baa3d295d47b7/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b", size = 23015, upload-time = "2025-09-27T18:36:37.868Z" }, + { url = "https://files.pythonhosted.org/packages/2f/e1/78ee7a023dac597a5825441ebd17170785a9dab23de95d2c7508ade94e0e/markupsafe-3.0.3-cp312-cp312-win32.whl", hash = "sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d", size = 14540, upload-time = "2025-09-27T18:36:38.761Z" }, + { url = "https://files.pythonhosted.org/packages/aa/5b/bec5aa9bbbb2c946ca2733ef9c4ca91c91b6a24580193e891b5f7dbe8e1e/markupsafe-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c", size = 15105, upload-time = "2025-09-27T18:36:39.701Z" }, + { url = "https://files.pythonhosted.org/packages/e5/f1/216fc1bbfd74011693a4fd837e7026152e89c4bcf3e77b6692fba9923123/markupsafe-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f", size = 13906, upload-time = "2025-09-27T18:36:40.689Z" }, +] + +[[package]] +name = "mcp" +version = "1.28.1" +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/6e/77/9450b8f251a13affb6281997d0523c4615f8a8b35d0b21ff30db3a5aac9d/mcp-1.28.1.tar.gz", hash = "sha256:d51e36a5f5644faea4f85ea649bfffa6bc6c26770d42798ad6a3de3d2ba69683", size = 638501, upload-time = "2026-06-26T12:57:29.093Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e2/5e/d118fce19f87a2e7d8101c35c8ae0ec289098a4df0ff244cec23e415aca0/mcp-1.28.1-py3-none-any.whl", hash = "sha256:2726bca5e7193f61c5dde8b12500a6de2d9acf6d1a1c0be9e8c2e706437991df", size = 222620, upload-time = "2026-06-26T12:57:27.218Z" }, +] + +[package.optional-dependencies] +ws = [ + { name = "websockets" }, +] + +[[package]] +name = "mcpadapt" +version = "0.1.19" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.12'", +] +dependencies = [ + { name = "jsonref", marker = "python_full_version < '3.12'" }, + { name = "mcp", extra = ["ws"], marker = "python_full_version < '3.12'" }, + { name = "pydantic", marker = "python_full_version < '3.12'" }, + { name = "python-dotenv", marker = "python_full_version < '3.12'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d0/28/64fc666fa5d86bb1b048c167975d4ea19210f9f8571b64b26563739774ac/mcpadapt-0.1.19.tar.gz", hash = "sha256:dfab84fc75cc84a49a40bd61079773b1faf840227b74b82c71a7755b9c1957c5", size = 4227721, upload-time = "2025-10-16T07:11:56.736Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0f/21/703a79103273b5dd268457ffb94dc8b7d6efcc7fe54413e9723cf2caa8c9/mcpadapt-0.1.19-py3-none-any.whl", hash = "sha256:052e91dea8b6f530770d6fd45a1640a8c34816d18d060918dc752c5221083525", size = 19454, upload-time = "2025-10-16T07:11:55.487Z" }, +] + +[[package]] +name = "mcpadapt" +version = "0.1.20" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.12'", +] +dependencies = [ + { name = "jsonref", marker = "python_full_version >= '3.12'" }, + { name = "mcp", extra = ["ws"], marker = "python_full_version >= '3.12'" }, + { name = "pydantic", marker = "python_full_version >= '3.12'" }, + { name = "python-dotenv", marker = "python_full_version >= '3.12'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e3/71/1bbbe157e55d30ab4a74fa878f6942cc0586e9820f03e03451a3d2297e9b/mcpadapt-0.1.20.tar.gz", hash = "sha256:4047c0da61e481dd0673a48936a427da9e6547c6cf0d580ff4e4761dcf058ed1", size = 4203656, upload-time = "2025-10-24T15:35:02.135Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ba/d8/5b6c8cf2070d765904fcb9066f8d7956cb9d399807d86c7fb7f7503b80bf/mcpadapt-0.1.20-py3-none-any.whl", hash = "sha256:117a661eb536dfb0b2a73e5730c2f5ad4e611263e014fb1cebaaff9e78a18f78", size = 19481, upload-time = "2025-10-24T15:35:00.159Z" }, +] + +[[package]] +name = "mdit-py-plugins" +version = "0.6.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/59/fc/f8d0863f8862f25602c0404d75568e89fb6b4109804645e5cdfb1be5cf56/mdit_py_plugins-0.6.1.tar.gz", hash = "sha256:a2bca0f039f39dbd35fb74ae1b5f998608c437463371f0ff7f49a19a17a114d0", size = 56114, upload-time = "2026-05-13T09:03:38.91Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a5/69/6da5581c6a7fede7dc261bf4e67d6adca4196f176b43288b55b3db395b6e/mdit_py_plugins-0.6.1-py3-none-any.whl", hash = "sha256:214c82fb2ac524472ab6a5bcab1de80f73b50443e187f401bfd77efbc7c6481d", size = 66663, upload-time = "2026-05-13T09:03:37.76Z" }, +] + +[[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, 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, upload-time = "2022-08-14T12:40:09.779Z" }, +] + +[[package]] +name = "mmh3" +version = "5.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/91/1a/edb23803a168f070ded7a3014c6d706f63b90c84ccc024f89d794a3b7a6d/mmh3-5.2.1.tar.gz", hash = "sha256:bbea5b775f0ac84945191fb83f845a6fd9a21a03ea7f2e187defac7e401616ad", size = 33775, upload-time = "2026-03-05T15:55:57.716Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/65/d7/3312a59df3c1cdd783f4cf0c4ee8e9decff9c5466937182e4cc7dbbfe6c5/mmh3-5.2.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:dae0f0bd7d30c0ad61b9a504e8e272cb8391eed3f1587edf933f4f6b33437450", size = 56082, upload-time = "2026-03-05T15:53:59.702Z" }, + { url = "https://files.pythonhosted.org/packages/61/96/6f617baa098ca0d2989bfec6d28b5719532cd8d8848782662f5b755f657f/mmh3-5.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:9aeaf53eaa075dd63e81512522fd180097312fb2c9f476333309184285c49ce0", size = 40458, upload-time = "2026-03-05T15:54:01.548Z" }, + { url = "https://files.pythonhosted.org/packages/c1/b4/9cd284bd6062d711e13d26c04d4778ab3f690c1c38a4563e3c767ec8802e/mmh3-5.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0634581290e6714c068f4aa24020acf7880927d1f0084fa753d9799ae9610082", size = 40079, upload-time = "2026-03-05T15:54:02.743Z" }, + { url = "https://files.pythonhosted.org/packages/f6/09/a806334ce1d3d50bf782b95fcee8b3648e1e170327d4bb7b4bad2ad7d956/mmh3-5.2.1-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:e080c0637aea036f35507e803a4778f119a9b436617694ae1c5c366805f1e997", size = 97242, upload-time = "2026-03-05T15:54:04.536Z" }, + { url = "https://files.pythonhosted.org/packages/ee/93/723e317dd9e041c4dc4566a2eb53b01ad94de31750e0b834f1643905e97c/mmh3-5.2.1-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:db0562c5f71d18596dcd45e854cf2eeba27d7543e1a3acdafb7eef728f7fe85d", size = 103082, upload-time = "2026-03-05T15:54:06.387Z" }, + { url = "https://files.pythonhosted.org/packages/61/b5/f96121e69cc48696075071531cf574f112e1ffd08059f4bffb41210e6fc5/mmh3-5.2.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1d9f9a3ce559a5267014b04b82956993270f63ec91765e13e9fd73daf2d2738e", size = 106054, upload-time = "2026-03-05T15:54:07.506Z" }, + { url = "https://files.pythonhosted.org/packages/82/49/192b987ec48d0b2aecf8ac285a9b11fbc00030f6b9c694664ae923458dde/mmh3-5.2.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:960b1b3efa39872ac8b6cc3a556edd6fb90ed74f08c9c45e028f1005b26aa55d", size = 112910, upload-time = "2026-03-05T15:54:09.403Z" }, + { url = "https://files.pythonhosted.org/packages/cf/a1/03e91fd334ed0144b83343a76eb11f17434cd08f746401488cfeafb2d241/mmh3-5.2.1-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d30b650595fdbe32366b94cb14f30bb2b625e512bd4e1df00611f99dc5c27fd4", size = 120551, upload-time = "2026-03-05T15:54:10.587Z" }, + { url = "https://files.pythonhosted.org/packages/93/b9/b89a71d2ff35c3a764d1c066c7313fc62c7cc48fa48a4b3b0304a4a0146f/mmh3-5.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:82f3802bfc4751f420d591c5c864de538b71cea117fce67e4595c2afede08a15", size = 99096, upload-time = "2026-03-05T15:54:11.76Z" }, + { url = "https://files.pythonhosted.org/packages/36/b5/613772c1c6ed5f7b63df55eb131e887cc43720fec392777b95a79d34e640/mmh3-5.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:915e7a2418f10bd1151b1953df06d896db9783c9cfdb9a8ee1f9b3a4331ab503", size = 98524, upload-time = "2026-03-05T15:54:13.122Z" }, + { url = "https://files.pythonhosted.org/packages/5e/0e/1524566fe8eaf871e4f7bc44095929fcd2620488f402822d848df19d679c/mmh3-5.2.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:fc78739b5ec6e4fb02301984a3d442a91406e7700efbe305071e7fd1c78278f2", size = 106239, upload-time = "2026-03-05T15:54:14.601Z" }, + { url = "https://files.pythonhosted.org/packages/04/94/21adfa7d90a7a697137ad6de33eeff6445420ca55e433a5d4919c79bc3b5/mmh3-5.2.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:41aac7002a749f08727cb91babff1daf8deac317c0b1f317adc69be0e6c375d1", size = 109797, upload-time = "2026-03-05T15:54:15.819Z" }, + { url = "https://files.pythonhosted.org/packages/b5/e6/1aacc3a219e1aa62fa65669995d4a3562b35be5200ec03680c7e4bec9676/mmh3-5.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9d8089d853c7963a8ce87fff93e2a67075c0bc08684a08ea6ad13577c38ffc38", size = 97228, upload-time = "2026-03-05T15:54:16.992Z" }, + { url = "https://files.pythonhosted.org/packages/f1/b9/5e4cca8dcccf298add0a27f3c357bc8cf8baf821d35cdc6165e4bd5a48b0/mmh3-5.2.1-cp311-cp311-win32.whl", hash = "sha256:baeb47635cb33375dee4924cd93d7f5dcaa786c740b08423b0209b824a1ee728", size = 40751, upload-time = "2026-03-05T15:54:18.714Z" }, + { url = "https://files.pythonhosted.org/packages/72/fc/5b11d49247f499bcda591171e9cf3b6ee422b19e70aa2cef2e0ae65ca3b9/mmh3-5.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:1e4ecee40ba19e6975e1120829796770325841c2f153c0e9aecca927194c6a2a", size = 41517, upload-time = "2026-03-05T15:54:19.764Z" }, + { url = "https://files.pythonhosted.org/packages/8a/5f/2a511ee8a1c2a527c77726d5231685b72312c5a1a1b7639ad66a9652aa84/mmh3-5.2.1-cp311-cp311-win_arm64.whl", hash = "sha256:c302245fd6c33d96bd169c7ccf2513c20f4c1e417c07ce9dce107c8bc3f8411f", size = 39287, upload-time = "2026-03-05T15:54:20.904Z" }, + { url = "https://files.pythonhosted.org/packages/92/94/bc5c3b573b40a328c4d141c20e399039ada95e5e2a661df3425c5165fd84/mmh3-5.2.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0cc21533878e5586b80d74c281d7f8da7932bc8ace50b8d5f6dbf7e3935f63f1", size = 56087, upload-time = "2026-03-05T15:54:21.92Z" }, + { url = "https://files.pythonhosted.org/packages/f6/80/64a02cc3e95c3af0aaa2590849d9ed24a9f14bb93537addde688e039b7c3/mmh3-5.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4eda76074cfca2787c8cf1bec603eaebdddd8b061ad5502f85cddae998d54f00", size = 40500, upload-time = "2026-03-05T15:54:22.953Z" }, + { url = "https://files.pythonhosted.org/packages/8b/72/e6d6602ce18adf4ddcd0e48f2e13590cc92a536199e52109f46f259d3c46/mmh3-5.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:eee884572b06bbe8a2b54f424dbd996139442cf83c76478e1ec162512e0dd2c7", size = 40034, upload-time = "2026-03-05T15:54:23.943Z" }, + { url = "https://files.pythonhosted.org/packages/59/c2/bf4537a8e58e21886ef16477041238cab5095c836496e19fafc34b7445d2/mmh3-5.2.1-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:0d0b7e803191db5f714d264044e06189c8ccd3219e936cc184f07106bd17fd7b", size = 97292, upload-time = "2026-03-05T15:54:25.335Z" }, + { url = "https://files.pythonhosted.org/packages/e5/e2/51ed62063b44d10b06d975ac87af287729eeb5e3ed9772f7584a17983e90/mmh3-5.2.1-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8e6c219e375f6341d0959af814296372d265a8ca1af63825f65e2e87c618f006", size = 103274, upload-time = "2026-03-05T15:54:26.44Z" }, + { url = "https://files.pythonhosted.org/packages/75/ce/12a7524dca59eec92e5b31fdb13ede1e98eda277cf2b786cf73bfbc24e81/mmh3-5.2.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:26fb5b9c3946bf7f1daed7b37e0c03898a6f062149127570f8ede346390a0825", size = 106158, upload-time = "2026-03-05T15:54:28.578Z" }, + { url = "https://files.pythonhosted.org/packages/86/1f/d3ba6dd322d01ab5d44c46c8f0c38ab6bbbf9b5e20e666dfc05bf4a23604/mmh3-5.2.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3c38d142c706201db5b2345166eeef1e7740e3e2422b470b8ba5c8727a9b4c7a", size = 113005, upload-time = "2026-03-05T15:54:29.767Z" }, + { url = "https://files.pythonhosted.org/packages/b6/a9/15d6b6f913294ea41b44d901741298e3718e1cb89ee626b3694625826a43/mmh3-5.2.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:50885073e2909251d4718634a191c49ae5f527e5e1736d738e365c3e8be8f22b", size = 120744, upload-time = "2026-03-05T15:54:30.931Z" }, + { url = "https://files.pythonhosted.org/packages/76/b3/70b73923fd0284c439860ff5c871b20210dfdbe9a6b9dd0ee6496d77f174/mmh3-5.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b3f99e1756fc48ad507b95e5d86f2fb21b3d495012ff13e6592ebac14033f166", size = 99111, upload-time = "2026-03-05T15:54:32.353Z" }, + { url = "https://files.pythonhosted.org/packages/dd/38/99f7f75cd27d10d8b899a1caafb9d531f3903e4d54d572220e3d8ac35e89/mmh3-5.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:62815d2c67f2dd1be76a253d88af4e1da19aeaa1820146dec52cf8bee2958b16", size = 98623, upload-time = "2026-03-05T15:54:33.801Z" }, + { url = "https://files.pythonhosted.org/packages/fd/68/6e292c0853e204c44d2f03ea5f090be3317a0e2d9417ecb62c9eb27687df/mmh3-5.2.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:8f767ba0911602ddef289404e33835a61168314ebd3c729833db2ed685824211", size = 106437, upload-time = "2026-03-05T15:54:35.177Z" }, + { url = "https://files.pythonhosted.org/packages/dd/c6/fedd7284c459cfb58721d461fcf5607a4c1f5d9ab195d113d51d10164d16/mmh3-5.2.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:67e41a497bac88cc1de96eeba56eeb933c39d54bc227352f8455aa87c4ca4000", size = 110002, upload-time = "2026-03-05T15:54:36.673Z" }, + { url = "https://files.pythonhosted.org/packages/3b/ac/ca8e0c19a34f5b71390171d2ff0b9f7f187550d66801a731bb68925126a4/mmh3-5.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3d74a03fb57757ece25aa4b3c1c60157a1cece37a020542785f942e2f827eed5", size = 97507, upload-time = "2026-03-05T15:54:37.804Z" }, + { url = "https://files.pythonhosted.org/packages/df/94/6ebb9094cfc7ac5e7950776b9d13a66bb4a34f83814f32ba2abc9494fc68/mmh3-5.2.1-cp312-cp312-win32.whl", hash = "sha256:7374d6e3ef72afe49697ecd683f3da12f4fc06af2d75433d0580c6746d2fa025", size = 40773, upload-time = "2026-03-05T15:54:40.077Z" }, + { url = "https://files.pythonhosted.org/packages/5b/3c/cd3527198cf159495966551c84a5f36805a10ac17b294f41f67b83f6a4d6/mmh3-5.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:3a9fed49c6ce4ed7e73f13182760c65c816da006debe67f37635580dfb0fae00", size = 41560, upload-time = "2026-03-05T15:54:41.148Z" }, + { url = "https://files.pythonhosted.org/packages/15/96/6fe5ebd0f970a076e3ed5512871ce7569447b962e96c125528a2f9724470/mmh3-5.2.1-cp312-cp312-win_arm64.whl", hash = "sha256:bbfcb95d9a744e6e2827dfc66ad10e1020e0cac255eb7f85652832d5a264c2fc", size = 39313, upload-time = "2026-03-05T15:54:42.171Z" }, +] + +[[package]] +name = "multidict" +version = "6.7.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1a/c2/c2d94cbe6ac1753f3fc980da97b3d930efe1da3af3c9f5125354436c073d/multidict-6.7.1.tar.gz", hash = "sha256:ec6652a1bee61c53a3e5776b6049172c53b6aaba34f18c9ad04f82712bac623d", size = 102010, upload-time = "2026-01-26T02:46:45.979Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ce/f1/a90635c4f88fb913fbf4ce660b83b7445b7a02615bda034b2f8eb38fd597/multidict-6.7.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7ff981b266af91d7b4b3793ca3382e53229088d193a85dfad6f5f4c27fc73e5d", size = 76626, upload-time = "2026-01-26T02:43:26.485Z" }, + { url = "https://files.pythonhosted.org/packages/a6/9b/267e64eaf6fc637a15b35f5de31a566634a2740f97d8d094a69d34f524a4/multidict-6.7.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:844c5bca0b5444adb44a623fb0a1310c2f4cd41f402126bb269cd44c9b3f3e1e", size = 44706, upload-time = "2026-01-26T02:43:27.607Z" }, + { url = "https://files.pythonhosted.org/packages/dd/a4/d45caf2b97b035c57267791ecfaafbd59c68212004b3842830954bb4b02e/multidict-6.7.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f2a0a924d4c2e9afcd7ec64f9de35fcd96915149b2216e1cb2c10a56df483855", size = 44356, upload-time = "2026-01-26T02:43:28.661Z" }, + { url = "https://files.pythonhosted.org/packages/fd/d2/0a36c8473f0cbaeadd5db6c8b72d15bbceeec275807772bfcd059bef487d/multidict-6.7.1-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:8be1802715a8e892c784c0197c2ace276ea52702a0ede98b6310c8f255a5afb3", size = 244355, upload-time = "2026-01-26T02:43:31.165Z" }, + { url = "https://files.pythonhosted.org/packages/5d/16/8c65be997fd7dd311b7d39c7b6e71a0cb449bad093761481eccbbe4b42a2/multidict-6.7.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2e2d2ed645ea29f31c4c7ea1552fcfd7cb7ba656e1eafd4134a6620c9f5fdd9e", size = 246433, upload-time = "2026-01-26T02:43:32.581Z" }, + { url = "https://files.pythonhosted.org/packages/01/fb/4dbd7e848d2799c6a026ec88ad39cf2b8416aa167fcc903baa55ecaa045c/multidict-6.7.1-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:95922cee9a778659e91db6497596435777bd25ed116701a4c034f8e46544955a", size = 225376, upload-time = "2026-01-26T02:43:34.417Z" }, + { url = "https://files.pythonhosted.org/packages/b6/8a/4a3a6341eac3830f6053062f8fbc9a9e54407c80755b3f05bc427295c2d0/multidict-6.7.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6b83cabdc375ffaaa15edd97eb7c0c672ad788e2687004990074d7d6c9b140c8", size = 257365, upload-time = "2026-01-26T02:43:35.741Z" }, + { url = "https://files.pythonhosted.org/packages/f7/a2/dd575a69c1aa206e12d27d0770cdf9b92434b48a9ef0cd0d1afdecaa93c4/multidict-6.7.1-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:38fb49540705369bab8484db0689d86c0a33a0a9f2c1b197f506b71b4b6c19b0", size = 254747, upload-time = "2026-01-26T02:43:36.976Z" }, + { url = "https://files.pythonhosted.org/packages/5a/56/21b27c560c13822ed93133f08aa6372c53a8e067f11fbed37b4adcdac922/multidict-6.7.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:439cbebd499f92e9aa6793016a8acaa161dfa749ae86d20960189f5398a19144", size = 246293, upload-time = "2026-01-26T02:43:38.258Z" }, + { url = "https://files.pythonhosted.org/packages/5a/a4/23466059dc3854763423d0ad6c0f3683a379d97673b1b89ec33826e46728/multidict-6.7.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6d3bc717b6fe763b8be3f2bee2701d3c8eb1b2a8ae9f60910f1b2860c82b6c49", size = 242962, upload-time = "2026-01-26T02:43:40.034Z" }, + { url = "https://files.pythonhosted.org/packages/1f/67/51dd754a3524d685958001e8fa20a0f5f90a6a856e0a9dcabff69be3dbb7/multidict-6.7.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:619e5a1ac57986dbfec9f0b301d865dddf763696435e2962f6d9cf2fdff2bb71", size = 237360, upload-time = "2026-01-26T02:43:41.752Z" }, + { url = "https://files.pythonhosted.org/packages/64/3f/036dfc8c174934d4b55d86ff4f978e558b0e585cef70cfc1ad01adc6bf18/multidict-6.7.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:0b38ebffd9be37c1170d33bc0f36f4f262e0a09bc1aac1c34c7aa51a7293f0b3", size = 245940, upload-time = "2026-01-26T02:43:43.042Z" }, + { url = "https://files.pythonhosted.org/packages/3d/20/6214d3c105928ebc353a1c644a6ef1408bc5794fcb4f170bb524a3c16311/multidict-6.7.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:10ae39c9cfe6adedcdb764f5e8411d4a92b055e35573a2eaa88d3323289ef93c", size = 253502, upload-time = "2026-01-26T02:43:44.371Z" }, + { url = "https://files.pythonhosted.org/packages/b1/e2/c653bc4ae1be70a0f836b82172d643fcf1dade042ba2676ab08ec08bff0f/multidict-6.7.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:25167cc263257660290fba06b9318d2026e3c910be240a146e1f66dd114af2b0", size = 247065, upload-time = "2026-01-26T02:43:45.745Z" }, + { url = "https://files.pythonhosted.org/packages/c8/11/a854b4154cd3bd8b1fd375e8a8ca9d73be37610c361543d56f764109509b/multidict-6.7.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:128441d052254f42989ef98b7b6a6ecb1e6f708aa962c7984235316db59f50fa", size = 241870, upload-time = "2026-01-26T02:43:47.054Z" }, + { url = "https://files.pythonhosted.org/packages/13/bf/9676c0392309b5fdae322333d22a829715b570edb9baa8016a517b55b558/multidict-6.7.1-cp311-cp311-win32.whl", hash = "sha256:d62b7f64ffde3b99d06b707a280db04fb3855b55f5a06df387236051d0668f4a", size = 41302, upload-time = "2026-01-26T02:43:48.753Z" }, + { url = "https://files.pythonhosted.org/packages/c9/68/f16a3a8ba6f7b6dc92a1f19669c0810bd2c43fc5a02da13b1cbf8e253845/multidict-6.7.1-cp311-cp311-win_amd64.whl", hash = "sha256:bdbf9f3b332abd0cdb306e7c2113818ab1e922dc84b8f8fd06ec89ed2a19ab8b", size = 45981, upload-time = "2026-01-26T02:43:49.921Z" }, + { url = "https://files.pythonhosted.org/packages/ac/ad/9dd5305253fa00cd3c7555dbef69d5bf4133debc53b87ab8d6a44d411665/multidict-6.7.1-cp311-cp311-win_arm64.whl", hash = "sha256:b8c990b037d2fff2f4e33d3f21b9b531c5745b33a49a7d6dbe7a177266af44f6", size = 43159, upload-time = "2026-01-26T02:43:51.635Z" }, + { url = "https://files.pythonhosted.org/packages/8d/9c/f20e0e2cf80e4b2e4b1c365bf5fe104ee633c751a724246262db8f1a0b13/multidict-6.7.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:a90f75c956e32891a4eda3639ce6dd86e87105271f43d43442a3aedf3cddf172", size = 76893, upload-time = "2026-01-26T02:43:52.754Z" }, + { url = "https://files.pythonhosted.org/packages/fe/cf/18ef143a81610136d3da8193da9d80bfe1cb548a1e2d1c775f26b23d024a/multidict-6.7.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3fccb473e87eaa1382689053e4a4618e7ba7b9b9b8d6adf2027ee474597128cd", size = 45456, upload-time = "2026-01-26T02:43:53.893Z" }, + { url = "https://files.pythonhosted.org/packages/a9/65/1caac9d4cd32e8433908683446eebc953e82d22b03d10d41a5f0fefe991b/multidict-6.7.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b0fa96985700739c4c7853a43c0b3e169360d6855780021bfc6d0f1ce7c123e7", size = 43872, upload-time = "2026-01-26T02:43:55.041Z" }, + { url = "https://files.pythonhosted.org/packages/cf/3b/d6bd75dc4f3ff7c73766e04e705b00ed6dbbaccf670d9e05a12b006f5a21/multidict-6.7.1-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:cb2a55f408c3043e42b40cc8eecd575afa27b7e0b956dfb190de0f8499a57a53", size = 251018, upload-time = "2026-01-26T02:43:56.198Z" }, + { url = "https://files.pythonhosted.org/packages/fd/80/c959c5933adedb9ac15152e4067c702a808ea183a8b64cf8f31af8ad3155/multidict-6.7.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eb0ce7b2a32d09892b3dd6cc44877a0d02a33241fafca5f25c8b6b62374f8b75", size = 258883, upload-time = "2026-01-26T02:43:57.499Z" }, + { url = "https://files.pythonhosted.org/packages/86/85/7ed40adafea3d4f1c8b916e3b5cc3a8e07dfcdcb9cd72800f4ed3ca1b387/multidict-6.7.1-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c3a32d23520ee37bf327d1e1a656fec76a2edd5c038bf43eddfa0572ec49c60b", size = 242413, upload-time = "2026-01-26T02:43:58.755Z" }, + { url = "https://files.pythonhosted.org/packages/d2/57/b8565ff533e48595503c785f8361ff9a4fde4d67de25c207cd0ba3befd03/multidict-6.7.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9c90fed18bffc0189ba814749fdcc102b536e83a9f738a9003e569acd540a733", size = 268404, upload-time = "2026-01-26T02:44:00.216Z" }, + { url = "https://files.pythonhosted.org/packages/e0/50/9810c5c29350f7258180dfdcb2e52783a0632862eb334c4896ac717cebcb/multidict-6.7.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:da62917e6076f512daccfbbde27f46fed1c98fee202f0559adec8ee0de67f71a", size = 269456, upload-time = "2026-01-26T02:44:02.202Z" }, + { url = "https://files.pythonhosted.org/packages/f3/8d/5e5be3ced1d12966fefb5c4ea3b2a5b480afcea36406559442c6e31d4a48/multidict-6.7.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bfde23ef6ed9db7eaee6c37dcec08524cb43903c60b285b172b6c094711b3961", size = 256322, upload-time = "2026-01-26T02:44:03.56Z" }, + { url = "https://files.pythonhosted.org/packages/31/6e/d8a26d81ac166a5592782d208dd90dfdc0a7a218adaa52b45a672b46c122/multidict-6.7.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3758692429e4e32f1ba0df23219cd0b4fc0a52f476726fff9337d1a57676a582", size = 253955, upload-time = "2026-01-26T02:44:04.845Z" }, + { url = "https://files.pythonhosted.org/packages/59/4c/7c672c8aad41534ba619bcd4ade7a0dc87ed6b8b5c06149b85d3dd03f0cd/multidict-6.7.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:398c1478926eca669f2fd6a5856b6de9c0acf23a2cb59a14c0ba5844fa38077e", size = 251254, upload-time = "2026-01-26T02:44:06.133Z" }, + { url = "https://files.pythonhosted.org/packages/7b/bd/84c24de512cbafbdbc39439f74e967f19570ce7924e3007174a29c348916/multidict-6.7.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:c102791b1c4f3ab36ce4101154549105a53dc828f016356b3e3bcae2e3a039d3", size = 252059, upload-time = "2026-01-26T02:44:07.518Z" }, + { url = "https://files.pythonhosted.org/packages/fa/ba/f5449385510825b73d01c2d4087bf6d2fccc20a2d42ac34df93191d3dd03/multidict-6.7.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:a088b62bd733e2ad12c50dad01b7d0166c30287c166e137433d3b410add807a6", size = 263588, upload-time = "2026-01-26T02:44:09.382Z" }, + { url = "https://files.pythonhosted.org/packages/d7/11/afc7c677f68f75c84a69fe37184f0f82fce13ce4b92f49f3db280b7e92b3/multidict-6.7.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:3d51ff4785d58d3f6c91bdbffcb5e1f7ddfda557727043aa20d20ec4f65e324a", size = 259642, upload-time = "2026-01-26T02:44:10.73Z" }, + { url = "https://files.pythonhosted.org/packages/2b/17/ebb9644da78c4ab36403739e0e6e0e30ebb135b9caf3440825001a0bddcb/multidict-6.7.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fc5907494fccf3e7d3f94f95c91d6336b092b5fc83811720fae5e2765890dfba", size = 251377, upload-time = "2026-01-26T02:44:12.042Z" }, + { url = "https://files.pythonhosted.org/packages/ca/a4/840f5b97339e27846c46307f2530a2805d9d537d8b8bd416af031cad7fa0/multidict-6.7.1-cp312-cp312-win32.whl", hash = "sha256:28ca5ce2fd9716631133d0e9a9b9a745ad7f60bac2bccafb56aa380fc0b6c511", size = 41887, upload-time = "2026-01-26T02:44:14.245Z" }, + { url = "https://files.pythonhosted.org/packages/80/31/0b2517913687895f5904325c2069d6a3b78f66cc641a86a2baf75a05dcbb/multidict-6.7.1-cp312-cp312-win_amd64.whl", hash = "sha256:fcee94dfbd638784645b066074b338bc9cc155d4b4bffa4adce1615c5a426c19", size = 46053, upload-time = "2026-01-26T02:44:15.371Z" }, + { url = "https://files.pythonhosted.org/packages/0c/5b/aba28e4ee4006ae4c7df8d327d31025d760ffa992ea23812a601d226e682/multidict-6.7.1-cp312-cp312-win_arm64.whl", hash = "sha256:ba0a9fb644d0c1a2194cf7ffb043bd852cea63a57f66fbd33959f7dae18517bf", size = 43307, upload-time = "2026-01-26T02:44:16.852Z" }, + { url = "https://files.pythonhosted.org/packages/81/08/7036c080d7117f28a4af526d794aab6a84463126db031b007717c1a6676e/multidict-6.7.1-py3-none-any.whl", hash = "sha256:55d97cc6dae627efa6a6e548885712d4864b81110ac76fa4e534c03819fa4a56", size = 12319, upload-time = "2026-01-26T02:46:44.004Z" }, +] + +[[package]] +name = "numpy" +version = "2.4.6" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.12'", +] +sdist = { url = "https://files.pythonhosted.org/packages/d0/ad/fed0499ce6a338d2a03ebae59cd15093910c8875328855781952abf6c2fe/numpy-2.4.6.tar.gz", hash = "sha256:f3a3570c4a2a16746ac2c31a7c7c7b0c186b95ce902e33db6f28094ed7387dda", size = 20735807, upload-time = "2026-05-18T23:37:14.07Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/49/ec46835a70be8fa6446c495126ac84fdb28cb2558e1620ffb87a10c8b64c/numpy-2.4.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0280e0356c0829a18d9de1cb7eee50ec22ca639878d7240307ca0943d73cd2c4", size = 16969194, upload-time = "2026-05-18T23:33:13.503Z" }, + { url = "https://files.pythonhosted.org/packages/0e/0d/f5957185c0ee2f3e12f78715aa9e3b353fd83633316c8532b38faa37e3f6/numpy-2.4.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:110f8b71aacb688ec69062bb7f6938a0f8acb01b7c1c4beb453c65b6d234584d", size = 14964111, upload-time = "2026-05-18T23:33:17.795Z" }, + { url = "https://files.pythonhosted.org/packages/ad/40/40a40ee0ddf7ceb782c49af278894b686e586d65d8c1889c8b5da01a3d7d/numpy-2.4.6-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:4cfe66903cc32a9921a6733d96b19bb6abf310397581bbad89c228f5abaf0ee8", size = 5469159, upload-time = "2026-05-18T23:33:20.654Z" }, + { url = "https://files.pythonhosted.org/packages/63/13/f9a8046535cb21deae82f8d03de9617e08882d274fad2539630761888228/numpy-2.4.6-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:8155154c7c691289fe18f510b5d4657c68c67989f293f0535a91360392ff6538", size = 6798936, upload-time = "2026-05-18T23:33:22.987Z" }, + { url = "https://files.pythonhosted.org/packages/33/a8/6fa8c1a345a8c85dbb21932c447bee07c30a2c2a3f31e369c0a84b300147/numpy-2.4.6-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ab0a9c4ffb1a6d95ef519fe4247dba8eb6b18ad93999f76b7f657039acabd47", size = 15966692, upload-time = "2026-05-18T23:33:26.62Z" }, + { url = "https://files.pythonhosted.org/packages/02/03/74fe2a4cb3817d94d86402f2506554130a2f01414e299b5a843e5a8a957f/numpy-2.4.6-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:89cd468399cfd2504718f0ba50e410dca55a170b61a02ad92bb18c8a65186e93", size = 16918164, upload-time = "2026-05-18T23:33:29.955Z" }, + { url = "https://files.pythonhosted.org/packages/c5/80/3615be3313f7e7696609bc194b9f0101da809df79e859bdb84e0cd043f46/numpy-2.4.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c2d37ab77531417474168eb79d6d80b14f821a966818505d03013d0833edb7a8", size = 17322877, upload-time = "2026-05-18T23:33:34.724Z" }, + { url = "https://files.pythonhosted.org/packages/ca/ac/a691e0fe2675e370d0e08ff905adc49a1c8830e8cae03efe4477e92cd55d/numpy-2.4.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f407cb6b8e9d6d8c626bc73c945db1706035af8fd632295547bf1c9e46d092d6", size = 18651487, upload-time = "2026-05-18T23:33:38.217Z" }, + { url = "https://files.pythonhosted.org/packages/15/a7/9bc1cd626d7bf6869bfedf27b91b6ab5dd607758bf8e959d6fa80c6a59cb/numpy-2.4.6-cp311-cp311-win32.whl", hash = "sha256:ddea102b48f9e339f3948bf22040944184627a30fdf7f858667673b9c5f033c8", size = 6233945, upload-time = "2026-05-18T23:33:41.331Z" }, + { url = "https://files.pythonhosted.org/packages/c5/31/7fc6239c12bce7e931463251cca4426c465e1876ba3cc785402ef4dd8f4e/numpy-2.4.6-cp311-cp311-win_amd64.whl", hash = "sha256:1e254a00cdf42b1e4d5b3d68d33af63268d41340d8885df2ab6470f2e1500147", size = 12608406, upload-time = "2026-05-18T23:33:44.131Z" }, + { url = "https://files.pythonhosted.org/packages/27/83/140f85a466595a16382996a1bf06b2b54bcd597488921b0c9daaeeda72af/numpy-2.4.6-cp311-cp311-win_arm64.whl", hash = "sha256:ed9749eef4cbd126da3dc1d6bcb3a57f5eb7ac6a6484146bdbf743f552dfc577", size = 10479528, upload-time = "2026-05-18T23:33:50.725Z" }, + { url = "https://files.pythonhosted.org/packages/95/2a/3d7b5ac8aac24feaf9ad7ed58f45b0bbc06d37e4338ae84c9f2298b570f9/numpy-2.4.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:001fbb8e08d942dd57599e781f2472269ee7f2755fae407b4f67b2f0b17da3f1", size = 16689119, upload-time = "2026-05-18T23:33:54.065Z" }, + { url = "https://files.pythonhosted.org/packages/ea/12/92c4c131527599e8288d6918e888d88726f84d805d784b771f32408aeaef/numpy-2.4.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ebfb099f8dcf083deef3ac1ca4c1503f387cf76296fcb3816b66f5ecb5f54fdb", size = 14699246, upload-time = "2026-05-18T23:33:57.621Z" }, + { url = "https://files.pythonhosted.org/packages/ad/fe/c0a6b7b2ca128a8fb228575147073b660656734b8ebe4d76c8fd748dcc79/numpy-2.4.6-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:3213d622a0283a39a93d188f3cf72b26862df52fbb4ca3697f51705016523d41", size = 5204410, upload-time = "2026-05-18T23:34:00.302Z" }, + { url = "https://files.pythonhosted.org/packages/f3/d4/9770d14ba719432bb90a421bfd443872ed0f70f7264b64bec12ea363d5fd/numpy-2.4.6-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:357cc07a6d7b0b182ff02249616a03742827ebb1277546b5c7cd7f7620a45698", size = 6551240, upload-time = "2026-05-18T23:34:02.852Z" }, + { url = "https://files.pythonhosted.org/packages/c9/c6/50a46a6205feba2343f1d6d17438107c5dc491ed1c736e6ea68689fd906b/numpy-2.4.6-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f9fb9157b4ce2971008323afe46053787b526ef624fea915b261468a8421a0f", size = 15671012, upload-time = "2026-05-18T23:34:05.485Z" }, + { url = "https://files.pythonhosted.org/packages/99/60/14115e6364fa676c5397c2ad3004e527e9aa487abf5d0706ec81bbd08529/numpy-2.4.6-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:90f9849678c75fe7afa2d348ac842c168b0a4d3d61919687216dfc547976d853", size = 16645538, upload-time = "2026-05-18T23:34:09.265Z" }, + { url = "https://files.pythonhosted.org/packages/ae/c5/693cbe59e57db94d2231fa519ca3978dc9e19da5a8f088588f5c6e947ff2/numpy-2.4.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c1a2af6c6ef86344a6b0db6b97834208bf598db514f2b155042439b62605601a", size = 17020706, upload-time = "2026-05-18T23:34:13.053Z" }, + { url = "https://files.pythonhosted.org/packages/ef/fc/85b7c4eff9b4966ade25c2273cf7e7012e92366c032058653934b37de044/numpy-2.4.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e5805d5a22fd19c8ccff10a9561f9df94436b0545619ea579db2d3c35294bce2", size = 18368541, upload-time = "2026-05-18T23:34:17.024Z" }, + { url = "https://files.pythonhosted.org/packages/f6/81/e1b27545deedce7f4a0b348618c6b62d74e36a4dc9ccd42f3eb2f85eee32/numpy-2.4.6-cp312-cp312-win32.whl", hash = "sha256:e3eeb0aabd6bd5ce64faae67e9935203a6991b4bc2a485a767fbafb2c5125f45", size = 5962825, upload-time = "2026-05-18T23:34:20.3Z" }, + { url = "https://files.pythonhosted.org/packages/ab/ca/feab00bd44aa5fe1ad2c18f08b4d3bb92e26484b0b1d1443897809ed528c/numpy-2.4.6-cp312-cp312-win_amd64.whl", hash = "sha256:d8e8286dd7cea7895157318d1b91cdacac64c479f3cbc8dce548331728484751", size = 12321687, upload-time = "2026-05-18T23:34:23.095Z" }, + { url = "https://files.pythonhosted.org/packages/63/cf/5a6d34850a39d1093558564f77ee8e8e0bee5061151b8f05a55711001ec7/numpy-2.4.6-cp312-cp312-win_arm64.whl", hash = "sha256:4081eb135ac24158bd51cdfbef16f1c64df7063b1143f24731387137c092bec8", size = 10221482, upload-time = "2026-05-18T23:34:25.876Z" }, + { url = "https://files.pythonhosted.org/packages/de/12/b422cc84439adc0d00de605bf4a308890ae5c26f2c71fbd73e5d08fbb0dd/numpy-2.4.6-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:55cced7c52e981362f708ad635198e97a752dfba412cc03c23bbf3bd8d5cd662", size = 16847511, upload-time = "2026-05-18T23:36:50.673Z" }, + { url = "https://files.pythonhosted.org/packages/44/53/f481bef68011740f8849418d82db07230e825013f31f4eef5ba5b805316a/numpy-2.4.6-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:d6da64deb6b8ed903e7560180a92f2d804ee1ba5eeb849ac2748b8c1aba1f6d7", size = 14889064, upload-time = "2026-05-18T23:36:53.879Z" }, + { url = "https://files.pythonhosted.org/packages/7f/57/42ed575c10ced8af951d426bc4e1f8aff16fd851db33f067036215a7f860/numpy-2.4.6-pp311-pypy311_pp73-macosx_14_0_arm64.whl", hash = "sha256:68a5124b13fa6cc2086764a20005d30bc0548146f7f5322f02fce212ca14317f", size = 5394157, upload-time = "2026-05-18T23:36:57.194Z" }, + { url = "https://files.pythonhosted.org/packages/6a/ef/f66cc724fcc36c1e364c67f51ae9146090b8b584f27d58b97fdae3edd737/numpy-2.4.6-pp311-pypy311_pp73-macosx_14_0_x86_64.whl", hash = "sha256:948424b06129ce883307e8cff868c31396d8dc7630a59c61d70d98dbe70f222c", size = 6708728, upload-time = "2026-05-18T23:36:59.575Z" }, + { url = "https://files.pythonhosted.org/packages/1a/9c/c531f2293b91265d8b48e9b329f54fdd7ffae73cb4134ea10cca4237e9cc/numpy-2.4.6-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5dbbdb29840ca3d91ee0fece42fc29278886d908280bfec0a5846c6f901a3eb0", size = 15798374, upload-time = "2026-05-18T23:37:02.674Z" }, + { url = "https://files.pythonhosted.org/packages/1a/b0/413077f6b1153ed3cba361401c6783bbad6114804a000cc22eb71c13e190/numpy-2.4.6-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8ad03c0965fb3c692200e74d458ca28c1dbb4ce96f9a479a8aa041ad5fabca02", size = 16747286, upload-time = "2026-05-18T23:37:06.327Z" }, + { url = "https://files.pythonhosted.org/packages/15/ce/e5ec180bc41812edcd8daeb8639d205622c0e8c02259d8ab25a0201b3c2a/numpy-2.4.6-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:2803abfebfc990042cd494d8ce2d5f82e9d847af6d35ec486923aa19dbad5e73", size = 12504263, upload-time = "2026-05-18T23:37:09.715Z" }, +] + +[[package]] +name = "numpy" +version = "2.5.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.12'", +] +sdist = { url = "https://files.pythonhosted.org/packages/22/fd/89965aa4ac08c74998539fcbf24fa3540f3e15237fbeb6bcf9c908f4aade/numpy-2.5.1.tar.gz", hash = "sha256:a48a113e6afea91f5608793bafa7ef2ad481fefbda87ec5069f483de61cb9fa3", size = 20755553, upload-time = "2026-07-04T17:08:00.933Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/7b/14687aa674250e5e546f616f486b0d56d3631cd5b2415739141ce40bdcea/numpy-2.5.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2c889b56fe48b1018f764b0eec8df59ab654e9148aa91faa12596043500de277", size = 16801574, upload-time = "2026-07-04T17:06:12.423Z" }, + { url = "https://files.pythonhosted.org/packages/e1/19/cc5bb2a3f2913d27d6dbb2c78d25921fabaedc6741d4a5a615a11f3c5bf3/numpy-2.5.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ab451b59c5643c570974c43aef780703ef1d3b4965d2be07afd530615a9358d1", size = 11772250, upload-time = "2026-07-04T17:06:15.726Z" }, + { url = "https://files.pythonhosted.org/packages/42/77/fdf34a71dd30f54979b18603bee915e0aaf825b07afe79acd60b04b691e2/numpy-2.5.1-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:78798bd5b9ad744056af8efa90e3b9ddaa53272a0848a483084a1cc0a13b2dc0", size = 5331516, upload-time = "2026-07-04T17:06:17.913Z" }, + { url = "https://files.pythonhosted.org/packages/ce/e2/eb7efa015b4cce41e2517bf182a7fce0d7d5b9d9ed76a29bfa0f4fe4505c/numpy-2.5.1-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:2ae0ca40bcb22d6ba59c1dfd5446f49940b0f2d821fde133f10dda11f816b84e", size = 6664863, upload-time = "2026-07-04T17:06:20.02Z" }, + { url = "https://files.pythonhosted.org/packages/a9/4b/a2b32dd94ee9ffbeecb28152240042a3949db33b1c834d44090b80e1b3b8/numpy-2.5.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:61ac47e772e6b8ea489e1d2f441a34c5c3ac17327e7ce294cbdf535795ad4e75", size = 15167977, upload-time = "2026-07-04T17:06:21.621Z" }, + { url = "https://files.pythonhosted.org/packages/b8/a9/6e73d68500f80773f65f0654ea932019d6694329a0eb0ed0533de38df376/numpy-2.5.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:59fda5e192b570217ec2580c96f00e9a7e12ef6866a900eb089b62c1a32545ca", size = 16672469, upload-time = "2026-07-04T17:06:24.064Z" }, + { url = "https://files.pythonhosted.org/packages/24/7d/ad3e59015135f5261c95fd4cafeff159c955febd83a99a1d9250c4233815/numpy-2.5.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f7119ebff1a9829e9f431a4f9d28e703023bb6b9fe7c8f724467dbfc27c94ab3", size = 16527531, upload-time = "2026-07-04T17:06:26.69Z" }, + { url = "https://files.pythonhosted.org/packages/83/d0/a39b2fbcde9cb17a1dac678f254b33a6336298af9df338824c685425d5e8/numpy-2.5.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e824c2acf8862052246be5a44c15da1777940c60d010dd2aab897824d9c430f9", size = 18431940, upload-time = "2026-07-04T17:06:29.521Z" }, + { url = "https://files.pythonhosted.org/packages/04/12/cff070947791c1ed425ff76413189adbdc2fbe215eba7ce7fa454a03c7f8/numpy-2.5.1-cp312-cp312-win32.whl", hash = "sha256:08d60c810432eb83360958dea0999ac4cfb94531ea8efcbf0b7f277c2068aeb2", size = 6066764, upload-time = "2026-07-04T17:06:32.571Z" }, + { url = "https://files.pythonhosted.org/packages/65/66/53f31807a48a750f9d748da273bc3fcedd12b27ff1f3e373bfec55ef2dc0/numpy-2.5.1-cp312-cp312-win_amd64.whl", hash = "sha256:f7d60026c0bdb1380e83bfa7a0419c4577ee4b9a08880afcb6dadeb74c649fa2", size = 12430966, upload-time = "2026-07-04T17:06:34.926Z" }, + { url = "https://files.pythonhosted.org/packages/2b/2a/d1a88066b1c14186f5d3c0d18c94f17b064511982bab0578d49ee9d43c29/numpy-2.5.1-cp312-cp312-win_arm64.whl", hash = "sha256:17a25e09640602e10bc8de0e6fa2b3fd68eedd84ba6d7842dc8f32f9ab87bd0b", size = 10350488, upload-time = "2026-07-04T17:06:37.785Z" }, +] + +[[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, 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, upload-time = "2025-06-19T22:48:06.508Z" }, +] + +[[package]] +name = "onnxruntime" +version = "1.27.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "flatbuffers" }, + { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, + { name = "numpy", version = "2.5.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, + { name = "packaging" }, + { name = "protobuf" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/d4/e4/5353d7e09ced4a8f473f843223fc75d726b2b5519dcefc12f22a6c92852d/onnxruntime-1.27.0-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:8ba14a38c570087f3cdb8cfba33f7a38a1e826c1e5b29e17c28ceda0cc910016", size = 18416484, upload-time = "2026-06-15T22:43:43.894Z" }, + { url = "https://files.pythonhosted.org/packages/ed/1f/a2117aa3f144fce88774efa37440d0ca72d0c9144854dfc0961f2b04c6fc/onnxruntime-1.27.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2eb083321af8a236a84c7c140a7f4cecbfa2a987a18c07c78db471c20cd390ef", size = 16419330, upload-time = "2026-06-15T22:42:37.58Z" }, + { url = "https://files.pythonhosted.org/packages/e0/cd/74bb804170ceb622fda9111df31a07b3024f7491472256d3a90b5391a4d2/onnxruntime-1.27.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e4f7b0e90d2d212e2c2deaa6c8291616183ab815d3ec558ea12d3ac8b26d36f4", size = 18636930, upload-time = "2026-06-15T22:43:01.584Z" }, + { url = "https://files.pythonhosted.org/packages/fe/8f/5b8e2b85e81735696887175dbaf6409f215683f5ca9d4928fbb038211d32/onnxruntime-1.27.0-cp311-cp311-win_amd64.whl", hash = "sha256:ff050e4f6bf7f12918fa14dcb047c0b02e295f35e86d42532552be4b3d54e977", size = 13356110, upload-time = "2026-06-15T22:43:32.172Z" }, + { url = "https://files.pythonhosted.org/packages/b0/3a/4f568de678126b6a371a93862f015a82138359decd97fcac61fc84b5b774/onnxruntime-1.27.0-cp311-cp311-win_arm64.whl", hash = "sha256:75fbc1e1fb43a39a856c8209c544cca7817b5de7ac16b15b1bdf55d1cc67b9df", size = 13098635, upload-time = "2026-06-15T22:43:19.607Z" }, + { url = "https://files.pythonhosted.org/packages/c3/b7/dd3a524ed93a820dff1af902d0412957ab12499953333e9daa01af5bc480/onnxruntime-1.27.0-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:a14c2ce45312def86b77aea651f46565e45960cf5f0721bfdff449165086ab76", size = 18433506, upload-time = "2026-06-15T22:43:47.026Z" }, + { url = "https://files.pythonhosted.org/packages/84/86/c3b6b17745a1997d784dadc9bd88d713d2e6721139a5a0e885b28cfb79b1/onnxruntime-1.27.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c6fddce0539a4898c7bef35b052ffd37935b2190e35488eab99ce91887743ea1", size = 16438140, upload-time = "2026-06-15T22:42:40.666Z" }, + { url = "https://files.pythonhosted.org/packages/26/81/24dd9b31b0fb912ee19ca53ac1c9764bfd79d58a2ccef564eb693be831a5/onnxruntime-1.27.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7c65a7438632d55dfbc8a02ee60bd6cf7dd9d1ba05a43d4b851452f32338e194", size = 18658316, upload-time = "2026-06-15T22:43:04.012Z" }, + { url = "https://files.pythonhosted.org/packages/4f/88/8ec9db1a4d126bb8b758992beb40d1249df171917d75f44a327eb5f20dda/onnxruntime-1.27.0-cp312-cp312-win_amd64.whl", hash = "sha256:20c321cf187ba496e648acf6b4cf90b4d398b0d17c2a77fdaeba365b908cc1c1", size = 13358769, upload-time = "2026-06-15T22:43:34.581Z" }, + { url = "https://files.pythonhosted.org/packages/ae/9f/fdad359dfcba7e7cd8815569b304a596531d4efa77a75d77f8b4981891a2/onnxruntime-1.27.0-cp312-cp312-win_arm64.whl", hash = "sha256:d0d1f68868e2ef30ef70998ba9bbbc5c305e9b17041e3936751c1b8aa6aade06", size = 13104440, upload-time = "2026-06-15T22:43:22.893Z" }, +] + +[[package]] +name = "openai" +version = "2.45.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "distro" }, + { name = "httpx" }, + { name = "jiter" }, + { name = "pydantic" }, + { name = "sniffio" }, + { name = "tqdm" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/78/60/d4219875289b11d2c2f7da93c36283da224a2e55865ed865ab64e0ce9217/openai-2.45.0.tar.gz", hash = "sha256:10d34ca9c5643bce775852fddbfc172505cb1d4de1ccd101696c3ecff358765d", size = 1109653, upload-time = "2026-07-09T18:02:44.091Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f1/b0/2291689e3ec4723fbf5bbf3b54afcd7b160f9ddc98ca7aedfd0132af5677/openai-2.45.0-py3-none-any.whl", hash = "sha256:5df105f5f8c9b711fcb9d06d2d3888cebc82506db216484c14a4e53cdf651777", size = 1629470, upload-time = "2026-07-09T18:02:42.21Z" }, +] + +[[package]] +name = "openpyxl" +version = "3.1.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "et-xmlfile" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3d/f9/88d94a75de065ea32619465d2f77b29a0469500e99012523b91cc4141cd1/openpyxl-3.1.5.tar.gz", hash = "sha256:cf0e3cf56142039133628b5acffe8ef0c12bc902d2aadd3e0fe5878dc08d1050", size = 186464, upload-time = "2024-06-28T14:03:44.161Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c0/da/977ded879c29cbd04de313843e76868e6e13408a94ed6b987245dc7c8506/openpyxl-3.1.5-py2.py3-none-any.whl", hash = "sha256:5282c12b107bffeef825f4617dc029afaf41d0ea60823bbb665ef3079dc79de2", size = 250910, upload-time = "2024-06-28T14:03:41.161Z" }, +] + +[[package]] +name = "opentelemetry-api" +version = "1.42.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b4/1c/125e1c936c0873796771b7f04f6c93b9f1bf5d424cea90fda94a99f61da8/opentelemetry_api-1.42.1.tar.gz", hash = "sha256:56c63bea9f77b62856be8c47600474acad853b2924b99b1687c4cb6297166716", size = 72296, upload-time = "2026-05-21T16:32:49.335Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a3/ca/9520cc1f3dfbbd03ac5903bbf55833e257bc64b1cf30fa8b0d6df374d821/opentelemetry_api-1.42.1-py3-none-any.whl", hash = "sha256:51a69edacadbc03a8950ace1c4c21099cacc538820ac2c9e36277e78cebba714", size = 61311, upload-time = "2026-05-21T16:32:28.822Z" }, +] + +[[package]] +name = "opentelemetry-exporter-otlp-proto-common" +version = "1.42.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-proto" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0e/9c/216acfeaedadf2e1937f4373929b20f73197c5c4a2546d4f584b7fa63813/opentelemetry_exporter_otlp_proto_common-1.42.1.tar.gz", hash = "sha256:04f1f01fb597c4249dfcd7f8b861c902c2102369d376d9d346ff38de4469a2ee", size = 21433, upload-time = "2026-05-21T16:32:55.526Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d6/43/2375e7612e1121a4518c17603b6e0b03ad94f565aafad53f464dc5be2bf6/opentelemetry_exporter_otlp_proto_common-1.42.1-py3-none-any.whl", hash = "sha256:f48d395ab815b444da118868977e9798ea354c25737d5cf39578ae894011c140", size = 17327, upload-time = "2026-05-21T16:32:33.387Z" }, +] + +[[package]] +name = "opentelemetry-exporter-otlp-proto-grpc" +version = "1.42.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "googleapis-common-protos" }, + { name = "grpcio" }, + { name = "opentelemetry-api" }, + { name = "opentelemetry-exporter-otlp-proto-common" }, + { name = "opentelemetry-proto" }, + { name = "opentelemetry-sdk" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/87/87/ca7fc790dfdbcf4f9e9aab14a39ef1b7508ead13707e283de0b3131478d2/opentelemetry_exporter_otlp_proto_grpc-1.42.1.tar.gz", hash = "sha256:975c4461f167dd8ed8857d68d3b6b25f3d272eab896f6a9470d0f5b90e2faf15", size = 27140, upload-time = "2026-05-21T16:32:56.162Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/89/2b/28ba5b128f47fe8c3bab541000d6feb4b5a9bd26623ca013406f01c0fb60/opentelemetry_exporter_otlp_proto_grpc-1.42.1-py3-none-any.whl", hash = "sha256:0ae1177e2038b18a929b3098215243631ef91136cba26b7e2b12790ceb7e87cc", size = 19617, upload-time = "2026-05-21T16:32:34.278Z" }, +] + +[[package]] +name = "opentelemetry-exporter-otlp-proto-http" +version = "1.42.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "googleapis-common-protos" }, + { name = "opentelemetry-api" }, + { name = "opentelemetry-exporter-otlp-proto-common" }, + { name = "opentelemetry-proto" }, + { name = "opentelemetry-sdk" }, + { name = "requests" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/77/32/826bfa1d80ecea24f47808de03cd4a0d13c17ecc07712f45123f0f61e4ac/opentelemetry_exporter_otlp_proto_http-1.42.1.tar.gz", hash = "sha256:bf142a21035d7571ac3a09cb2e5639f49886f243972883cfe777ed3bf02b734d", size = 25406, upload-time = "2026-05-21T16:32:56.807Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d3/96/82cb223a1502f0787d4bbff12907f5f8d870a50731febcd5818d93ef9555/opentelemetry_exporter_otlp_proto_http-1.42.1-py3-none-any.whl", hash = "sha256:00a16da1b312a1d6c7233d600d557c91df71125af73020f3b9a7765bd699d59d", size = 21793, upload-time = "2026-05-21T16:32:35.277Z" }, +] + +[[package]] +name = "opentelemetry-proto" +version = "1.42.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "protobuf" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b4/55/63eac3e1089b768ba014091fdd2ae8a9a440c821ef5e2b786909c94c8836/opentelemetry_proto-1.42.1.tar.gz", hash = "sha256:c6a51e6b4f05ae63565f3a113217f3d2bfaec68f78c02d7a6c85f9010d1cfca6", size = 45839, upload-time = "2026-05-21T16:33:03.937Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/41/9d/171c02c84a76940b7e601805b3bb536985aded9168fbcc9ba52f0a730fa2/opentelemetry_proto-1.42.1-py3-none-any.whl", hash = "sha256:dedb74cba2886c59c7789b227a7a670613025a07489040050aedff6e5c0fb43c", size = 71782, upload-time = "2026-05-21T16:32:44.867Z" }, +] + +[[package]] +name = "opentelemetry-sdk" +version = "1.42.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-api" }, + { name = "opentelemetry-semantic-conventions" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/40/f7/b390bd9bfd703bf98a68fea1f27786c6872331fd617164a54b8a59bdc008/opentelemetry_sdk-1.42.1.tar.gz", hash = "sha256:8c834e8f8c9ba4171d4ec843d0cb8a67e4c7394d3f9e9297e582cbd9456ddbf7", size = 239262, upload-time = "2026-05-21T16:33:04.641Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8f/6b/4287766cfbde577ae2272e8884abac325aeaac0d64f41c61d5b8cc595105/opentelemetry_sdk-1.42.1-py3-none-any.whl", hash = "sha256:083cd4bbfaa5aa7b5a9e552430d9951219967cfb27aa61feb13a77aba1fc839d", size = 170907, upload-time = "2026-05-21T16:32:45.894Z" }, +] + +[[package]] +name = "opentelemetry-semantic-conventions" +version = "0.63b1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-api" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/93/99/4d7dd6df64795951413ce6e815f8cf1eb191daf7196ae86574589643d5f3/opentelemetry_semantic_conventions-0.63b1.tar.gz", hash = "sha256:3daf963611334b365e98a57438183eb012d3bfb40b2d931a9af613476b8701a9", size = 148340, upload-time = "2026-05-21T16:33:05.455Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/7a/7fe66f5f3682b1dd47d88cc4e11f1c6c0966b737de2d16671146e23c39a5/opentelemetry_semantic_conventions-0.63b1-py3-none-any.whl", hash = "sha256:dfe5ef4dee82586b746f522b818ceb298d00b3d59f660042bd79404bff8d0682", size = 203713, upload-time = "2026-05-21T16:32:47.016Z" }, +] + +[[package]] +name = "orjson" +version = "3.11.9" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7e/0c/964746fcafbd16f8ff53219ad9f6b412b34f345c75f384ad434ceaadb538/orjson-3.11.9.tar.gz", hash = "sha256:4fef17e1f8722c11587a6ef18e35902450221da0028e65dbaaa543619e68e48f", size = 5599163, upload-time = "2026-05-06T15:11:08.309Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/51/3fb9e65ae76ee97bd611869a503fa3fc0a6e81dd8b737cf3003f682df7ff/orjson-3.11.9-cp311-cp311-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:f01c4818b3fc9b0da8e096722a84318071eaa118df35f6ed2344da0e73a5444f", size = 228522, upload-time = "2026-05-06T15:09:35.362Z" }, + { url = "https://files.pythonhosted.org/packages/16/fa/9d54b07cb3f3b0bfd57841478e42d7a0ece4a9f49f9907eecf5a45461687/orjson-3.11.9-cp311-cp311-macosx_15_0_arm64.whl", hash = "sha256:3ebca4179031ee716ed076ffadc29428e900512f6fccee8614c9983157fcf19c", size = 128463, upload-time = "2026-05-06T15:09:37.063Z" }, + { url = "https://files.pythonhosted.org/packages/88/b1/6ceafc2eefd0a553e3be77ce6c49d107e772485d9568629376171c50e634/orjson-3.11.9-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:48ee05097750de0ff69ed5b7bbcf0732182fd57a24043dcc2a1da780a5ead3a5", size = 132306, upload-time = "2026-05-06T15:09:38.299Z" }, + { url = "https://files.pythonhosted.org/packages/ea/76/f11311285324a40aab1e3031385c50b635a7cd0734fdaf60c7e89a696f60/orjson-3.11.9-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a6082706765a95a6680d812e1daf1c0cfe8adec7831b3ff3b625693f3b461b1c", size = 127988, upload-time = "2026-05-06T15:09:39.597Z" }, + { url = "https://files.pythonhosted.org/packages/9e/85/0ef63bcf1337f44031ce9b91b1919563f62a37527b3ea4368bb15a22e5d7/orjson-3.11.9-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:277fefe9d76ee17eb14debf399e3533d4d63b5f677a4d3719eb763536af1f4bd", size = 135188, upload-time = "2026-05-06T15:09:40.957Z" }, + { url = "https://files.pythonhosted.org/packages/05/94/b0d27090ea8a2095db3c2bd1b1c96f96f19bbb494d7fef33130e846e613d/orjson-3.11.9-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:03db380e3780fa0015ed776a90f20e8e20bb11dde13b216ce19e5718e3dfba62", size = 145937, upload-time = "2026-05-06T15:09:42.249Z" }, + { url = "https://files.pythonhosted.org/packages/09/eb/75d50c29c05b8054013e221e598820a365c8e64065312e75e202ed880709/orjson-3.11.9-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:33d7d766701847dc6729846362dc27895d2f2d2251264f9d10e7cb9878194877", size = 132758, upload-time = "2026-05-06T15:09:43.945Z" }, + { url = "https://files.pythonhosted.org/packages/49/bd/360686f39348aa88827cb6fbf7dc606fd41c831a35235e1abf1db8e3a9e6/orjson-3.11.9-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:147302878da387104b66bb4a8b0227d1d487e976ce41a8501916161072ed87b1", size = 133971, upload-time = "2026-05-06T15:09:45.239Z" }, + { url = "https://files.pythonhosted.org/packages/0e/30/3178eb16f3221aeef068b6f1f1ebe05f656ea5c6dffe9f6c917329fe17a3/orjson-3.11.9-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:3513550321f8c8c811a7c3297b8a630e82dc08e4c10216d07703c997776236cd", size = 141685, upload-time = "2026-05-06T15:09:46.858Z" }, + { url = "https://files.pythonhosted.org/packages/5f/f1/ff2f19ed0225f9680fafa42febca3570dd59444ebf190980738d376214c2/orjson-3.11.9-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:c5d001196b89fa9cf0a4ab79766cd835b991a166e4b621ba95089edc50c429ff", size = 415167, upload-time = "2026-05-06T15:09:48.312Z" }, + { url = "https://files.pythonhosted.org/packages/9b/61/863bddf0da6e9e586765414debd54b4e58db05f560902b6d00658cb88636/orjson-3.11.9-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:16969c9d369c98eb084889c6e4d2d39b77c7eb38ceccf8da2a9fff62ae908980", size = 147913, upload-time = "2026-05-06T15:09:49.733Z" }, + { url = "https://files.pythonhosted.org/packages/b6/8a/4081492586d75b073d60c5271a8d0f05a0955cabf1e34c8473f6fcd84235/orjson-3.11.9-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:63e0efbc991250c0b3143488fa57d95affcabbfc63c99c48d625dd37779aafe2", size = 136959, upload-time = "2026-05-06T15:09:51.311Z" }, + { url = "https://files.pythonhosted.org/packages/0d/bd/70b6ab193594d7abb875320c0a7c8335e846f28968c432c31042409c3c8d/orjson-3.11.9-cp311-cp311-win32.whl", hash = "sha256:14ed654580c1ed2bc217352ec82f91b047aef82951aa71c7f64e0dcb03c0e180", size = 131533, upload-time = "2026-05-06T15:09:52.637Z" }, + { url = "https://files.pythonhosted.org/packages/3f/17/1a1a228183d62d1b77e2c30d210f47dd4768b310ebe1607c63e3c0e3a71e/orjson-3.11.9-cp311-cp311-win_amd64.whl", hash = "sha256:57ea77fb70a448ce87d18fca050193202a3da5e54598f6501ca5476fb66cfe02", size = 127106, upload-time = "2026-05-06T15:09:54.204Z" }, + { url = "https://files.pythonhosted.org/packages/b8/95/285de5fa296d09681ee9c546cd4a8aeb773b701cf343dc125994f4d52953/orjson-3.11.9-cp311-cp311-win_arm64.whl", hash = "sha256:19b72ed11572a2ee51a67a903afbe5af504f84ed6f529c0fe44b0ab3fb5cc697", size = 126848, upload-time = "2026-05-06T15:09:55.551Z" }, + { url = "https://files.pythonhosted.org/packages/16/6d/11867a3ffa3a3608d84a4de51ef4dd0896d6b5cc9132fbe1daf593e677bc/orjson-3.11.9-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:9ef6fe90aadef185c7b128859f40beb24720b4ecea95379fc9000931179c3a49", size = 228515, upload-time = "2026-05-06T15:09:57.265Z" }, + { url = "https://files.pythonhosted.org/packages/24/75/05912954c8b288f34fcf5cd4b9b071cb4f6e77b9961e175e56ebb258089f/orjson-3.11.9-cp312-cp312-macosx_15_0_arm64.whl", hash = "sha256:e5c9b8f28e726e97d97696c826bc7bea5d71cecd63576dba92924a32c1961291", size = 128409, upload-time = "2026-05-06T15:09:59.063Z" }, + { url = "https://files.pythonhosted.org/packages/ab/86/1c3a47df3bc8191ea9ac51603bbb872a95167a364320c269f2557911f406/orjson-3.11.9-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:26a473dbb4162108b27901492546f83c76fdcea3d0eadff00ae7a07e18dcce09", size = 132106, upload-time = "2026-05-06T15:10:00.798Z" }, + { url = "https://files.pythonhosted.org/packages/d7/cf/b33b5f3e695ae7d63feef9d915c37cc3b8f465493dcd4f8e0b4c697a2366/orjson-3.11.9-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:011382e2a60fda9d46f1cdee31068cfc52ffe952b587d683ec0463002802a0f4", size = 127864, upload-time = "2026-05-06T15:10:02.15Z" }, + { url = "https://files.pythonhosted.org/packages/31/6a/6cf69385a58208024fcb8c014e2141b8ce838aba6492b589f8acfff97fab/orjson-3.11.9-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c2d3dc759490128c5c1711a53eeaa8ee1d437fd0038ffd2b6008abf46db3f882", size = 135213, upload-time = "2026-05-06T15:10:03.515Z" }, + { url = "https://files.pythonhosted.org/packages/e8/f8/0b1bd3e8f2efcdd376af5c8cfd79eaf13f018080c0089c80ebd724e3c7fb/orjson-3.11.9-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d8ea516b3726d190e1b4297e6f4e7a8650347ae053868a18163b4dd3641d1fff", size = 145994, upload-time = "2026-05-06T15:10:05.083Z" }, + { url = "https://files.pythonhosted.org/packages/f3/59/dab79f61044c529d2c81aecdc589b1f833a1c8dec11ba3b1c2498a02ca7e/orjson-3.11.9-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:380cdce7ba24989af81d0a7013d0aaec5d0e2a21734c0e2681b1bc4f141957fe", size = 132744, upload-time = "2026-05-06T15:10:06.853Z" }, + { url = "https://files.pythonhosted.org/packages/0e/a4/82b7a2fe5d8a67a59ed831b24d59a3d46ea7d207b66e1602d376541d94a6/orjson-3.11.9-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:be4fa4f0af7fa18951f7ab3fc2148e223af211bf03f59e1c6034ec3f97f21d61", size = 134014, upload-time = "2026-05-06T15:10:08.213Z" }, + { url = "https://files.pythonhosted.org/packages/50/c7/375e83a76851b73b2e39f3bcf0e5a19e2b89bad13e5bca97d0b293d27f24/orjson-3.11.9-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a8f5f8bc7ce7d59f08d9f99fa510c06496164a24cb5f3d34537dbd9ca30132e2", size = 141509, upload-time = "2026-05-06T15:10:09.595Z" }, + { url = "https://files.pythonhosted.org/packages/7f/7c/49d5d82a3d3097f641f094f552131f1e2723b0b8cb0fa2874ab65ecfffa6/orjson-3.11.9-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:4d7fde5501b944f83b3e665e1b31343ff6e154b15560a16b7130ea1e594a4206", size = 415127, upload-time = "2026-05-06T15:10:11.049Z" }, + { url = "https://files.pythonhosted.org/packages/3a/dc/7446c538590d55f455647e5f3c61fc33f7108714e7afcffa6a2a033f8350/orjson-3.11.9-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:cde1a448023ba7d5bb4c01c5afb48894380b5e4956e0627266526587ef4e535f", size = 148025, upload-time = "2026-05-06T15:10:12.842Z" }, + { url = "https://files.pythonhosted.org/packages/df/e5/4d2d8af06f788329b4f78f8cc3679bb395392fcaa1e4d8d3c33e85308fa4/orjson-3.11.9-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:71e63adb0e1f1ed5d9e168f50a91ceb93ae6420731d222dc7da5c69409aa47aa", size = 136943, upload-time = "2026-05-06T15:10:14.405Z" }, + { url = "https://files.pythonhosted.org/packages/06/69/850264ccf6d80f6b174620d30a87f65c9b1490aba33fe6b62798e618cad3/orjson-3.11.9-cp312-cp312-win32.whl", hash = "sha256:2d057a602cdd19a0ad680417527c45b6961a095081c0f46fe0e03e304aac6470", size = 131606, upload-time = "2026-05-06T15:10:15.791Z" }, + { url = "https://files.pythonhosted.org/packages/b9/d5/973a43fc9c55e20f2051e9830997649f669be0cb3ca52192087c0143f118/orjson-3.11.9-cp312-cp312-win_amd64.whl", hash = "sha256:59e403b1cc5a676da8eaf31f6254801b7341b3e29efa85f92b48d272637e77be", size = 127101, upload-time = "2026-05-06T15:10:17.129Z" }, + { url = "https://files.pythonhosted.org/packages/fe/ae/495470f0e4a18f73fa10b7f6b84b464ec4cc5291c4e0c7c2a6c400bef006/orjson-3.11.9-cp312-cp312-win_arm64.whl", hash = "sha256:9af678d6488357948f1f84c6cd1c1d397c014e1ae2f98ae082a44eb48f602624", size = 126736, upload-time = "2026-05-06T15:10:18.645Z" }, +] + +[[package]] +name = "overrides" +version = "7.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/36/86/b585f53236dec60aba864e050778b25045f857e17f6e5ea0ae95fe80edd2/overrides-7.7.0.tar.gz", hash = "sha256:55158fa3d93b98cc75299b1e67078ad9003ca27945c76162c1c0766d6f91820a", size = 22812, upload-time = "2024-01-27T21:01:33.423Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/ab/fc8290c6a4c722e5514d80f62b2dc4c4df1a68a41d1364e625c35990fcf3/overrides-7.7.0-py3-none-any.whl", hash = "sha256:c7ed9d062f78b8e4c1a7b70bd8796b35ead4d9f510227ef9c5dc7626c60d7e49", size = 17832, upload-time = "2024-01-27T21:01:31.393Z" }, +] + +[[package]] +name = "packaging" +version = "26.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d7/f1/e7a6dd94a8d4a5626c03e4e99c87f241ba9e350cd9e6d75123f992427270/packaging-26.2.tar.gz", hash = "sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661", size = 228134, upload-time = "2026-04-24T20:15:23.917Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e", size = 100195, upload-time = "2026-04-24T20:15:22.081Z" }, +] + +[[package]] +name = "pdfminer-six" +version = "20260107" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "charset-normalizer" }, + { name = "cryptography" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/34/a4/5cec1112009f0439a5ca6afa8ace321f0ab2f48da3255b7a1c8953014670/pdfminer_six-20260107.tar.gz", hash = "sha256:96bfd431e3577a55a0efd25676968ca4ce8fd5b53f14565f85716ff363889602", size = 8512094, upload-time = "2026-01-07T13:29:12.937Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/8b/28c4eaec9d6b036a52cb44720408f26b1a143ca9bce76cc19e8f5de00ab4/pdfminer_six-20260107-py3-none-any.whl", hash = "sha256:366585ba97e80dffa8f00cebe303d2f381884d8637af4ce422f1df3ef38111a9", size = 6592252, upload-time = "2026-01-07T13:29:10.742Z" }, +] + +[[package]] +name = "pdfplumber" +version = "0.11.10" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pdfminer-six" }, + { name = "pillow" }, + { name = "pypdfium2" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/05/56/6f450312ba05a27d7713b73857c1a25100dbda04fbc1331b13fb227a607d/pdfplumber-0.11.10.tar.gz", hash = "sha256:b95b2d28c66efb0a794a83b88c6c6aea5987532a445d20a1cbcfa657022e6e57", size = 102892, upload-time = "2026-06-15T03:31:31.035Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a2/9a/07d658e1e7fad860f1c541ab941348125dbdab773be3a0afaf32361866c7/pdfplumber-0.11.10-py3-none-any.whl", hash = "sha256:7741ea81bf165b474b153e6789d10d18e06b6ddcf3ec84289c3ef2fed6802580", size = 60047, upload-time = "2026-06-15T03:31:29.702Z" }, +] + +[[package]] +name = "pendulum" +version = "3.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "python-dateutil" }, + { name = "tzdata" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cb/72/9a51afa0a822b09e286c4cb827ed7b00bc818dac7bd11a5f161e493a217d/pendulum-3.2.0.tar.gz", hash = "sha256:e80feda2d10fa3ff8b1526715f7d33dcb7e08494b3088f2c8a3ac92d4a4331ce", size = 86912, upload-time = "2026-01-30T11:22:24.093Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c4/27/a4be6ec12161b503dd036f8d7cc57f8626170ae31bb298038be9af0001ce/pendulum-3.2.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:5d775cc608c909ad415c8e789c84a9f120bb6a794c4215b2d8d910893cf0ec6a", size = 337923, upload-time = "2026-01-30T11:20:51.61Z" }, + { url = "https://files.pythonhosted.org/packages/59/e1/2a214e18355ec2a6ce3f683a97eecdb6050866ff3a6cf165d411450aeb1b/pendulum-3.2.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8de794a7f665aebc8c1ba4dd4b05ab8fe1a36ce9c0498366adf1d1edd79b2686", size = 327379, upload-time = "2026-01-30T11:20:53.085Z" }, + { url = "https://files.pythonhosted.org/packages/9d/01/7392e58ebc1d9e70b987dc8bb0c89710b47ac8125067efe7aa4c420b616f/pendulum-3.2.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7bac7df7696e1c942e17c0556b3a7bcdd1d7aa5b24faee7620cb071e754a0622", size = 340115, upload-time = "2026-01-30T11:20:54.635Z" }, + { url = "https://files.pythonhosted.org/packages/ef/33/80de84c5ca1a3e4f7f3b75090c9b61b6dbb6d095e302ee592cebbaf0bbfb/pendulum-3.2.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:db0f6a8a04475d9cba26ce701e7d66d266fd97227f2f5f499270eba04be1c7e9", size = 373969, upload-time = "2026-01-30T11:20:56.209Z" }, + { url = "https://files.pythonhosted.org/packages/75/e4/f7b4c1818927ab394a2a0a9b7011f360a0a75839a22678833c5bc0a84183/pendulum-3.2.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c352c63c1ff05f2198409b28498d7158547a8be23e1fbd4aa2cf5402fb239b55", size = 379058, upload-time = "2026-01-30T11:20:57.618Z" }, + { url = "https://files.pythonhosted.org/packages/36/94/9947cf710620afcc68751683f2f8de88d902505e7c13c0349d7e9d362f97/pendulum-3.2.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:de8c1ad1d1aa7d4ceae341528bab35a0f8c88a5aa63f2f5d84e16b517d1b32c2", size = 348403, upload-time = "2026-01-30T11:20:59.56Z" }, + { url = "https://files.pythonhosted.org/packages/6f/12/0e6ba0bb00fa57907af2a3fca8643bded5dba1e87072d50673776a0d6ed2/pendulum-3.2.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:1ba955511c12fec2252038b0c866c25c0c30b720bf74d3023710f121e42b1498", size = 517457, upload-time = "2026-01-30T11:21:01.602Z" }, + { url = "https://files.pythonhosted.org/packages/c6/fe/dae5fbfe67bd41d943def0ad8f1e7f6988aa8e527255e433cd7c494f9ad5/pendulum-3.2.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:4115bf364a2ec6d5ddc476751ceaa4164a04f2c15589f0d29aa210ddb784b15d", size = 561103, upload-time = "2026-01-30T11:21:03.924Z" }, + { url = "https://files.pythonhosted.org/packages/ce/a0/8f646160b98abfc19152505af19bd643a4279ec2bdbe0959f16b7025fc6b/pendulum-3.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:4151a903356413fdd9549de0997b708fb95a214ed97803ffb479ffd834088378", size = 260595, upload-time = "2026-01-30T11:21:05.495Z" }, + { url = "https://files.pythonhosted.org/packages/79/01/feead7af9ded7a13f2d798fb6573e70f469113eafcd8cc8f59671584ca3e/pendulum-3.2.0-cp311-cp311-win_arm64.whl", hash = "sha256:acfdee9ddc56053cb7c8c075afbfde0857322d09e56a56195b9cd127fae87e4c", size = 255382, upload-time = "2026-01-30T11:21:06.847Z" }, + { url = "https://files.pythonhosted.org/packages/41/56/dd0ea9f97d25a0763cda09e2217563b45714786118d8c68b0b745395d6eb/pendulum-3.2.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:bf0b489def51202a39a2a665dcc4162d5e46934a740fe4c4fe3068979610156c", size = 337830, upload-time = "2026-01-30T11:21:08.298Z" }, + { url = "https://files.pythonhosted.org/packages/cf/98/83d62899bf7226fc12396de4bc1fb2b5da27e451c7c60790043aaf8b4731/pendulum-3.2.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:937a529aa302efa18dcf25e53834964a87ffb2df8f80e3669ab7757a6126beaf", size = 327574, upload-time = "2026-01-30T11:21:09.715Z" }, + { url = "https://files.pythonhosted.org/packages/76/fa/ff2aa992b23f0543c709b1a3f3f9ed760ec71fd02c8bb01f93bf008b52e4/pendulum-3.2.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:85c7689defc65c4dc29bf257f7cca55d210fabb455de9476e1748d2ab2ae80d7", size = 339891, upload-time = "2026-01-30T11:21:11.089Z" }, + { url = "https://files.pythonhosted.org/packages/c5/4e/25b4fa11d19503d50d7b52d7ef943c0f20fd54422aaeb9e38f588c815c50/pendulum-3.2.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d5e216e5a412563ea2ecf5de467dcf3d02717947fcdabe6811d5ee360726b02b", size = 373726, upload-time = "2026-01-30T11:21:12.493Z" }, + { url = "https://files.pythonhosted.org/packages/4f/30/0acad6396c4e74e5c689aa4f0b0c49e2ecdcfce368e7b5bf35ca1c0fc61a/pendulum-3.2.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3a2af22eeec438fbaac72bb7fba783e0950a514fba980d9a32db394b51afccec", size = 379827, upload-time = "2026-01-30T11:21:14.08Z" }, + { url = "https://files.pythonhosted.org/packages/3a/f7/e6a2fdf2a23d59b4b48b8fa89e8d4bf2dd371aea2c6ba8fcecec20a4acb9/pendulum-3.2.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3159cceb54f5aa8b85b141c7f0ce3fac8bdd1ffdc7c79e67dca9133eac7c4d11", size = 348921, upload-time = "2026-01-30T11:21:15.816Z" }, + { url = "https://files.pythonhosted.org/packages/7f/f2/c15fa7f9ad4e181aa469b6040b574988bd108ccdf4ae509ad224f9e4db44/pendulum-3.2.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c39ea5e9ffa20ea8bae986d00e0908bd537c8468b71d6b6503ab0b4c3d76e0ea", size = 517188, upload-time = "2026-01-30T11:21:17.835Z" }, + { url = "https://files.pythonhosted.org/packages/47/c7/5f80b12ee88ec26e930c3a5a602608a63c29cf60c81a0eb066d583772550/pendulum-3.2.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:e5afc753e570cce1f44197676371f68953f7d4f022303d141bb09f804d5fe6d7", size = 561833, upload-time = "2026-01-30T11:21:19.232Z" }, + { url = "https://files.pythonhosted.org/packages/90/15/1ac481626cb63db751f6281e294661947c1f0321ebe5d1c532a3b51a8006/pendulum-3.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:fd55c12560816d9122ca2142d9e428f32c0c083bf77719320b1767539c7a3a3b", size = 258725, upload-time = "2026-01-30T11:21:20.558Z" }, + { url = "https://files.pythonhosted.org/packages/40/ae/50b0398d7d027eb70a3e1e336de7b6e599c6b74431cb7d3863287e1292bb/pendulum-3.2.0-cp312-cp312-win_arm64.whl", hash = "sha256:faef52a7ed99729f0838353b956f3fabf6c550c062db247e9e2fc2b48fcb9457", size = 253089, upload-time = "2026-01-30T11:21:22.497Z" }, + { url = "https://files.pythonhosted.org/packages/c9/0d/d5ac8468a1b40f09a62d6e91654088de432367907579dd161c0fb1bdf222/pendulum-3.2.0-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:9585594d32faa71efa5a78f576f1ee4f79e9c5340d7c6f0cd6c5dfe725effaaa", size = 338760, upload-time = "2026-01-30T11:22:12.225Z" }, + { url = "https://files.pythonhosted.org/packages/a0/e5/7fa8c8be6caac8e0be78fbe7668df571f44820ed779cb3736fab645fcba8/pendulum-3.2.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:26401e2de77c437e8f3b6160c08c6c5d45518d906f8f9b48fd7cb5aa0f4e2aff", size = 328333, upload-time = "2026-01-30T11:22:13.811Z" }, + { url = "https://files.pythonhosted.org/packages/ad/78/73a1031b7d1bf7986e8e655cea3f018164b3470aecfea25a4074e77dda73/pendulum-3.2.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:637e65af042f383a2764a886aa28ccc6f853bf7a142df18e41c720542934c13b", size = 340841, upload-time = "2026-01-30T11:22:15.278Z" }, + { url = "https://files.pythonhosted.org/packages/49/40/4e36e9074e92b0164c088b9ada3c02bfea386d83e24fa98b30fe9b6e61a8/pendulum-3.2.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d6e46c28f4d067233c4a4c42748f4ffa641d9289c09e0e81488beb6d4b3fab51", size = 348959, upload-time = "2026-01-30T11:22:16.718Z" }, + { url = "https://files.pythonhosted.org/packages/24/99/8bf7fcb91b526e1efe17d047faa845709b88800fff915ff848ff26054293/pendulum-3.2.0-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:71d46bcc86269f97bfd8c5f1475d55e717696a0a010b1871023605ca94624031", size = 518102, upload-time = "2026-01-30T11:22:18.2Z" }, + { url = "https://files.pythonhosted.org/packages/b8/b0/a36c468d2d0dec62ddea7c5e4177e93abb12f48ac90f09f24d0581c5189f/pendulum-3.2.0-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:5cd956d4176afc7bfe8a91bf3f771b46ff8d326f6c5bf778eb5010eb742ebba6", size = 561884, upload-time = "2026-01-30T11:22:19.671Z" }, + { url = "https://files.pythonhosted.org/packages/c5/4d/dad105261898907bf806cabca53d3878529a9fa2c0d5d7f95f2035246fc2/pendulum-3.2.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:39ef129d7b90aab49708645867abdd207b714ba7bff12dae549975b0aca09716", size = 261236, upload-time = "2026-01-30T11:22:21.059Z" }, + { url = "https://files.pythonhosted.org/packages/02/fb/d65db067a67df7252f18b0cb7420dda84078b9e8bfb375215469c14a50be/pendulum-3.2.0-py3-none-any.whl", hash = "sha256:f3a9c18a89b4d9ef39c5fa6a78722aaff8d5be2597c129a3b16b9f40a561acf3", size = 114111, upload-time = "2026-01-30T11:22:22.361Z" }, +] + +[[package]] +name = "pillow" +version = "12.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1c/3d/bb7fca845737cf9d7dbde16ed1843984665ff2e0a518f5db43e77ec540b9/pillow-12.3.0.tar.gz", hash = "sha256:3b8182a766685eaa002637e28b4ec8d6b18819a0c71f579bf0dbaa5830297cce", size = 47025035, upload-time = "2026-07-01T11:56:38.965Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fb/c8/0a78b0e02d7ac54bc03e5321c9220da52f0c2ea83b21f7c40e7f3169c502/pillow-12.3.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:00808c5e14ef63ac5161091d242999076604ff74b883423a11e5d7bbb38bf756", size = 5392415, upload-time = "2026-07-01T11:53:47.162Z" }, + { url = "https://files.pythonhosted.org/packages/b2/5b/a02d30018abd97ced9f5a6c63d28597694a00d066516b9c1c6de45859fc9/pillow-12.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:37d6d0a00072fd2948eb22bce7e1475f34569d90c87c59f7a2ec59541b77f7a6", size = 4785266, upload-time = "2026-07-01T11:53:49.079Z" }, + { url = "https://files.pythonhosted.org/packages/c8/98/766667a4be768150a202836acd9fad19c06824ca86c4286d3cf6b274964e/pillow-12.3.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bcb46e2f9feff8d06323983bd83ed00c201fdcab3d74973e7072a889b3979fcd", size = 6263814, upload-time = "2026-07-01T11:53:51.32Z" }, + { url = "https://files.pythonhosted.org/packages/3b/2d/ede717bc1144f63886c21fd349bb95860b0d1a21149ff16f2bb362b612b6/pillow-12.3.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:23d27a3e0307ec2244cc51e7287b919aa68d097504ebe19df4e76a98a3eea5bd", size = 6934408, upload-time = "2026-07-01T11:53:53.487Z" }, + { url = "https://files.pythonhosted.org/packages/a3/48/9c58b685e69d49c31af6c8eb9012055fab7e665785165c84796e2c73ce72/pillow-12.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:4f883547d4b7f0495ebe7056b0cc2aea76094e7a4abc8e933540f3271df27d9c", size = 6337160, upload-time = "2026-07-01T11:53:55.457Z" }, + { url = "https://files.pythonhosted.org/packages/ff/fa/dc2a5c0ba6df93f67c31d34b808b7ce440b40cdbf96f0b81cde1d1e6fa93/pillow-12.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:236ff70b9312fb68943c703aa842ca6a758abfa45ac187a5e7c1452e96ef72b5", size = 7045172, upload-time = "2026-07-01T11:53:57.736Z" }, + { url = "https://files.pythonhosted.org/packages/86/a5/444817a4d4c4c2417df00513086ca196f388d8f9ef40c2e4ccd1ad1af54b/pillow-12.3.0-cp311-cp311-win32.whl", hash = "sha256:10e41f0fbf1eec8cfd234b8fe17a4caac7c9d0db4c204d3c173a8f9f6ef3232b", size = 6472232, upload-time = "2026-07-01T11:53:59.767Z" }, + { url = "https://files.pythonhosted.org/packages/63/c6/4bad1b18d132a50b27e1365e1ab163616f7a5bb56d330f66f9d1d9d4f9d4/pillow-12.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:8e95e1385e4998ae9694eeaa4730ba5457ff61185b3a55e2e7bea0880aef452a", size = 7233653, upload-time = "2026-07-01T11:54:02.066Z" }, + { url = "https://files.pythonhosted.org/packages/fd/16/00f91ab7760dc842f5aad55217e80fc4a7067a0604535249bc8a2d6d9870/pillow-12.3.0-cp311-cp311-win_arm64.whl", hash = "sha256:ebaea975e03d3141d9d3a507df75c9b3ec90fa9d2ffd07567b3a978d9d790b26", size = 2568195, upload-time = "2026-07-01T11:54:04.622Z" }, + { url = "https://files.pythonhosted.org/packages/37/bf/fb3ebff8ddcb76aac5a01389251bbbb9519922a9b520d8247c1ca864a25d/pillow-12.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ba09209fbe443b4acccebe845d8a138b89a8f4fbaeedd44953490b5315d5e965", size = 5345969, upload-time = "2026-07-01T11:54:06.397Z" }, + { url = "https://files.pythonhosted.org/packages/d8/66/9a386a92561f402389a4fc70c18838bf6d35eb5eb5c6850b4b2dc64f5048/pillow-12.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ffd0c5368496f41b0944be820fcb7a838aa6e623d250b01acf2643939c3f99d7", size = 4780323, upload-time = "2026-07-01T11:54:09.351Z" }, + { url = "https://files.pythonhosted.org/packages/25/27/ac8f99618ffd3dde21db0f4d4b1d2ab00c0880595bfd17df103f7f39fd0c/pillow-12.3.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d9c7f76c0673154f044e9d78c8655fb4213f6ca31a836df48b40fe5d187717b9", size = 6266838, upload-time = "2026-07-01T11:54:11.71Z" }, + { url = "https://files.pythonhosted.org/packages/84/21/a35af28dcc61f37ed850a2d64c65c701321dfbf25085e469d5559360cbbf/pillow-12.3.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:78cb2c6865a35ab8ff8b75fd122f6033b92a62c82801110e48ddd6c936a45d91", size = 6940830, upload-time = "2026-07-01T11:54:13.732Z" }, + { url = "https://files.pythonhosted.org/packages/eb/51/8b08617af3ad95e33ce6d7dd2c99ed6c8298f7fb131636303956be022e25/pillow-12.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e491916b378fba47242221bb9ead245211b70d504f495d105d17b14a24b4907c", size = 6344383, upload-time = "2026-07-01T11:54:15.756Z" }, + { url = "https://files.pythonhosted.org/packages/1d/72/cf78ac9780bb93c28328f408973845a309d4d145041665f734572ced1b52/pillow-12.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0dd2064cbc55aaec028ef5fbb60fa47bb6c3e7918e07ff17935284b227a9d2df", size = 7052934, upload-time = "2026-07-01T11:54:17.721Z" }, + { url = "https://files.pythonhosted.org/packages/20/20/25e0f4dc178a6bc0696793720055519a0de89e7661dae886992decbd2f81/pillow-12.3.0-cp312-cp312-win32.whl", hash = "sha256:dbce0b29841537a2fa4a214c2bbf14de3587c9680caa9b4e217568472490b28f", size = 6472684, upload-time = "2026-07-01T11:54:19.839Z" }, + { url = "https://files.pythonhosted.org/packages/45/89/da2f7971a317f83d807fdd4065c0af40208e59e692cc43d315a71a0e96d1/pillow-12.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:a2b55dd6b2a4c4b7d87ffa56bdb33fdc5fdb9a462173861a7bc097f17d91cb09", size = 7227137, upload-time = "2026-07-01T11:54:22.025Z" }, + { url = "https://files.pythonhosted.org/packages/de/47/4845a0a6c0dbf1db8456bd9fc791f13c5ced7ced20606d08a0aacfd25b49/pillow-12.3.0-cp312-cp312-win_arm64.whl", hash = "sha256:331b624368d4f1d069149002f25f44bc61c8919ce8ddb3c45bdad8f6e2d89510", size = 2568267, upload-time = "2026-07-01T11:54:24.051Z" }, + { url = "https://files.pythonhosted.org/packages/75/18/2e8b40223153ccbc60df07f9e8928dc0c76202aa4e55ae9f53962b6510d6/pillow-12.3.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:b3c777e849237620b022f7f297dd67705f9f5cf1685f09f02e46f93e92725468", size = 5302510, upload-time = "2026-07-01T11:56:25.736Z" }, + { url = "https://files.pythonhosted.org/packages/46/3e/51fabf59d5ab801ceab709453d3ab6b180083496579549de4c45ced6528a/pillow-12.3.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:b343699e8308bdc51978310e1c959c584e7869cc8c40780058c87da7781a1e94", size = 4736058, upload-time = "2026-07-01T11:56:28.041Z" }, + { url = "https://files.pythonhosted.org/packages/bf/20/22fe9384b7949e25fb1293bcfc84fb82590ff4ea6b37c95b24d26d793d86/pillow-12.3.0-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fbd139c8447d25dd750ab79ee274cc5e1fe80fc56340ab10b18a195e1b6eca3e", size = 5237776, upload-time = "2026-07-01T11:56:30.263Z" }, + { url = "https://files.pythonhosted.org/packages/08/14/f6ba68107680ffa74b39985f3f30884e41318fbc4250caa423c79b4788bb/pillow-12.3.0-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e7e480451b9fa137494bccd3a7d69adbe8ac65a87d97be61e11f1b1050a5bac3", size = 5860358, upload-time = "2026-07-01T11:56:32.68Z" }, + { url = "https://files.pythonhosted.org/packages/36/54/0169bc772ec491108b62f644f8ecf1fe5d8ae5ebafde2ee2142210166903/pillow-12.3.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:04f01d28a6aaff387bf842a13be313df23ba0597a44f1a976c9feb3c6ff4711a", size = 7231786, upload-time = "2026-07-01T11:56:35.046Z" }, +] + +[[package]] +name = "platformdirs" +version = "4.10.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d7/47/e4501f49c178ae1d9f4a75073fda4204f52647993f075a9db4d14930e0c5/platformdirs-4.10.0.tar.gz", hash = "sha256:31e761a6a0ca04faf7353ea759bdba55652be214725111e5aac52dfa29d4bef7", size = 31224, upload-time = "2026-05-28T03:32:53.587Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/81/e6/cd9575ac904136b3cbf7aa7ee819ef86eedb7274e46f230e94ea4342e729/platformdirs-4.10.0-py3-none-any.whl", hash = "sha256:fb516cdb12eb0d857d0cd85a7c57cea4d060bee4578d6cf5a14dfdf8cbf8784a", size = 22743, upload-time = "2026-05-28T03:32:52.175Z" }, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, +] + +[[package]] +name = "portalocker" +version = "2.7.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pywin32", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1f/f8/969e6f280201b40b31bcb62843c619f343dcc351dff83a5891530c9dd60e/portalocker-2.7.0.tar.gz", hash = "sha256:032e81d534a88ec1736d03f780ba073f047a06c478b06e2937486f334e955c51", size = 20183, upload-time = "2023-01-18T23:36:14.436Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8c/df/d4f711d168524f5aebd7fb30969eaa31e3048cf8979688cde3b08f6e5eb8/portalocker-2.7.0-py2.py3-none-any.whl", hash = "sha256:a07c5b4f3985c3cf4798369631fb7011adb498e2a46d8440efc75a8f29a0f983", size = 15502, upload-time = "2023-01-18T23:36:12.849Z" }, +] + +[[package]] +name = "posthog" +version = "5.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "backoff" }, + { name = "distro" }, + { name = "python-dateutil" }, + { name = "requests" }, + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/48/20/60ae67bb9d82f00427946218d49e2e7e80fb41c15dc5019482289ec9ce8d/posthog-5.4.0.tar.gz", hash = "sha256:701669261b8d07cdde0276e5bc096b87f9e200e3b9589c5ebff14df658c5893c", size = 88076, upload-time = "2025-06-20T23:19:23.485Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4f/98/e480cab9a08d1c09b1c59a93dade92c1bb7544826684ff2acbfd10fcfbd4/posthog-5.4.0-py3-none-any.whl", hash = "sha256:284dfa302f64353484420b52d4ad81ff5c2c2d1d607c4e2db602ac72761831bd", size = 105364, upload-time = "2025-06-20T23:19:22.001Z" }, +] + +[[package]] +name = "propcache" +version = "0.5.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ec/44/c87281c333769159c50594f22610f77398a47ccbfbbf23074e744e86f87c/propcache-0.5.2.tar.gz", hash = "sha256:01c4fc7480cd0598bb4b57022df55b9ca296da7fc5a8760bd8451a7e63a7d427", size = 50208, upload-time = "2026-05-08T21:02:12.199Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e7/f1/8a8cc1c2c7e7934ab77e0163414f736fadbc0f5e8dd9673b952355ac175b/propcache-0.5.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:74b70780220e2dd89175ca24b81b68b67c83db499ae611e7f2313cb329801c78", size = 90744, upload-time = "2026-05-08T20:59:45.799Z" }, + { url = "https://files.pythonhosted.org/packages/c2/f4/651b1225e976bd1a2ba5cfba0c29d096581c2636b437e3a9a7ab6276270a/propcache-0.5.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a4840ab0ae0216d952f4b53dc6d0b992bfc2bedbfe360bdd9b548bc184c08959", size = 52033, upload-time = "2026-05-08T20:59:47.408Z" }, + { url = "https://files.pythonhosted.org/packages/15/a8/8ede85d6aa1f79fc7dc2f8fd2c8d65920b8272c3892903c8a1affde48cfb/propcache-0.5.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c6844ba6364fb12f403928a82cfd295ab103a2b315c77c747b2dbe4a41894ea7", size = 52754, upload-time = "2026-05-08T20:59:49.202Z" }, + { url = "https://files.pythonhosted.org/packages/7d/fe/b3551b41bbc2f5b5bb088fc6920567cd43101253e68fbaa261339eb96fe1/propcache-0.5.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2293949b855ce597f2826452d17c2d545fb5622379c4ea6fdf525e9b8e8a2511", size = 57573, upload-time = "2026-05-08T20:59:50.778Z" }, + { url = "https://files.pythonhosted.org/packages/83/27/ab851ebd1b7172e3e161f5f8d39e315d54a91bea246f01f4d872d3376aef/propcache-0.5.2-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0fd59b5af35f74da48d905dcbad55449ba13be91823cb05a9bd590bbf5b61660", size = 60645, upload-time = "2026-05-08T20:59:52.227Z" }, + { url = "https://files.pythonhosted.org/packages/95/7d/466b3d18022e9897cbda9c735c493c5bd747d7a4c6f5ea1480b4cec434b6/propcache-0.5.2-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:29f9309a2e42b0d273be006fdb4be2d6c39a47f6f57d8fb1cf9f81481df81b66", size = 61563, upload-time = "2026-05-08T20:59:53.866Z" }, + { url = "https://files.pythonhosted.org/packages/27/1b/16ab7f2cf2041da2f60d156ba64c2484eadf9168075b4ff43c3ef60045af/propcache-0.5.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5aaa2b923c1944ac8febd6609cb373540a5563e7cbcb0fd770f75dace2eb817b", size = 58888, upload-time = "2026-05-08T20:59:55.457Z" }, + { url = "https://files.pythonhosted.org/packages/0a/67/bb777ffd907633563bf35fd859c4ce97b0512c32f4633cf5d1eb7c33512b/propcache-0.5.2-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:66ea454f095ddf5b6b14f56c064c0941c4788be11e18d2464cf643bf7203ff67", size = 59253, upload-time = "2026-05-08T20:59:57.075Z" }, + { url = "https://files.pythonhosted.org/packages/b9/42/64f8d90b73fd9cdc1499b48057ff6d9cd2a98a25734c9bb62ecf07e87061/propcache-0.5.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:95f1e3f4760d404b13c9976c0229b2b49a3c8e2c62a9ce92efdd2b11ada75e3f", size = 57558, upload-time = "2026-05-08T20:59:58.602Z" }, + { url = "https://files.pythonhosted.org/packages/eb/02/dba5bc03c9041f2092ea55a449caf5dfe68352c6654511b29ba0654ddb69/propcache-0.5.2-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:85341b12b9d55bad0bded24cac341bb34289469e03a11f3f583ea1cc1db0326c", size = 55007, upload-time = "2026-05-08T20:59:59.837Z" }, + { url = "https://files.pythonhosted.org/packages/14/c0/43f649c7aa2a77a3b100d84e9dea3a483120ecb608bfe36ce49eaff517fe/propcache-0.5.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:26a4dca084132874e639895c3135dfad5eb20bae209f62d1aeb31b03e601c3c0", size = 60355, upload-time = "2026-05-08T21:00:01.144Z" }, + { url = "https://files.pythonhosted.org/packages/83/c0/435dafd27f1cb4a495381dae60e25883ccfe4020bb72818e8184c1678092/propcache-0.5.2-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:3b199b9b2b3d6a7edf3183ba8a9a137a22b97f7df525feb5ae1eccf026d2a9c6", size = 59057, upload-time = "2026-05-08T21:00:02.401Z" }, + { url = "https://files.pythonhosted.org/packages/53/ae/6e292df9135d659944e96cb3389258e4a663e5b2b5f6c217ef0ddc8d2f73/propcache-0.5.2-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:e59bc9e66329185b93dab73f210f1a37f81cb40f321501db8017c9aea15dba27", size = 61938, upload-time = "2026-05-08T21:00:03.638Z" }, + { url = "https://files.pythonhosted.org/packages/0b/42/314ebc50d8159055411fd6b0bda322ff510e4b1f7d2e4927940ad0f6af20/propcache-0.5.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:552ffadf6ad409844bc5919c42a0a83d88314cedddaea0e41e80a8b8fffe881f", size = 59731, upload-time = "2026-05-08T21:00:04.881Z" }, + { url = "https://files.pythonhosted.org/packages/b8/9b/2da6dee38871c3c8772fabc2758325a5c9077d6d18c597737dc04dd884cd/propcache-0.5.2-cp311-cp311-win32.whl", hash = "sha256:cd416c1de191973c52ff1a12a57446bfc7642797b282d7caf2162d7d1b8aa9a0", size = 38966, upload-time = "2026-05-08T21:00:06.511Z" }, + { url = "https://files.pythonhosted.org/packages/42/4e/f17363fb58c0afe05b067361cb6d86ed2d29de6506779a27547c4d183075/propcache-0.5.2-cp311-cp311-win_amd64.whl", hash = "sha256:44e488ef40dbb452700b2b1f8188934121f6648f52c295055662d2191959ff82", size = 42135, upload-time = "2026-05-08T21:00:08.088Z" }, + { url = "https://files.pythonhosted.org/packages/c6/eb/6af6685077d22e8b33358d3c548e3282706a0b3cd85044ffba4e5dd08e3b/propcache-0.5.2-cp311-cp311-win_arm64.whl", hash = "sha256:54adaa85a22078d1e306304a40984dc5be99d599bf3dc0a24dc98f7daeab89ab", size = 38381, upload-time = "2026-05-08T21:00:09.692Z" }, + { url = "https://files.pythonhosted.org/packages/4a/cb/e27bc2b2737a0bb49962b275efa051e8f1c35a936df7d5139b6b658b7dc9/propcache-0.5.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:806719138ecd720339a12410fb9614ac9b2b2d3a5fdf8235d56981c36f4039ba", size = 95887, upload-time = "2026-05-08T21:00:11.277Z" }, + { url = "https://files.pythonhosted.org/packages/e6/13/b8ae04c59392f8d11c6cd9fb4011d1dc7c86b81225c770280300e259ffe1/propcache-0.5.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:db2b80ea58eab4f86b2beec3cc8b39e8ff9276ac20e96b7cce43c8ae84cd6b5a", size = 54654, upload-time = "2026-05-08T21:00:12.604Z" }, + { url = "https://files.pythonhosted.org/packages/2c/7d/49777a3e20b55863d4794384a38acd460c04157b0a00f8602b0d508b8431/propcache-0.5.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:e5cbfac9f61484f7e9f3597775500cd3ebe8274e9b050c38f9525c77c97520bf", size = 55190, upload-time = "2026-05-08T21:00:13.935Z" }, + { url = "https://files.pythonhosted.org/packages/44/c7/085d0cd63062e84044e3f05797749c3f8e3938ff3aeb0eb2f69d43fafc91/propcache-0.5.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5dbc581d2814337da56222fab8dc5f161cd798a434e49bac27930aaef798e144", size = 59995, upload-time = "2026-05-08T21:00:15.526Z" }, + { url = "https://files.pythonhosted.org/packages/9c/42/32cf8e3009e92b2645cf1e944f701e8ea4e924dffde1ee26db860bcbf7e4/propcache-0.5.2-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:857187f381f88c8e2fa2fe56ab94879d011b883d5a2ee5a1b60a8cd2a06846d9", size = 63422, upload-time = "2026-05-08T21:00:16.824Z" }, + { url = "https://files.pythonhosted.org/packages/9e/1b/f112433f99fc979431b87a39ef169e3f8df070d99a72792c56d6937ac48b/propcache-0.5.2-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:178b4a2cdaac1818e2bf1c5a99b94383fa73ea5382e032a48dec07dc5668dc42", size = 64342, upload-time = "2026-05-08T21:00:18.362Z" }, + { url = "https://files.pythonhosted.org/packages/14/15/5574111ae50dd6e879456888c0eadd4c5a869959775854e18e18a6b345f3/propcache-0.5.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6f328175a2cde1f0ff2c4ed8ce968b9dcfb55f3a7153f39e2957ed994da13476", size = 61639, upload-time = "2026-05-08T21:00:19.692Z" }, + { url = "https://files.pythonhosted.org/packages/cc/da/4d775080b1490c0ae604acda868bd71aabe3a89ed16f2aa4339eb8a283e7/propcache-0.5.2-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5671d09a36b06d0fd4a3da0fccbcae360e9b1570924171a15e9e0997f0249fba", size = 61588, upload-time = "2026-05-08T21:00:21.155Z" }, + { url = "https://files.pythonhosted.org/packages/04/ac/f076982cbe2195ee9cf32de5a1e46951d9fb399fc207f390562dd0fd8fb2/propcache-0.5.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:80168e2ebe4d3ec6599d10ad8f520304ae1cad9b6c5a95372aef1b66b7bfb53a", size = 60029, upload-time = "2026-05-08T21:00:22.713Z" }, + { url = "https://files.pythonhosted.org/packages/70/60/189be62e0dd898dce3b331e1b8c7a543cd3a405ac0c81fe8ee8a9d5d77e1/propcache-0.5.2-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:45f11346f884bc47444f6e6647131055844134c3175b629f84952e2b5cd62b64", size = 56774, upload-time = "2026-05-08T21:00:24.001Z" }, + { url = "https://files.pythonhosted.org/packages/ea/9e/93377b9c7939c1ffae98f878dee955efadfd638078bc86dbc21f9d52f651/propcache-0.5.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:8e778ebd44ef4f66ed60a0416b06b489687db264a9c0b3620362f26489492913", size = 63532, upload-time = "2026-05-08T21:00:25.545Z" }, + { url = "https://files.pythonhosted.org/packages/14/f9/590ef6cfb9b8028d516d287812ece32bb0bc5f11fbb9c8bf6b2e6313fec8/propcache-0.5.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:c0cb9ed24c8964e172768d455a38254c2dd8a552905729ce006cad3d3dda59b1", size = 61592, upload-time = "2026-05-08T21:00:27.186Z" }, + { url = "https://files.pythonhosted.org/packages/b4/5e/70958b3034c297a630bba2f17ca7abc2d5f39a803ad7e370ab79d1ecd022/propcache-0.5.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:1d1ad32d9d4355e2be65574fd0bfd3677e7066b009cd5b9b2dee8aa6a6393b33", size = 64788, upload-time = "2026-05-08T21:00:28.8Z" }, + { url = "https://files.pythonhosted.org/packages/12/fd/77fe5936d8c3086ca9048f7f415f122ed82e53884a9ec193646b42deef06/propcache-0.5.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c80f4ba3e8f00189165999a742ee526ebeccedf6c3f7beb0c7df821e9772435a", size = 62514, upload-time = "2026-05-08T21:00:30.098Z" }, + { url = "https://files.pythonhosted.org/packages/cf/74/66bd798b5b3be70aa1b391f5cc9d6a0a5532d7fd3b19ec0b213e72e6ad9d/propcache-0.5.2-cp312-cp312-win32.whl", hash = "sha256:8c7972d8f193740d9175f0998ab38717e6cd322d5935c5b0fef8c0d323fd9031", size = 39018, upload-time = "2026-05-08T21:00:31.622Z" }, + { url = "https://files.pythonhosted.org/packages/61/7c/5c0d34aa3024694d6dcb9271cdbdd08c4e47c1c0ad95ec7e7bc74cdea145/propcache-0.5.2-cp312-cp312-win_amd64.whl", hash = "sha256:d9ee8826a7d47863a08ac44e1a5f611a462eefc3a194b492da242128bec75b42", size = 42322, upload-time = "2026-05-08T21:00:32.918Z" }, + { url = "https://files.pythonhosted.org/packages/4d/91/875812f1a3feb20ceba818ef39fbe4d92f1081e04ac815c822496d0d038b/propcache-0.5.2-cp312-cp312-win_arm64.whl", hash = "sha256:2800a4a8ead6b28cccd1ec54b59346f0def7922ee1c7598e8499c733cfbb7c84", size = 38172, upload-time = "2026-05-08T21:00:35.124Z" }, + { url = "https://files.pythonhosted.org/packages/3a/ed/1cdcab6ba3d6ab7feca11fc14f0eeea80755bb53ef4e892079f31b10a25f/propcache-0.5.2-py3-none-any.whl", hash = "sha256:be1ddfcbb376e3de5d2e2db1d58d6d67463e6b4f9f040c000de8e300295465fe", size = 14036, upload-time = "2026-05-08T21:02:10.673Z" }, +] + +[[package]] +name = "proto-plus" +version = "1.28.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "protobuf" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/87/44/767757fd2cdd4a60d7e4440d9f7b491d6131103d313638d2c03e06c268fb/proto_plus-1.28.1.tar.gz", hash = "sha256:832e68e7fe064cf90ab153b6e5eb935b27891bb89aaeb68b115e9b702f6cb168", size = 57166, upload-time = "2026-07-08T17:04:02.367Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6a/34/2f2b57dbfd145b995a29847a16b0903fce5ef6ad3c7aad740a609c5d3678/proto_plus-1.28.1-py3-none-any.whl", hash = "sha256:6660f5f1970874bdcfc3088b435188a36a37bd3596668f7d726417c4ae8cfbed", size = 50408, upload-time = "2026-07-08T17:03:34.532Z" }, +] + +[[package]] +name = "protobuf" +version = "6.33.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/66/70/e908e9c5e52ef7c3a6c7902c9dfbb34c7e29c25d2f81ade3856445fd5c94/protobuf-6.33.6.tar.gz", hash = "sha256:a6768d25248312c297558af96a9f9c929e8c4cee0659cb07e780731095f38135", size = 444531, upload-time = "2026-03-18T19:05:00.988Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fc/9f/2f509339e89cfa6f6a4c4ff50438db9ca488dec341f7e454adad60150b00/protobuf-6.33.6-cp310-abi3-win32.whl", hash = "sha256:7d29d9b65f8afef196f8334e80d6bc1d5d4adedb449971fefd3723824e6e77d3", size = 425739, upload-time = "2026-03-18T19:04:48.373Z" }, + { url = "https://files.pythonhosted.org/packages/76/5d/683efcd4798e0030c1bab27374fd13a89f7c2515fb1f3123efdfaa5eab57/protobuf-6.33.6-cp310-abi3-win_amd64.whl", hash = "sha256:0cd27b587afca21b7cfa59a74dcbd48a50f0a6400cfb59391340ad729d91d326", size = 437089, upload-time = "2026-03-18T19:04:50.381Z" }, + { url = "https://files.pythonhosted.org/packages/5c/01/a3c3ed5cd186f39e7880f8303cc51385a198a81469d53d0fdecf1f64d929/protobuf-6.33.6-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:9720e6961b251bde64edfdab7d500725a2af5280f3f4c87e57c0208376aa8c3a", size = 427737, upload-time = "2026-03-18T19:04:51.866Z" }, + { url = "https://files.pythonhosted.org/packages/ee/90/b3c01fdec7d2f627b3a6884243ba328c1217ed2d978def5c12dc50d328a3/protobuf-6.33.6-cp39-abi3-manylinux2014_aarch64.whl", hash = "sha256:e2afbae9b8e1825e3529f88d514754e094278bb95eadc0e199751cdd9a2e82a2", size = 324610, upload-time = "2026-03-18T19:04:53.096Z" }, + { url = "https://files.pythonhosted.org/packages/9b/ca/25afc144934014700c52e05103c2421997482d561f3101ff352e1292fb81/protobuf-6.33.6-cp39-abi3-manylinux2014_s390x.whl", hash = "sha256:c96c37eec15086b79762ed265d59ab204dabc53056e3443e702d2681f4b39ce3", size = 339381, upload-time = "2026-03-18T19:04:54.616Z" }, + { url = "https://files.pythonhosted.org/packages/16/92/d1e32e3e0d894fe00b15ce28ad4944ab692713f2e7f0a99787405e43533a/protobuf-6.33.6-cp39-abi3-manylinux2014_x86_64.whl", hash = "sha256:e9db7e292e0ab79dd108d7f1a94fe31601ce1ee3f7b79e0692043423020b0593", size = 323436, upload-time = "2026-03-18T19:04:55.768Z" }, + { url = "https://files.pythonhosted.org/packages/c4/72/02445137af02769918a93807b2b7890047c32bfb9f90371cbc12688819eb/protobuf-6.33.6-py3-none-any.whl", hash = "sha256:77179e006c476e69bf8e8ce866640091ec42e1beb80b213c3900006ecfba6901", size = 170656, upload-time = "2026-03-18T19:04:59.826Z" }, +] + +[[package]] +name = "pyarrow" +version = "25.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/27/f3/95428098d1fa7d04432fb750eed06b41304c2f6a5d3319985e64db2d9d41/pyarrow-25.0.0.tar.gz", hash = "sha256:d2d697008b5ec06d75952ef260c2e9a8a0f6ccfce24266c04c9c8ade927cb3b4", size = 1199181, upload-time = "2026-07-10T08:29:50.116Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a7/98/ae2b5acf9876dbeffa6f320776242c52caab062df55c8ac5501ed2679e74/pyarrow-25.0.0-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:2e3b6544e26e393fe2cd530f523e36c1c8d3c345bbbb60cca3fd866be8322517", size = 35939080, upload-time = "2026-07-10T08:26:04.53Z" }, + { url = "https://files.pythonhosted.org/packages/80/09/3de2a968edbd496c86cb8b932cdbee2d4b08c4a28e9884a15e5c705a646b/pyarrow-25.0.0-cp311-cp311-macosx_12_0_x86_64.whl", hash = "sha256:b724d127783b4c19f088fcdfc844cbc318809246a30307bcabd5ed02045e890e", size = 37633420, upload-time = "2026-07-10T08:26:10.354Z" }, + { url = "https://files.pythonhosted.org/packages/19/86/8399243a4ce080426ec37db18d5e29148b7ec960a8a8c7f9059a7bf6ef0a/pyarrow-25.0.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:244f98a595f70fa4fd35faa7508c4ae67e14a173397a4b3b49d2b3c360fb0062", size = 46861050, upload-time = "2026-07-10T08:26:16.397Z" }, + { url = "https://files.pythonhosted.org/packages/7b/79/72d704b02bc5fc6d06954d76a0208c1e79cad3ab370f6d6a91ffe5078870/pyarrow-25.0.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:0222f0071d13313962a88d21bf28b80d355ac39d81bfa6ff3fe00eeaf748e4be", size = 50056458, upload-time = "2026-07-10T08:26:23.271Z" }, + { url = "https://files.pythonhosted.org/packages/06/5d/3c31a60b6403d63cad2e0f829096f5fc5763a129ead4207a5d4690b96448/pyarrow-25.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b58726f118c079f9d4ed7e904975d4f15fd69d0741ba511a4e2dcaa4ef16354f", size = 49957793, upload-time = "2026-07-10T08:26:30.232Z" }, + { url = "https://files.pythonhosted.org/packages/34/f7/8f8a019061f9863a831915329264372a87ed25eaf9109ce56eb0e84012c5/pyarrow-25.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:38a2c887cb3883e241b70201688db34133b6dfadd04f03c8f9213df53770c18e", size = 53100544, upload-time = "2026-07-10T08:26:36.414Z" }, + { url = "https://files.pythonhosted.org/packages/f1/e2/738071e95c5ddad7b3dfc12f569ffa992db89d7d7b4a95258fd184191249/pyarrow-25.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:161649d60a7a46c613a19fd795763ea8a88c36ba997dd99d9bc66e6794ee36e8", size = 27848311, upload-time = "2026-07-10T08:26:41.429Z" }, + { url = "https://files.pythonhosted.org/packages/73/44/fdd3a4377807b7dcabe2d4b5aa99dbbc98e2e5df3f1ca4e7f0aec492d987/pyarrow-25.0.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:149730a3d1f0fb59d663a0b8aa210adfd9c17c27cd94a0d143e60daea8320d4e", size = 35850884, upload-time = "2026-07-10T08:26:47.357Z" }, + { url = "https://files.pythonhosted.org/packages/bf/71/9f053177a7709b8c90abb00a2375b916286f9f0d6cfb21a5cadd4ef811e8/pyarrow-25.0.0-cp312-cp312-macosx_12_0_x86_64.whl", hash = "sha256:0721332c30fdd453fdd1fc203b2ac1f4c9db5aea28fa38d41f2574c4b068b9ec", size = 37616197, upload-time = "2026-07-10T08:26:53.564Z" }, + { url = "https://files.pythonhosted.org/packages/95/1a/22bfb6597dcdc861fa83c39c06e1457cb56f698940eff42fbb25de30e8e5/pyarrow-25.0.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:fa1482b3da10cac2d4db6e26b81da543e237616af2ef6d466018b31ca586496f", size = 46841966, upload-time = "2026-07-10T08:27:07.685Z" }, + { url = "https://files.pythonhosted.org/packages/55/0e/cd705c042bc4fe7022478db577fcab4abdcfabb9bc37ab7a75556b3fcb2b/pyarrow-25.0.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:5d1dbf24e151042f2fa3c129563f65d66674128868496fb008c4272b16bdf778", size = 50088993, upload-time = "2026-07-10T08:27:14.268Z" }, + { url = "https://files.pythonhosted.org/packages/98/ee/d822e1ee31fe31ec5d057210e0605c950b975dcd8d9a332976cc859a9df8/pyarrow-25.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:20887a762dd61dcc530f93a140840ab1f6aa7836b33270e42d627ab3cf11e537", size = 49941005, upload-time = "2026-07-10T08:27:21.274Z" }, + { url = "https://files.pythonhosted.org/packages/33/1b/207a90cc64619a095eb75a263ae069735f2810056d43c667befd573ec083/pyarrow-25.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:58d1ab556b0cea1c93fdb799b24ad58adb2f2a2788dbce782a94f64ae1a5cc9b", size = 53112355, upload-time = "2026-07-10T08:27:27.911Z" }, + { url = "https://files.pythonhosted.org/packages/7e/fe/81d1e5f8beed15c01e98649d5c6e2167b67fd395884a2488f18bf1cf0dba/pyarrow-25.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:3f356afe61186395c861d5cd63dc21ff7d5fa335012a4668d979257df7fea0f5", size = 27945954, upload-time = "2026-07-10T08:27:32.903Z" }, +] + +[[package]] +name = "pyasn1" +version = "0.6.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a4/9a/23310166d960def5897e91fe20e5b724601b02a22e84ba1f94232c0b7f67/pyasn1-0.6.4.tar.gz", hash = "sha256:9c447d8431c947fe4c8febc4ed9e760bc29011a5b01e5c74b67025bd9fb8ce81", size = 151262, upload-time = "2026-07-09T01:12:33.988Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9a/3b/6163796d69c3977d1e4287bea4a6979161cbbdd170ebb430511e8e1999ce/pyasn1-0.6.4-py3-none-any.whl", hash = "sha256:deda9277cfd454080ec40b207fb6df82206a3a2688735233cdcd8d3d565f088b", size = 84410, upload-time = "2026-07-09T01:12:32.92Z" }, +] + +[[package]] +name = "pyasn1-modules" +version = "0.4.2" +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, 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, upload-time = "2025-03-28T02:41:19.028Z" }, +] + +[[package]] +name = "pybase64" +version = "1.4.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/aa/b8/4ed5c7ad5ec15b08d35cc79ace6145d5c1ae426e46435f4987379439dfea/pybase64-1.4.3.tar.gz", hash = "sha256:c2ed274c9e0ba9c8f9c4083cfe265e66dd679126cd9c2027965d807352f3f053", size = 137272, upload-time = "2025-12-06T13:27:04.013Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2b/63/21e981e9d3f1f123e0b0ee2130112b1956cad9752309f574862c7ae77c08/pybase64-1.4.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:70b0d4a4d54e216ce42c2655315378b8903933ecfa32fced453989a92b4317b2", size = 38237, upload-time = "2025-12-06T13:22:52.159Z" }, + { url = "https://files.pythonhosted.org/packages/92/fb/3f448e139516404d2a3963915cc10dc9dde7d3a67de4edba2f827adfef17/pybase64-1.4.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8127f110cdee7a70e576c5c9c1d4e17e92e76c191869085efbc50419f4ae3c72", size = 31673, upload-time = "2025-12-06T13:22:53.241Z" }, + { url = "https://files.pythonhosted.org/packages/3c/fb/bb06a5b9885e7d853ac1e801c4d8abfdb4c8506deee33e53d55aa6690e67/pybase64-1.4.3-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:f9ef0388878bc15a084bd9bf73ec1b2b4ee513d11009b1506375e10a7aae5032", size = 68331, upload-time = "2025-12-06T13:22:54.197Z" }, + { url = "https://files.pythonhosted.org/packages/64/15/8d60b9ec5e658185fc2ee3333e01a6e30d717cf677b24f47cbb3a859d13c/pybase64-1.4.3-cp311-cp311-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:95a57cccf106352a72ed8bc8198f6820b16cc7d55aa3867a16dea7011ae7c218", size = 71370, upload-time = "2025-12-06T13:22:55.517Z" }, + { url = "https://files.pythonhosted.org/packages/ac/29/a3e5c1667cc8c38d025a4636855de0fc117fc62e2afeb033a3c6f12c6a22/pybase64-1.4.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cd1c47dfceb9c7bd3de210fb4e65904053ed2d7c9dce6d107f041ff6fbd7e21", size = 59834, upload-time = "2025-12-06T13:22:56.682Z" }, + { url = "https://files.pythonhosted.org/packages/a9/00/8ffcf9810bd23f3984698be161cf7edba656fd639b818039a7be1d6405d4/pybase64-1.4.3-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.whl", hash = "sha256:9fe9922698f3e2f72874b26890d53a051c431d942701bb3a37aae94da0b12107", size = 56652, upload-time = "2025-12-06T13:22:57.724Z" }, + { url = "https://files.pythonhosted.org/packages/81/62/379e347797cdea4ab686375945bc77ad8d039c688c0d4d0cfb09d247beb9/pybase64-1.4.3-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:af5f4bd29c86b59bb4375e0491d16ec8a67548fa99c54763aaedaf0b4b5a6632", size = 59382, upload-time = "2025-12-06T13:22:58.758Z" }, + { url = "https://files.pythonhosted.org/packages/c6/f2/9338ffe2f487086f26a2c8ca175acb3baa86fce0a756ff5670a0822bb877/pybase64-1.4.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:c302f6ca7465262908131411226e02100f488f531bb5e64cb901aa3f439bccd9", size = 59990, upload-time = "2025-12-06T13:23:01.007Z" }, + { url = "https://files.pythonhosted.org/packages/f9/a4/85a6142b65b4df8625b337727aa81dc199642de3d09677804141df6ee312/pybase64-1.4.3-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:2f3f439fa4d7fde164ebbbb41968db7d66b064450ab6017c6c95cef0afa2b349", size = 54923, upload-time = "2025-12-06T13:23:02.369Z" }, + { url = "https://files.pythonhosted.org/packages/ac/00/e40215d25624012bf5b7416ca37f168cb75f6dd15acdb91ea1f2ea4dc4e7/pybase64-1.4.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7a23c6866551043f8b681a5e1e0d59469148b2920a3b4fc42b1275f25ea4217a", size = 58664, upload-time = "2025-12-06T13:23:03.378Z" }, + { url = "https://files.pythonhosted.org/packages/b0/73/d7e19a63e795c13837f2356268d95dc79d1180e756f57ced742a1e52fdeb/pybase64-1.4.3-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:56e6526f8565642abc5f84338cc131ce298a8ccab696b19bdf76fa6d7dc592ef", size = 52338, upload-time = "2025-12-06T13:23:04.458Z" }, + { url = "https://files.pythonhosted.org/packages/f2/32/3c746d7a310b69bdd9df77ffc85c41b80bce00a774717596f869b0d4a20e/pybase64-1.4.3-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:6a792a8b9d866ffa413c9687d9b611553203753987a3a582d68cbc51cf23da45", size = 68993, upload-time = "2025-12-06T13:23:05.526Z" }, + { url = "https://files.pythonhosted.org/packages/5d/b3/63cec68f9d6f6e4c0b438d14e5f1ef536a5fe63ce14b70733ac5e31d7ab8/pybase64-1.4.3-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:62ad29a5026bb22cfcd1ca484ec34b0a5ced56ddba38ceecd9359b2818c9c4f9", size = 58055, upload-time = "2025-12-06T13:23:06.931Z" }, + { url = "https://files.pythonhosted.org/packages/d5/cb/7acf7c3c06f9692093c07f109668725dc37fb9a3df0fa912b50add645195/pybase64-1.4.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:11b9d1d2d32ec358c02214363b8fc3651f6be7dd84d880ecd597a6206a80e121", size = 54430, upload-time = "2025-12-06T13:23:07.936Z" }, + { url = "https://files.pythonhosted.org/packages/33/39/4eb33ff35d173bfff4002e184ce8907f5d0a42d958d61cd9058ef3570179/pybase64-1.4.3-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:0aebaa7f238caa0a0d373616016e2040c6c879ebce3ba7ab3c59029920f13640", size = 56272, upload-time = "2025-12-06T13:23:09.253Z" }, + { url = "https://files.pythonhosted.org/packages/19/97/a76d65c375a254e65b730c6f56bf528feca91305da32eceab8bcc08591e6/pybase64-1.4.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:e504682b20c63c2b0c000e5f98a80ea867f8d97642e042a5a39818e44ba4d599", size = 70904, upload-time = "2025-12-06T13:23:10.336Z" }, + { url = "https://files.pythonhosted.org/packages/5e/2c/8338b6d3da3c265002839e92af0a80d6db88385c313c73f103dfb800c857/pybase64-1.4.3-cp311-cp311-win32.whl", hash = "sha256:e9a8b81984e3c6fb1db9e1614341b0a2d98c0033d693d90c726677db1ffa3a4c", size = 33639, upload-time = "2025-12-06T13:23:11.9Z" }, + { url = "https://files.pythonhosted.org/packages/39/dc/32efdf2f5927e5449cc341c266a1bbc5fecd5319a8807d9c5405f76e6d02/pybase64-1.4.3-cp311-cp311-win_amd64.whl", hash = "sha256:a90a8fa16a901fabf20de824d7acce07586e6127dc2333f1de05f73b1f848319", size = 35797, upload-time = "2025-12-06T13:23:13.174Z" }, + { url = "https://files.pythonhosted.org/packages/da/59/eda4f9cb0cbce5a45f0cd06131e710674f8123a4d570772c5b9694f88559/pybase64-1.4.3-cp311-cp311-win_arm64.whl", hash = "sha256:61d87de5bc94d143622e94390ec3e11b9c1d4644fe9be3a81068ab0f91056f59", size = 31160, upload-time = "2025-12-06T13:23:15.696Z" }, + { url = "https://files.pythonhosted.org/packages/86/a7/efcaa564f091a2af7f18a83c1c4875b1437db56ba39540451dc85d56f653/pybase64-1.4.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:18d85e5ab8b986bb32d8446aca6258ed80d1bafe3603c437690b352c648f5967", size = 38167, upload-time = "2025-12-06T13:23:16.821Z" }, + { url = "https://files.pythonhosted.org/packages/db/c7/c7ad35adff2d272bf2930132db2b3eea8c44bb1b1f64eb9b2b8e57cde7b4/pybase64-1.4.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3f5791a3491d116d0deaf4d83268f48792998519698f8751efb191eac84320e9", size = 31673, upload-time = "2025-12-06T13:23:17.835Z" }, + { url = "https://files.pythonhosted.org/packages/43/1b/9a8cab0042b464e9a876d5c65fe5127445a2436da36fda64899b119b1a1b/pybase64-1.4.3-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:f0b3f200c3e06316f6bebabd458b4e4bcd4c2ca26af7c0c766614d91968dee27", size = 68210, upload-time = "2025-12-06T13:23:18.813Z" }, + { url = "https://files.pythonhosted.org/packages/62/f7/965b79ff391ad208b50e412b5d3205ccce372a2d27b7218ae86d5295b105/pybase64-1.4.3-cp312-cp312-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:bb632edfd132b3eaf90c39c89aa314beec4e946e210099b57d40311f704e11d4", size = 71599, upload-time = "2025-12-06T13:23:20.195Z" }, + { url = "https://files.pythonhosted.org/packages/03/4b/a3b5175130b3810bbb8ccfa1edaadbd3afddb9992d877c8a1e2f274b476e/pybase64-1.4.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:356ef1d74648ce997f5a777cf8f1aefecc1c0b4fe6201e0ef3ec8a08170e1b54", size = 59922, upload-time = "2025-12-06T13:23:21.487Z" }, + { url = "https://files.pythonhosted.org/packages/da/5d/c38d1572027fc601b62d7a407721688b04b4d065d60ca489912d6893e6cf/pybase64-1.4.3-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.whl", hash = "sha256:c48361f90db32bacaa5518419d4eb9066ba558013aaf0c7781620279ecddaeb9", size = 56712, upload-time = "2025-12-06T13:23:22.77Z" }, + { url = "https://files.pythonhosted.org/packages/e7/d4/4e04472fef485caa8f561d904d4d69210a8f8fc1608ea15ebd9012b92655/pybase64-1.4.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:702bcaa16ae02139d881aeaef5b1c8ffb4a3fae062fe601d1e3835e10310a517", size = 59300, upload-time = "2025-12-06T13:23:24.543Z" }, + { url = "https://files.pythonhosted.org/packages/86/e7/16e29721b86734b881d09b7e23dfd7c8408ad01a4f4c7525f3b1088e25ec/pybase64-1.4.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:53d0ffe1847b16b647c6413d34d1de08942b7724273dd57e67dcbdb10c574045", size = 60278, upload-time = "2025-12-06T13:23:25.608Z" }, + { url = "https://files.pythonhosted.org/packages/b1/02/18515f211d7c046be32070709a8efeeef8a0203de4fd7521e6b56404731b/pybase64-1.4.3-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:9a1792e8b830a92736dae58f0c386062eb038dfe8004fb03ba33b6083d89cd43", size = 54817, upload-time = "2025-12-06T13:23:26.633Z" }, + { url = "https://files.pythonhosted.org/packages/e7/be/14e29d8e1a481dbff151324c96dd7b5d2688194bb65dc8a00ca0e1ad1e86/pybase64-1.4.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1d468b1b1ac5ad84875a46eaa458663c3721e8be5f155ade356406848d3701f6", size = 58611, upload-time = "2025-12-06T13:23:27.684Z" }, + { url = "https://files.pythonhosted.org/packages/b4/8a/a2588dfe24e1bbd742a554553778ab0d65fdf3d1c9a06d10b77047d142aa/pybase64-1.4.3-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:e97b7bdbd62e71898cd542a6a9e320d9da754ff3ebd02cb802d69087ee94d468", size = 52404, upload-time = "2025-12-06T13:23:28.714Z" }, + { url = "https://files.pythonhosted.org/packages/27/fc/afcda7445bebe0cbc38cafdd7813234cdd4fc5573ff067f1abf317bb0cec/pybase64-1.4.3-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:b33aeaa780caaa08ffda87fc584d5eab61e3d3bbb5d86ead02161dc0c20d04bc", size = 68817, upload-time = "2025-12-06T13:23:30.079Z" }, + { url = "https://files.pythonhosted.org/packages/d3/3a/87c3201e555ed71f73e961a787241a2438c2bbb2ca8809c29ddf938a3157/pybase64-1.4.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:1c0efcf78f11cf866bed49caa7b97552bc4855a892f9cc2372abcd3ed0056f0d", size = 57854, upload-time = "2025-12-06T13:23:31.17Z" }, + { url = "https://files.pythonhosted.org/packages/fd/7d/931c2539b31a7b375e7d595b88401eeb5bd6c5ce1059c9123f9b608aaa14/pybase64-1.4.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:66e3791f2ed725a46593f8bd2761ff37d01e2cdad065b1dceb89066f476e50c6", size = 54333, upload-time = "2025-12-06T13:23:32.422Z" }, + { url = "https://files.pythonhosted.org/packages/de/5e/537601e02cc01f27e9d75f440f1a6095b8df44fc28b1eef2cd739aea8cec/pybase64-1.4.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:72bb0b6bddadab26e1b069bb78e83092711a111a80a0d6b9edcb08199ad7299b", size = 56492, upload-time = "2025-12-06T13:23:33.515Z" }, + { url = "https://files.pythonhosted.org/packages/96/97/2a2e57acf8f5c9258d22aba52e71f8050e167b29ed2ee1113677c1b600c1/pybase64-1.4.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5b3365dbcbcdb0a294f0f50af0c0a16b27a232eddeeb0bceeefd844ef30d2a23", size = 70974, upload-time = "2025-12-06T13:23:36.27Z" }, + { url = "https://files.pythonhosted.org/packages/75/2e/a9e28941c6dab6f06e6d3f6783d3373044be9b0f9a9d3492c3d8d2260ac0/pybase64-1.4.3-cp312-cp312-win32.whl", hash = "sha256:7bca1ed3a5df53305c629ca94276966272eda33c0d71f862d2d3d043f1e1b91a", size = 33686, upload-time = "2025-12-06T13:23:37.848Z" }, + { url = "https://files.pythonhosted.org/packages/83/e3/507ab649d8c3512c258819c51d25c45d6e29d9ca33992593059e7b646a33/pybase64-1.4.3-cp312-cp312-win_amd64.whl", hash = "sha256:9f2da8f56d9b891b18b4daf463a0640eae45a80af548ce435be86aa6eff3603b", size = 35833, upload-time = "2025-12-06T13:23:38.877Z" }, + { url = "https://files.pythonhosted.org/packages/bc/8a/6eba66cd549a2fc74bb4425fd61b839ba0ab3022d3c401b8a8dc2cc00c7a/pybase64-1.4.3-cp312-cp312-win_arm64.whl", hash = "sha256:0631d8a2d035de03aa9bded029b9513e1fee8ed80b7ddef6b8e9389ffc445da0", size = 31185, upload-time = "2025-12-06T13:23:39.908Z" }, + { url = "https://files.pythonhosted.org/packages/b2/7c/545fd4935a0e1ddd7147f557bf8157c73eecec9cffd523382fa7af2557de/pybase64-1.4.3-graalpy311-graalpy242_311_native-macosx_10_9_x86_64.whl", hash = "sha256:d27c1dfdb0c59a5e758e7a98bd78eaca5983c22f4a811a36f4f980d245df4611", size = 38393, upload-time = "2025-12-06T13:26:19.535Z" }, + { url = "https://files.pythonhosted.org/packages/c3/ca/ae7a96be9ddc96030d4e9dffc43635d4e136b12058b387fd47eb8301b60f/pybase64-1.4.3-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:0f1a0c51d6f159511e3431b73c25db31095ee36c394e26a4349e067c62f434e5", size = 32109, upload-time = "2025-12-06T13:26:20.72Z" }, + { url = "https://files.pythonhosted.org/packages/bf/44/d4b7adc7bf4fd5b52d8d099121760c450a52c390223806b873f0b6a2d551/pybase64-1.4.3-graalpy311-graalpy242_311_native-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:a492518f3078a4e3faaef310697d21df9c6bc71908cebc8c2f6fbfa16d7d6b1f", size = 43227, upload-time = "2025-12-06T13:26:21.845Z" }, + { url = "https://files.pythonhosted.org/packages/08/86/2ba2d8734ef7939debeb52cf9952e457ba7aa226cae5c0e6dd631f9b851f/pybase64-1.4.3-graalpy311-graalpy242_311_native-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cae1a0f47784fd16df90d8acc32011c8d5fcdd9ab392c9ec49543e5f6a9c43a4", size = 35804, upload-time = "2025-12-06T13:26:23.149Z" }, + { url = "https://files.pythonhosted.org/packages/4f/5b/19c725dc3aaa6281f2ce3ea4c1628d154a40dd99657d1381995f8096768b/pybase64-1.4.3-graalpy311-graalpy242_311_native-win_amd64.whl", hash = "sha256:03cea70676ffbd39a1ab7930a2d24c625b416cacc9d401599b1d29415a43ab6a", size = 35880, upload-time = "2025-12-06T13:26:24.663Z" }, + { url = "https://files.pythonhosted.org/packages/17/45/92322aec1b6979e789b5710f73c59f2172bc37c8ce835305434796824b7b/pybase64-1.4.3-graalpy312-graalpy250_312_native-macosx_10_13_x86_64.whl", hash = "sha256:2baaa092f3475f3a9c87ac5198023918ea8b6c125f4c930752ab2cbe3cd1d520", size = 38746, upload-time = "2025-12-06T13:26:25.869Z" }, + { url = "https://files.pythonhosted.org/packages/11/94/f1a07402870388fdfc2ecec0c718111189732f7d0f2d7fe1386e19e8fad0/pybase64-1.4.3-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:cde13c0764b1af07a631729f26df019070dad759981d6975527b7e8ecb465b6c", size = 32573, upload-time = "2025-12-06T13:26:27.792Z" }, + { url = "https://files.pythonhosted.org/packages/fa/8f/43c3bb11ca9bacf81cb0b7a71500bb65b2eda6d5fe07433c09b543de97f3/pybase64-1.4.3-graalpy312-graalpy250_312_native-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:5c29a582b0ea3936d02bd6fe9bf674ab6059e6e45ab71c78404ab2c913224414", size = 43461, upload-time = "2025-12-06T13:26:28.906Z" }, + { url = "https://files.pythonhosted.org/packages/2d/4c/2a5258329200be57497d3972b5308558c6de42e3749c6cc2aa1cbe34b25a/pybase64-1.4.3-graalpy312-graalpy250_312_native-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b6b664758c804fa919b4f1257aa8cf68e95db76fc331de5f70bfc3a34655afe1", size = 36058, upload-time = "2025-12-06T13:26:30.092Z" }, + { url = "https://files.pythonhosted.org/packages/ea/6d/41faa414cde66ec023b0ca8402a8f11cb61731c3dc27c082909cbbd1f929/pybase64-1.4.3-graalpy312-graalpy250_312_native-win_amd64.whl", hash = "sha256:f7537fa22ae56a0bf51e4b0ffc075926ad91c618e1416330939f7ef366b58e3b", size = 36231, upload-time = "2025-12-06T13:26:31.656Z" }, + { url = "https://files.pythonhosted.org/packages/b2/76/160dded493c00d3376d4ad0f38a2119c5345de4a6693419ad39c3565959b/pybase64-1.4.3-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:277de6e03cc9090fb359365c686a2a3036d23aee6cd20d45d22b8c89d1247f17", size = 37939, upload-time = "2025-12-06T13:26:41.014Z" }, + { url = "https://files.pythonhosted.org/packages/b7/b8/a0f10be8d648d6f8f26e560d6e6955efa7df0ff1e009155717454d76f601/pybase64-1.4.3-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:ab1dd8b1ed2d1d750260ed58ab40defaa5ba83f76a30e18b9ebd5646f6247ae5", size = 31466, upload-time = "2025-12-06T13:26:42.539Z" }, + { url = "https://files.pythonhosted.org/packages/d3/22/832a2f9e76cdf39b52e01e40d8feeb6a04cf105494f2c3e3126d0149717f/pybase64-1.4.3-pp311-pypy311_pp73-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:bd4d2293de9fd212e294c136cec85892460b17d24e8c18a6ba18750928037750", size = 40681, upload-time = "2025-12-06T13:26:43.782Z" }, + { url = "https://files.pythonhosted.org/packages/12/d7/6610f34a8972415fab3bb4704c174a1cc477bffbc3c36e526428d0f3957d/pybase64-1.4.3-pp311-pypy311_pp73-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2af6d0d3a691911cc4c9a625f3ddcd3af720738c21be3d5c72de05629139d393", size = 41294, upload-time = "2025-12-06T13:26:44.936Z" }, + { url = "https://files.pythonhosted.org/packages/64/25/ed24400948a6c974ab1374a233cb7e8af0a5373cea0dd8a944627d17c34a/pybase64-1.4.3-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5cfc8c49a28322d82242088378f8542ce97459866ba73150b062a7073e82629d", size = 35447, upload-time = "2025-12-06T13:26:46.098Z" }, + { url = "https://files.pythonhosted.org/packages/ee/2b/e18ee7c5ee508a82897f021c1981533eca2940b5f072fc6ed0906c03a7a7/pybase64-1.4.3-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:debf737e09b8bf832ba86f5ecc3d3dbd0e3021d6cd86ba4abe962d6a5a77adb3", size = 36134, upload-time = "2025-12-06T13:26:47.35Z" }, +] + +[[package]] +name = "pycparser" +version = "3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1b/7d/92392ff7815c21062bea51aa7b87d45576f649f16458d78b7cf94b9ab2e6/pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29", size = 103492, upload-time = "2026-01-21T14:26:51.89Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992", size = 48172, upload-time = "2026-01-21T14:26:50.693Z" }, +] + +[[package]] +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, 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, upload-time = "2025-11-26T15:11:44.605Z" }, +] + +[[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, upload-time = "2025-11-04T13:43:49.098Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e8/72/74a989dd9f2084b3d9530b0915fdda64ac48831c30dbf7c72a41a5232db8/pydantic_core-2.41.5-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a3a52f6156e73e7ccb0f8cced536adccb7042be67cb45f9562e12b319c119da6", size = 2105873, upload-time = "2025-11-04T13:39:31.373Z" }, + { url = "https://files.pythonhosted.org/packages/12/44/37e403fd9455708b3b942949e1d7febc02167662bf1a7da5b78ee1ea2842/pydantic_core-2.41.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7f3bf998340c6d4b0c9a2f02d6a400e51f123b59565d74dc60d252ce888c260b", size = 1899826, upload-time = "2025-11-04T13:39:32.897Z" }, + { url = "https://files.pythonhosted.org/packages/33/7f/1d5cab3ccf44c1935a359d51a8a2a9e1a654b744b5e7f80d41b88d501eec/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:378bec5c66998815d224c9ca994f1e14c0c21cb95d2f52b6021cc0b2a58f2a5a", size = 1917869, upload-time = "2025-11-04T13:39:34.469Z" }, + { url = "https://files.pythonhosted.org/packages/6e/6a/30d94a9674a7fe4f4744052ed6c5e083424510be1e93da5bc47569d11810/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e7b576130c69225432866fe2f4a469a85a54ade141d96fd396dffcf607b558f8", size = 2063890, upload-time = "2025-11-04T13:39:36.053Z" }, + { url = "https://files.pythonhosted.org/packages/50/be/76e5d46203fcb2750e542f32e6c371ffa9b8ad17364cf94bb0818dbfb50c/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6cb58b9c66f7e4179a2d5e0f849c48eff5c1fca560994d6eb6543abf955a149e", size = 2229740, upload-time = "2025-11-04T13:39:37.753Z" }, + { url = "https://files.pythonhosted.org/packages/d3/ee/fed784df0144793489f87db310a6bbf8118d7b630ed07aa180d6067e653a/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:88942d3a3dff3afc8288c21e565e476fc278902ae4d6d134f1eeda118cc830b1", size = 2350021, upload-time = "2025-11-04T13:39:40.94Z" }, + { url = "https://files.pythonhosted.org/packages/c8/be/8fed28dd0a180dca19e72c233cbf58efa36df055e5b9d90d64fd1740b828/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f31d95a179f8d64d90f6831d71fa93290893a33148d890ba15de25642c5d075b", size = 2066378, upload-time = "2025-11-04T13:39:42.523Z" }, + { url = "https://files.pythonhosted.org/packages/b0/3b/698cf8ae1d536a010e05121b4958b1257f0b5522085e335360e53a6b1c8b/pydantic_core-2.41.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c1df3d34aced70add6f867a8cf413e299177e0c22660cc767218373d0779487b", size = 2175761, upload-time = "2025-11-04T13:39:44.553Z" }, + { url = "https://files.pythonhosted.org/packages/b8/ba/15d537423939553116dea94ce02f9c31be0fa9d0b806d427e0308ec17145/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:4009935984bd36bd2c774e13f9a09563ce8de4abaa7226f5108262fa3e637284", size = 2146303, upload-time = "2025-11-04T13:39:46.238Z" }, + { url = "https://files.pythonhosted.org/packages/58/7f/0de669bf37d206723795f9c90c82966726a2ab06c336deba4735b55af431/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:34a64bc3441dc1213096a20fe27e8e128bd3ff89921706e83c0b1ac971276594", size = 2340355, upload-time = "2025-11-04T13:39:48.002Z" }, + { url = "https://files.pythonhosted.org/packages/e5/de/e7482c435b83d7e3c3ee5ee4451f6e8973cff0eb6007d2872ce6383f6398/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:c9e19dd6e28fdcaa5a1de679aec4141f691023916427ef9bae8584f9c2fb3b0e", size = 2319875, upload-time = "2025-11-04T13:39:49.705Z" }, + { url = "https://files.pythonhosted.org/packages/fe/e6/8c9e81bb6dd7560e33b9053351c29f30c8194b72f2d6932888581f503482/pydantic_core-2.41.5-cp311-cp311-win32.whl", hash = "sha256:2c010c6ded393148374c0f6f0bf89d206bf3217f201faa0635dcd56bd1520f6b", size = 1987549, upload-time = "2025-11-04T13:39:51.842Z" }, + { url = "https://files.pythonhosted.org/packages/11/66/f14d1d978ea94d1bc21fc98fcf570f9542fe55bfcc40269d4e1a21c19bf7/pydantic_core-2.41.5-cp311-cp311-win_amd64.whl", hash = "sha256:76ee27c6e9c7f16f47db7a94157112a2f3a00e958bc626e2f4ee8bec5c328fbe", size = 2011305, upload-time = "2025-11-04T13:39:53.485Z" }, + { url = "https://files.pythonhosted.org/packages/56/d8/0e271434e8efd03186c5386671328154ee349ff0354d83c74f5caaf096ed/pydantic_core-2.41.5-cp311-cp311-win_arm64.whl", hash = "sha256:4bc36bbc0b7584de96561184ad7f012478987882ebf9f9c389b23f432ea3d90f", size = 1972902, upload-time = "2025-11-04T13:39:56.488Z" }, + { 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, upload-time = "2025-11-04T13:39:58.079Z" }, + { 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, upload-time = "2025-11-04T13:39:59.956Z" }, + { 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, upload-time = "2025-11-04T13:40:02.241Z" }, + { 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, upload-time = "2025-11-04T13:40:04.401Z" }, + { 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, upload-time = "2025-11-04T13:40:06.072Z" }, + { 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, upload-time = "2025-11-04T13:40:07.835Z" }, + { 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, upload-time = "2025-11-04T13:40:09.804Z" }, + { 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, upload-time = "2025-11-04T13:40:12.004Z" }, + { 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, upload-time = "2025-11-04T13:40:13.868Z" }, + { 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, upload-time = "2025-11-04T13:40:15.672Z" }, + { 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, upload-time = "2025-11-04T13:40:17.532Z" }, + { url = "https://files.pythonhosted.org/packages/49/3b/774f2b5cd4192d5ab75870ce4381fd89cf218af999515baf07e7206753f0/pydantic_core-2.41.5-cp312-cp312-win32.whl", hash = "sha256:b74557b16e390ec12dca509bce9264c3bbd128f8a2c376eaa68003d7f327276d", size = 1985908, upload-time = "2025-11-04T13:40:19.309Z" }, + { url = "https://files.pythonhosted.org/packages/86/45/00173a033c801cacf67c190fef088789394feaf88a98a7035b0e40d53dc9/pydantic_core-2.41.5-cp312-cp312-win_amd64.whl", hash = "sha256:1962293292865bca8e54702b08a4f26da73adc83dd1fcf26fbc875b35d81c815", size = 2020145, upload-time = "2025-11-04T13:40:21.548Z" }, + { url = "https://files.pythonhosted.org/packages/f9/22/91fbc821fa6d261b376a3f73809f907cec5ca6025642c463d3488aad22fb/pydantic_core-2.41.5-cp312-cp312-win_arm64.whl", hash = "sha256:1746d4a3d9a794cacae06a5eaaccb4b8643a131d45fbc9af23e353dc0a5ba5c3", size = 1976179, upload-time = "2025-11-04T13:40:23.393Z" }, + { url = "https://files.pythonhosted.org/packages/11/72/90fda5ee3b97e51c494938a4a44c3a35a9c96c19bba12372fb9c634d6f57/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:b96d5f26b05d03cc60f11a7761a5ded1741da411e7fe0909e27a5e6a0cb7b034", size = 2115441, upload-time = "2025-11-04T13:42:39.557Z" }, + { url = "https://files.pythonhosted.org/packages/1f/53/8942f884fa33f50794f119012dc6a1a02ac43a56407adaac20463df8e98f/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:634e8609e89ceecea15e2d61bc9ac3718caaaa71963717bf3c8f38bfde64242c", size = 1930291, upload-time = "2025-11-04T13:42:42.169Z" }, + { url = "https://files.pythonhosted.org/packages/79/c8/ecb9ed9cd942bce09fc888ee960b52654fbdbede4ba6c2d6e0d3b1d8b49c/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:93e8740d7503eb008aa2df04d3b9735f845d43ae845e6dcd2be0b55a2da43cd2", size = 1948632, upload-time = "2025-11-04T13:42:44.564Z" }, + { url = "https://files.pythonhosted.org/packages/2e/1b/687711069de7efa6af934e74f601e2a4307365e8fdc404703afc453eab26/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f15489ba13d61f670dcc96772e733aad1a6f9c429cc27574c6cdaed82d0146ad", size = 2138905, upload-time = "2025-11-04T13:42:47.156Z" }, + { url = "https://files.pythonhosted.org/packages/09/32/59b0c7e63e277fa7911c2fc70ccfb45ce4b98991e7ef37110663437005af/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:7da7087d756b19037bc2c06edc6c170eeef3c3bafcb8f532ff17d64dc427adfd", size = 2110495, upload-time = "2025-11-04T13:42:49.689Z" }, + { url = "https://files.pythonhosted.org/packages/aa/81/05e400037eaf55ad400bcd318c05bb345b57e708887f07ddb2d20e3f0e98/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:aabf5777b5c8ca26f7824cb4a120a740c9588ed58df9b2d196ce92fba42ff8dc", size = 1915388, upload-time = "2025-11-04T13:42:52.215Z" }, + { url = "https://files.pythonhosted.org/packages/6e/0d/e3549b2399f71d56476b77dbf3cf8937cec5cd70536bdc0e374a421d0599/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c007fe8a43d43b3969e8469004e9845944f1a80e6acd47c150856bb87f230c56", size = 1942879, upload-time = "2025-11-04T13:42:56.483Z" }, + { url = "https://files.pythonhosted.org/packages/f7/07/34573da085946b6a313d7c42f82f16e8920bfd730665de2d11c0c37a74b5/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:76d0819de158cd855d1cbb8fcafdf6f5cf1eb8e470abe056d5d161106e38062b", size = 2139017, upload-time = "2025-11-04T13:42:59.471Z" }, + { url = "https://files.pythonhosted.org/packages/5f/9b/1b3f0e9f9305839d7e84912f9e8bfbd191ed1b1ef48083609f0dabde978c/pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b2379fa7ed44ddecb5bfe4e48577d752db9fc10be00a6b7446e9663ba143de26", size = 2101980, upload-time = "2025-11-04T13:43:25.97Z" }, + { url = "https://files.pythonhosted.org/packages/a4/ed/d71fefcb4263df0da6a85b5d8a7508360f2f2e9b3bf5814be9c8bccdccc1/pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:266fb4cbf5e3cbd0b53669a6d1b039c45e3ce651fd5442eff4d07c2cc8d66808", size = 1923865, upload-time = "2025-11-04T13:43:28.763Z" }, + { url = "https://files.pythonhosted.org/packages/ce/3a/626b38db460d675f873e4444b4bb030453bbe7b4ba55df821d026a0493c4/pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58133647260ea01e4d0500089a8c4f07bd7aa6ce109682b1426394988d8aaacc", size = 2134256, upload-time = "2025-11-04T13:43:31.71Z" }, + { url = "https://files.pythonhosted.org/packages/83/d9/8412d7f06f616bbc053d30cb4e5f76786af3221462ad5eee1f202021eb4e/pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:287dad91cfb551c363dc62899a80e9e14da1f0e2b6ebde82c806612ca2a13ef1", size = 2174762, upload-time = "2025-11-04T13:43:34.744Z" }, + { url = "https://files.pythonhosted.org/packages/55/4c/162d906b8e3ba3a99354e20faa1b49a85206c47de97a639510a0e673f5da/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:03b77d184b9eb40240ae9fd676ca364ce1085f203e1b1256f8ab9984dca80a84", size = 2143141, upload-time = "2025-11-04T13:43:37.701Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f2/f11dd73284122713f5f89fc940f370d035fa8e1e078d446b3313955157fe/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:a668ce24de96165bb239160b3d854943128f4334822900534f2fe947930e5770", size = 2330317, upload-time = "2025-11-04T13:43:40.406Z" }, + { url = "https://files.pythonhosted.org/packages/88/9d/b06ca6acfe4abb296110fb1273a4d848a0bfb2ff65f3ee92127b3244e16b/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f14f8f046c14563f8eb3f45f499cc658ab8d10072961e07225e507adb700e93f", size = 2316992, upload-time = "2025-11-04T13:43:43.602Z" }, + { url = "https://files.pythonhosted.org/packages/36/c7/cfc8e811f061c841d7990b0201912c3556bfeb99cdcb7ed24adc8d6f8704/pydantic_core-2.41.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:56121965f7a4dc965bff783d70b907ddf3d57f6eba29b6d2e5dabfaf07799c51", size = 2145302, upload-time = "2025-11-04T13:43:46.64Z" }, +] + +[[package]] +name = "pydantic-settings" +version = "2.14.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic" }, + { name = "python-dotenv" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5c/b5/8f48e906c3e0205276e8bd8cb7512217a87b2685304d64be27cad5b3019f/pydantic_settings-2.14.2.tar.gz", hash = "sha256:c19dd64b19097f1de80184f0cc7b0272a13ae6e170cbf240a3e27e381ed14a5f", size = 237700, upload-time = "2026-06-19T13:44:56.324Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/77/c1/6e422f34e569cf8e18df68d1939c81c099d2b61e4f7d9621c8a77560799c/pydantic_settings-2.14.2-py3-none-any.whl", hash = "sha256:a20c97b37910b6550d5ea50fbcc2d4187defe58cd57070b73863d069419c9440", size = 61715, upload-time = "2026-06-19T13:44:55.02Z" }, +] + +[[package]] +name = "pygments" +version = "2.20.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991, upload-time = "2026-03-29T13:29:33.898Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" }, +] + +[[package]] +name = "pyjwt" +version = "2.13.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3b/81/58d0ac84e1ef3a3843791d6954d94c0b33d526c75eeb1efbce9d0a4c4077/pyjwt-2.13.0.tar.gz", hash = "sha256:41571c89ca91598c79e8ef18a2d07367d4810fbbd6f637794879baf1b7703423", size = 107515, upload-time = "2026-05-21T19:54:36.618Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a3/5e/ecf12fdb62546d64385c158514e9b2b671f7832108ef2ecd2020ce0af2d1/pyjwt-2.13.0-py3-none-any.whl", hash = "sha256:66adcc2aff09b3f1bbd95fc1e1577df8ac8723c978552fd43304c8a290ac5728", size = 31274, upload-time = "2026-05-21T19:54:35.362Z" }, +] + +[package.optional-dependencies] +crypto = [ + { name = "cryptography" }, +] + +[[package]] +name = "pymupdf" +version = "1.26.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/48/d6/09b28f027b510838559f7748807192149c419b30cb90e6d5f0cf916dc9dc/pymupdf-1.26.7.tar.gz", hash = "sha256:71add8bdc8eb1aaa207c69a13400693f06ad9b927bea976f5d5ab9df0bb489c3", size = 84327033, upload-time = "2025-12-11T21:48:50.694Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/94/35/cd74cea1787b2247702ef8522186bdef32e9cb30a099e6bb864627ef6045/pymupdf-1.26.7-cp310-abi3-macosx_10_9_x86_64.whl", hash = "sha256:07085718dfdae5ab83b05eb5eb397f863bcc538fe05135318a01ea353e7a1353", size = 23179369, upload-time = "2025-12-11T21:47:21.587Z" }, + { url = "https://files.pythonhosted.org/packages/72/74/448b6172927c829c6a3fba80078d7b0a016ebbe2c9ee528821f5ea21677a/pymupdf-1.26.7-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:31aa9c8377ea1eea02934b92f4dcf79fb2abba0bf41f8a46d64c3e31546a3c02", size = 22470101, upload-time = "2025-12-11T21:47:37.105Z" }, + { url = "https://files.pythonhosted.org/packages/65/e7/47af26f3ac76be7ac3dd4d6cc7ee105948a8355d774e5ca39857bf91c11c/pymupdf-1.26.7-cp310-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:e419b609996434a14a80fa060adec72c434a1cca6a511ec54db9841bc5d51b3c", size = 23502486, upload-time = "2025-12-12T09:51:25.824Z" }, + { url = "https://files.pythonhosted.org/packages/2a/6b/3de1714d734ff949be1e90a22375d0598d3540b22ae73eb85c2d7d1f36a9/pymupdf-1.26.7-cp310-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:69dfc78f206a96e5b3ac22741263ebab945fdf51f0dbe7c5757c3511b23d9d72", size = 24115727, upload-time = "2025-12-11T21:47:51.274Z" }, + { url = "https://files.pythonhosted.org/packages/62/9b/f86224847949577a523be2207315ae0fd3155b5d909cd66c274d095349a3/pymupdf-1.26.7-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:1d5106f46e1ca0d64d46bd51892372a4f82076bdc14a9678d33d630702abca36", size = 24324386, upload-time = "2025-12-12T14:58:45.483Z" }, + { url = "https://files.pythonhosted.org/packages/85/8e/a117d39092ca645fde8b903f4a941d9aa75b370a67b4f1f435f56393dc5a/pymupdf-1.26.7-cp310-abi3-win32.whl", hash = "sha256:7c9645b6f5452629c747690190350213d3e5bbdb6b2eca227d82702b327f6eee", size = 17203888, upload-time = "2025-12-12T13:59:57.613Z" }, + { url = "https://files.pythonhosted.org/packages/dd/c3/d0047678146c294469c33bae167c8ace337deafb736b0bf97b9bc481aa65/pymupdf-1.26.7-cp310-abi3-win_amd64.whl", hash = "sha256:425b1befe40d41b72eb0fe211711c7ae334db5eb60307e9dd09066ed060cceba", size = 18405952, upload-time = "2025-12-11T21:48:02.947Z" }, +] + +[[package]] +name = "pypdfium2" +version = "5.11.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a8/b9/a887db9950379fe619f6921ade84730346e6adf008b197951ddcc42dfd1a/pypdfium2-5.11.0.tar.gz", hash = "sha256:0733749ac253c615953a5e75d4322b9f1de8c0d90137ec8dbcef403f3e57b5d9", size = 276614, upload-time = "2026-06-29T11:11:40.936Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/94/09/0aec4588adaa7eb4e42da127a57a5dd3a3997ff32be62af93f8afeb4668b/pypdfium2-5.11.0-py3-none-android_23_arm64_v8a.whl", hash = "sha256:115bc68ab2e85c9ee46f97a4c1794f1d7205c8b763fc5c16d9893eaa54608627", size = 3388299, upload-time = "2026-06-29T11:11:00.183Z" }, + { url = "https://files.pythonhosted.org/packages/7a/53/9e349a311cb0e7cef5dd0824a6f96c9b161da5e5e67a3db030d38e624fe9/pypdfium2-5.11.0-py3-none-android_23_armeabi_v7a.whl", hash = "sha256:4396d0848b79134fcdf141a4e3dfe6719efe1162309a02876b440025cfd80720", size = 2843597, upload-time = "2026-06-29T11:11:02.351Z" }, + { url = "https://files.pythonhosted.org/packages/d2/13/13571dc7f1d11a4e4bde6ab8961318b04ff70bdc2aea5e7f88be5ef53167/pypdfium2-5.11.0-py3-none-macosx_12_0_arm64.whl", hash = "sha256:73fe55dd258f02332bc0a34128ddc2994fd610e664d1f2f7d78dd9e2570f15ee", size = 3586638, upload-time = "2026-06-29T11:11:04.264Z" }, + { url = "https://files.pythonhosted.org/packages/84/a2/9865e2822e97f6e4bed3b0e4838bd88444c62df6ebe0ccae352027133120/pypdfium2-5.11.0-py3-none-macosx_12_0_x86_64.whl", hash = "sha256:badb4f4df98e25fb1b58301911c98fa18da60de98b0223a6ffa91717be068b96", size = 3649576, upload-time = "2026-06-29T11:11:06.011Z" }, + { url = "https://files.pythonhosted.org/packages/a2/bb/dcd0ba152626c30a67bff43974b728c6bdd9a0d34af54cfa875489e46ce2/pypdfium2-5.11.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1fbff7c3e3141f24f3a25cedddbb6c6d17baa95f83c7b7ea40d6866956df3a43", size = 3648292, upload-time = "2026-06-29T11:11:07.767Z" }, + { url = "https://files.pythonhosted.org/packages/65/62/39b40afda909bd65b212e9dbf32e7c6a2da4205ff64576965a49ba689c47/pypdfium2-5.11.0-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:faeda49c171be254b5028b7d17e54306c585374210d3e7ea4962a36774d5b76a", size = 3380060, upload-time = "2026-06-29T11:11:09.571Z" }, + { url = "https://files.pythonhosted.org/packages/6f/cf/999b793ca17d153765dc3656e7f4cc90c4bf411b9262f15b8e7e31af164f/pypdfium2-5.11.0-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b63f6f1e67d05d2bc36f55c35ede5cd18ded8886a2fcf68f320d188ee7934348", size = 3778180, upload-time = "2026-06-29T11:11:11.234Z" }, + { url = "https://files.pythonhosted.org/packages/73/6c/54fd2b487eed6a420ee7fff5c7754739379c4459fa185deab3cb2b72e4e2/pypdfium2-5.11.0-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:44f96d8459b4ba9d1330ab44a8dde836b361cb00027830eaab710080888cf44a", size = 4190704, upload-time = "2026-06-29T11:11:13.076Z" }, + { url = "https://files.pythonhosted.org/packages/3b/8f/690b9a1b8de405ff7693c1d10472f0c5395294c6d53654ae1ef97ccf70fa/pypdfium2-5.11.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba8a78329c10f01c80b1f6e9aa33cac13665c198d056a007827b703958bd986d", size = 3705232, upload-time = "2026-06-29T11:11:14.864Z" }, + { url = "https://files.pythonhosted.org/packages/54/fc/18298367dc073041320bbb5d51fb3b42b0d9164fd13bd4d13dcb0d66bc64/pypdfium2-5.11.0-py3-none-manylinux_2_27_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e7a60197c5d18b0ec77a695e4a1082f89f323c13063cecf4446f1297ed632a36", size = 4031064, upload-time = "2026-06-29T11:11:17.201Z" }, + { url = "https://files.pythonhosted.org/packages/45/1f/53d5b6ab0869963474b7ea13097c5abd8f2cf13548f0076e9bafc7e9d6b0/pypdfium2-5.11.0-py3-none-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:53d167456433d565398a5c9f1efad23dfa6dfadc1a201ec687594fec645d843e", size = 3995092, upload-time = "2026-06-29T11:11:18.905Z" }, + { url = "https://files.pythonhosted.org/packages/82/7a/942a390e41fb59a797d9303eae6e6a8c3d1b84deade03206ac163076033f/pypdfium2-5.11.0-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:953f5bcb6a65067381f0a38c50ffaed864fac6f20b0fa2c14ffb15a52bda8943", size = 4995668, upload-time = "2026-06-29T11:11:20.677Z" }, + { url = "https://files.pythonhosted.org/packages/8e/e9/fd1f8d3157c92665166f7d9bfcdd26901290b4271fcd2f90c7419ca0ee4c/pypdfium2-5.11.0-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:479b081423b2c16de44ddb6e25826d665298e1ee7cf09d802b656716dd930201", size = 4539453, upload-time = "2026-06-29T11:11:22.474Z" }, + { url = "https://files.pythonhosted.org/packages/e9/56/4280069868245d2346e6ec7b42e155c16a19be731f2f57fb726c35661aac/pypdfium2-5.11.0-py3-none-musllinux_1_2_i686.whl", hash = "sha256:81356f0503356d6f21629fcd47bbfffcfe26487f47a7989a56c02bd4e39b4c55", size = 5237429, upload-time = "2026-06-29T11:11:24.17Z" }, + { url = "https://files.pythonhosted.org/packages/84/9f/99d962bea28c3305835d07a1732a771104ce958f46e68a1892e740a7c904/pypdfium2-5.11.0-py3-none-musllinux_1_2_ppc64le.whl", hash = "sha256:25be9d70f5776c68fa28b92ec42a7be91de5d6c4085ed5c6c89e1bfa373ae918", size = 5143685, upload-time = "2026-06-29T11:11:26.04Z" }, + { url = "https://files.pythonhosted.org/packages/e6/4a/c935d1fdd73092fd036627282948c794c8f14a7ddde39cf732898b258866/pypdfium2-5.11.0-py3-none-musllinux_1_2_riscv64.whl", hash = "sha256:8edec20239f77d4dea2eeee5d29ebd83e77e1e2900691b77387fead76f81bc4a", size = 4647708, upload-time = "2026-06-29T11:11:28.006Z" }, + { url = "https://files.pythonhosted.org/packages/62/50/f4f1895efa581f8b85748bf8d620ad37550583ca5226feeb9a33dcb3031e/pypdfium2-5.11.0-py3-none-musllinux_1_2_s390x.whl", hash = "sha256:2560db37ce77a6caf651dc129bc44ee610a5fbb0050ec70f5a643f76bf241173", size = 5089405, upload-time = "2026-06-29T11:11:30.013Z" }, + { url = "https://files.pythonhosted.org/packages/21/40/ca5b1071aee0a96937a30007504a0ca484340709e22a459e99650165fd57/pypdfium2-5.11.0-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:34086bc1fcb964bb8104889d941ba9c8a35a75866250bb81d2f2bb2791bb42af", size = 5051368, upload-time = "2026-06-29T11:11:31.868Z" }, + { url = "https://files.pythonhosted.org/packages/28/01/e445a77e784a5b0047a2c30b263630dfaa57318ff3196e6ad3093dca6929/pypdfium2-5.11.0-py3-none-win32.whl", hash = "sha256:324054f36acf6bea42a9d2534eb407da2e312dba746d955c9fd7e324bc160898", size = 3645281, upload-time = "2026-06-29T11:11:33.691Z" }, + { url = "https://files.pythonhosted.org/packages/73/d4/8b8af6eedbc5c8af49817d8f37c2e55be8a2c7f75fca9bee78efbfeb40f6/pypdfium2-5.11.0-py3-none-win_amd64.whl", hash = "sha256:d3b698e7b51bdf2d633cc834395677319e1d66757896b30c632dfac7d7236c81", size = 3778234, upload-time = "2026-06-29T11:11:35.64Z" }, + { url = "https://files.pythonhosted.org/packages/c5/a7/657503553694c70fba0aa5d213144d80401611c82e0205f43f1ff39f40af/pypdfium2-5.11.0-py3-none-win_arm64.whl", hash = "sha256:897788d71b740752e29875856c369fdb52283f609aecf2355f0473db05dfc1b6", size = 3567726, upload-time = "2026-06-29T11:11:38.689Z" }, +] + +[[package]] +name = "pypika" +version = "0.51.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f8/78/cbaebba88e05e2dcda13ca203131b38d3640219f20ebb49676d26714861b/pypika-0.51.1.tar.gz", hash = "sha256:c30c7c1048fbf056fd3920c5a2b88b0c29dd190a9b2bee971fd17e4abe4d0ebe", size = 80919, upload-time = "2026-02-04T11:27:48.304Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/57/83/c77dfeed04022e8930b08eedca2b6e5efed256ab3321396fde90066efb65/pypika-0.51.1-py2.py3-none-any.whl", hash = "sha256:77985b4d7ce71b9905255bf12468cf598349e98837c037541cfc240e528aec46", size = 60585, upload-time = "2026-02-04T11:27:46.251Z" }, +] + +[[package]] +name = "pyproject-hooks" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e7/82/28175b2414effca1cdac8dc99f76d660e7a4fb0ceefa4b4ab8f5f6742925/pyproject_hooks-1.2.0.tar.gz", hash = "sha256:1e859bd5c40fae9448642dd871adf459e5e2084186e8d2c2a79a824c970da1f8", size = 19228, upload-time = "2024-09-29T09:24:13.293Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bd/24/12818598c362d7f300f18e74db45963dbcb85150324092410c8b49405e42/pyproject_hooks-1.2.0-py3-none-any.whl", hash = "sha256:9e5c6bfa8dcc30091c74b0cf803c81fdd29d94f01992a7707bc97babb1141913", size = 10216, upload-time = "2024-09-29T09:24:11.978Z" }, +] + +[[package]] +name = "pytest" +version = "9.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e4/47/b9efed96c114afcfa3c9d3fe98a76a1d14c74a9e266d397cf6eb64be5e01/pytest-9.1.1.tar.gz", hash = "sha256:1088fbde8f2b49d95a549a195707afa7a76a3ce9bcadc26b6d71f0ffda5fe313", size = 1636369, upload-time = "2026-06-19T10:58:32.857Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/24/25/1de2678b631f5a49215c6c96fff41ba892b0a34df68d6d80292b1b48aa7f/pytest-9.1.1-py3-none-any.whl", hash = "sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c", size = 386536, upload-time = "2026-06-19T10:58:31.347Z" }, +] + +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +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, 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, upload-time = "2024-03-01T18:36:18.57Z" }, +] + +[[package]] +name = "python-docx" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "lxml" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a9/f7/eddfe33871520adab45aaa1a71f0402a2252050c14c7e3009446c8f4701c/python_docx-1.2.0.tar.gz", hash = "sha256:7bc9d7b7d8a69c9c02ca09216118c86552704edc23bac179283f2e38f86220ce", size = 5723256, upload-time = "2025-06-16T20:46:27.921Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d0/00/1e03a4989fa5795da308cd774f05b704ace555a70f9bf9d3be057b680bcf/python_docx-1.2.0-py3-none-any.whl", hash = "sha256:3fd478f3250fbbbfd3b94fe1e985955737c145627498896a8a6bf81f4baf66c7", size = 252987, upload-time = "2025-06-16T20:46:22.506Z" }, +] + +[[package]] +name = "python-dotenv" +version = "1.2.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/82/ed/0301aeeac3e5353ef3d94b6ec08bbcabd04a72018415dcb29e588514bba8/python_dotenv-1.2.2.tar.gz", hash = "sha256:2c371a91fbd7ba082c2c1dc1f8bf89ca22564a087c2c287cd9b662adde799cf3", size = 50135, upload-time = "2026-03-01T16:00:26.196Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0b/d7/1959b9648791274998a9c3526f6d0ec8fd2233e4d4acce81bbae76b44b2a/python_dotenv-1.2.2-py3-none-any.whl", hash = "sha256:1d8214789a24de455a8b8bd8ae6fe3c6b69a5e3d64aa8a8e5d68e694bbcb285a", size = 22101, upload-time = "2026-03-01T16:00:25.09Z" }, +] + +[[package]] +name = "python-multipart" +version = "0.0.32" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5b/42/55c32bb9b12693c092ad250a0e82edb5b31ddeda6eb772de5f308b3804ad/python_multipart-0.0.32.tar.gz", hash = "sha256:be54b7f3fa167bb83e4fcd936b887b708f4e57fe75911c02aebf53efaf8d938e", size = 46881, upload-time = "2026-06-04T16:18:58.647Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e1/04/e8135ebd1ad02c56ec633277529b2602ff99ff634be76cdba5744cf554fd/python_multipart-0.0.32-py3-none-any.whl", hash = "sha256:ff6d3f776f16878c894e52e107296ffc890e913c611b1a4ec6c44e2821fe2e23", size = 30042, upload-time = "2026-06-04T16:18:57.319Z" }, +] + +[[package]] +name = "pytube" +version = "15.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/e7/16fec46c8d255c4bbc4b185d89c91dc92cdb802836570d8004d0db169c91/pytube-15.0.0.tar.gz", hash = "sha256:076052efe76f390dfa24b1194ff821d4e86c17d41cb5562f3a276a8bcbfc9d1d", size = 67229, upload-time = "2023-05-07T19:39:01.903Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/51/64/bcf8632ed2b7a36bbf84a0544885ffa1d0b4bcf25cc0903dba66ec5fdad9/pytube-15.0.0-py3-none-any.whl", hash = "sha256:07b9904749e213485780d7eb606e5e5b8e4341aa4dccf699160876da00e12d78", size = 57594, upload-time = "2023-05-07T19:38:59.191Z" }, +] + +[[package]] +name = "pywin32" +version = "312" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1f/f5/10a6e845a00fc5e7afd0a988b744f403d4d57162a28d160a093c4d9322f0/pywin32-312-cp311-cp311-win32.whl", hash = "sha256:17948aeadbdb091f0ced6ef0841620794e68327b94ee415571c1203594b7215c", size = 6362659, upload-time = "2026-06-04T07:49:21.349Z" }, + { url = "https://files.pythonhosted.org/packages/35/c4/dcd2d62b5944b6d5db53413a5899016ccd57ffcb7278f3f81655d25d2027/pywin32-312-cp311-cp311-win_amd64.whl", hash = "sha256:d11417d84412f859b722fad0841b3614459ed0047f7542d8362e77884f6b6e8a", size = 6928825, upload-time = "2026-06-04T07:49:23.934Z" }, + { url = "https://files.pythonhosted.org/packages/b7/56/3cbb433fe4501cdba2eb9040f56a4e1a8243faa4186b25295564d1a7a79d/pywin32-312-cp311-cp311-win_arm64.whl", hash = "sha256:b2200a054ca6d6625c4842fc56a4976a4b47f96b73dbe5538c3f813a80359f47", size = 6721875, upload-time = "2026-06-04T07:49:26.416Z" }, + { url = "https://files.pythonhosted.org/packages/83/ff/32aa7d2ed0ab12b323aaa64f9b75e6ad4f8fd09f9ccfc28c79414d46838d/pywin32-312-cp312-cp312-win32.whl", hash = "sha256:dab4f65ac9c4e48400a2a0530c46c3c579cd5905ecd11b80692373915269208b", size = 6371877, upload-time = "2026-06-04T07:49:28.836Z" }, + { url = "https://files.pythonhosted.org/packages/03/d9/77040d3b43df3f3be32ea289433d660d2727f5ba327bc73be835127d9d60/pywin32-312-cp312-cp312-win_amd64.whl", hash = "sha256:b457f6d628a47e8a7346ce22acb7e1a46a4a78b52e1d17e1af56871bd19a93bc", size = 6914841, upload-time = "2026-06-04T07:49:31.85Z" }, + { url = "https://files.pythonhosted.org/packages/e3/cc/7b1ec671775756020a0ee7f4feeaf3c568f0ab86bd3900088cf986937a92/pywin32-312-cp312-cp312-win_arm64.whl", hash = "sha256:6017c58e12f6809fbb0555b75df144c2922a9ffd18e4b9b5afa863b6c1a9d950", size = 6727901, upload-time = "2026-06-04T07:49:34.244Z" }, +] + +[[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, upload-time = "2025-09-25T21:33:16.546Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6d/16/a95b6757765b7b031c9374925bb718d55e0a9ba8a1b6a12d25962ea44347/pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e", size = 185826, upload-time = "2025-09-25T21:31:58.655Z" }, + { url = "https://files.pythonhosted.org/packages/16/19/13de8e4377ed53079ee996e1ab0a9c33ec2faf808a4647b7b4c0d46dd239/pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824", size = 175577, upload-time = "2025-09-25T21:32:00.088Z" }, + { url = "https://files.pythonhosted.org/packages/0c/62/d2eb46264d4b157dae1275b573017abec435397aa59cbcdab6fc978a8af4/pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c", size = 775556, upload-time = "2025-09-25T21:32:01.31Z" }, + { url = "https://files.pythonhosted.org/packages/10/cb/16c3f2cf3266edd25aaa00d6c4350381c8b012ed6f5276675b9eba8d9ff4/pyyaml-6.0.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00", size = 882114, upload-time = "2025-09-25T21:32:03.376Z" }, + { url = "https://files.pythonhosted.org/packages/71/60/917329f640924b18ff085ab889a11c763e0b573da888e8404ff486657602/pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d", size = 806638, upload-time = "2025-09-25T21:32:04.553Z" }, + { url = "https://files.pythonhosted.org/packages/dd/6f/529b0f316a9fd167281a6c3826b5583e6192dba792dd55e3203d3f8e655a/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a", size = 767463, upload-time = "2025-09-25T21:32:06.152Z" }, + { url = "https://files.pythonhosted.org/packages/f2/6a/b627b4e0c1dd03718543519ffb2f1deea4a1e6d42fbab8021936a4d22589/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4", size = 794986, upload-time = "2025-09-25T21:32:07.367Z" }, + { url = "https://files.pythonhosted.org/packages/45/91/47a6e1c42d9ee337c4839208f30d9f09caa9f720ec7582917b264defc875/pyyaml-6.0.3-cp311-cp311-win32.whl", hash = "sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b", size = 142543, upload-time = "2025-09-25T21:32:08.95Z" }, + { url = "https://files.pythonhosted.org/packages/da/e3/ea007450a105ae919a72393cb06f122f288ef60bba2dc64b26e2646fa315/pyyaml-6.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf", size = 158763, upload-time = "2025-09-25T21:32:09.96Z" }, + { 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, upload-time = "2025-09-25T21:32:11.445Z" }, + { 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, upload-time = "2025-09-25T21:32:12.492Z" }, + { 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, upload-time = "2025-09-25T21:32:13.652Z" }, + { 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, upload-time = "2025-09-25T21:32:15.21Z" }, + { 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, upload-time = "2025-09-25T21:32:16.431Z" }, + { 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, upload-time = "2025-09-25T21:32:17.56Z" }, + { 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, upload-time = "2025-09-25T21:32:18.834Z" }, + { url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658, upload-time = "2025-09-25T21:32:20.209Z" }, + { url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003, upload-time = "2025-09-25T21:32:21.167Z" }, + { url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344, upload-time = "2025-09-25T21:32:22.617Z" }, +] + +[[package]] +name = "referencing" +version = "0.37.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "rpds-py" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/22/f5/df4e9027acead3ecc63e50fe1e36aca1523e1719559c499951bb4b53188f/referencing-0.37.0.tar.gz", hash = "sha256:44aefc3142c5b842538163acb373e24cce6632bd54bdb01b21ad5863489f50d8", size = 78036, upload-time = "2025-10-13T15:30:48.871Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/58/ca301544e1fa93ed4f80d724bf5b194f6e4b945841c5bfd555878eea9fcb/referencing-0.37.0-py3-none-any.whl", hash = "sha256:381329a9f99628c9069361716891d34ad94af76e461dcb0335825aecc7692231", size = 26766, upload-time = "2025-10-13T15:30:47.625Z" }, +] + +[[package]] +name = "regex" +version = "2026.1.15" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0b/86/07d5056945f9ec4590b518171c4254a5925832eb727b56d3c38a7476f316/regex-2026.1.15.tar.gz", hash = "sha256:164759aa25575cbc0651bef59a0b18353e54300d79ace8084c818ad8ac72b7d5", size = 414811, upload-time = "2026-01-14T23:18:02.775Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d0/c9/0c80c96eab96948363d270143138d671d5731c3a692b417629bf3492a9d6/regex-2026.1.15-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:1ae6020fb311f68d753b7efa9d4b9a5d47a5d6466ea0d5e3b5a471a960ea6e4a", size = 488168, upload-time = "2026-01-14T23:14:16.129Z" }, + { url = "https://files.pythonhosted.org/packages/17/f0/271c92f5389a552494c429e5cc38d76d1322eb142fb5db3c8ccc47751468/regex-2026.1.15-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:eddf73f41225942c1f994914742afa53dc0d01a6e20fe14b878a1b1edc74151f", size = 290636, upload-time = "2026-01-14T23:14:17.715Z" }, + { url = "https://files.pythonhosted.org/packages/a0/f9/5f1fd077d106ca5655a0f9ff8f25a1ab55b92128b5713a91ed7134ff688e/regex-2026.1.15-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e8cd52557603f5c66a548f69421310886b28b7066853089e1a71ee710e1cdc1", size = 288496, upload-time = "2026-01-14T23:14:19.326Z" }, + { url = "https://files.pythonhosted.org/packages/b5/e1/8f43b03a4968c748858ec77f746c286d81f896c2e437ccf050ebc5d3128c/regex-2026.1.15-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5170907244b14303edc5978f522f16c974f32d3aa92109fabc2af52411c9433b", size = 793503, upload-time = "2026-01-14T23:14:20.922Z" }, + { url = "https://files.pythonhosted.org/packages/8d/4e/a39a5e8edc5377a46a7c875c2f9a626ed3338cb3bb06931be461c3e1a34a/regex-2026.1.15-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2748c1ec0663580b4510bd89941a31560b4b439a0b428b49472a3d9944d11cd8", size = 860535, upload-time = "2026-01-14T23:14:22.405Z" }, + { url = "https://files.pythonhosted.org/packages/dc/1c/9dce667a32a9477f7a2869c1c767dc00727284a9fa3ff5c09a5c6c03575e/regex-2026.1.15-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2f2775843ca49360508d080eaa87f94fa248e2c946bbcd963bb3aae14f333413", size = 907225, upload-time = "2026-01-14T23:14:23.897Z" }, + { url = "https://files.pythonhosted.org/packages/a4/3c/87ca0a02736d16b6262921425e84b48984e77d8e4e572c9072ce96e66c30/regex-2026.1.15-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d9ea2604370efc9a174c1b5dcc81784fb040044232150f7f33756049edfc9026", size = 800526, upload-time = "2026-01-14T23:14:26.039Z" }, + { url = "https://files.pythonhosted.org/packages/4b/ff/647d5715aeea7c87bdcbd2f578f47b415f55c24e361e639fe8c0cc88878f/regex-2026.1.15-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0dcd31594264029b57bf16f37fd7248a70b3b764ed9e0839a8f271b2d22c0785", size = 773446, upload-time = "2026-01-14T23:14:28.109Z" }, + { url = "https://files.pythonhosted.org/packages/af/89/bf22cac25cb4ba0fe6bff52ebedbb65b77a179052a9d6037136ae93f42f4/regex-2026.1.15-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c08c1f3e34338256732bd6938747daa3c0d5b251e04b6e43b5813e94d503076e", size = 783051, upload-time = "2026-01-14T23:14:29.929Z" }, + { url = "https://files.pythonhosted.org/packages/1e/f4/6ed03e71dca6348a5188363a34f5e26ffd5db1404780288ff0d79513bce4/regex-2026.1.15-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:e43a55f378df1e7a4fa3547c88d9a5a9b7113f653a66821bcea4718fe6c58763", size = 854485, upload-time = "2026-01-14T23:14:31.366Z" }, + { url = "https://files.pythonhosted.org/packages/d9/9a/8e8560bd78caded8eb137e3e47612430a05b9a772caf60876435192d670a/regex-2026.1.15-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:f82110ab962a541737bd0ce87978d4c658f06e7591ba899192e2712a517badbb", size = 762195, upload-time = "2026-01-14T23:14:32.802Z" }, + { url = "https://files.pythonhosted.org/packages/38/6b/61fc710f9aa8dfcd764fe27d37edfaa023b1a23305a0d84fccd5adb346ea/regex-2026.1.15-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:27618391db7bdaf87ac6c92b31e8f0dfb83a9de0075855152b720140bda177a2", size = 845986, upload-time = "2026-01-14T23:14:34.898Z" }, + { url = "https://files.pythonhosted.org/packages/fd/2e/fbee4cb93f9d686901a7ca8d94285b80405e8c34fe4107f63ffcbfb56379/regex-2026.1.15-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bfb0d6be01fbae8d6655c8ca21b3b72458606c4aec9bbc932db758d47aba6db1", size = 788992, upload-time = "2026-01-14T23:14:37.116Z" }, + { url = "https://files.pythonhosted.org/packages/ed/14/3076348f3f586de64b1ab75a3fbabdaab7684af7f308ad43be7ef1849e55/regex-2026.1.15-cp311-cp311-win32.whl", hash = "sha256:b10e42a6de0e32559a92f2f8dc908478cc0fa02838d7dbe764c44dca3fa13569", size = 265893, upload-time = "2026-01-14T23:14:38.426Z" }, + { url = "https://files.pythonhosted.org/packages/0f/19/772cf8b5fc803f5c89ba85d8b1870a1ca580dc482aa030383a9289c82e44/regex-2026.1.15-cp311-cp311-win_amd64.whl", hash = "sha256:e9bf3f0bbdb56633c07d7116ae60a576f846efdd86a8848f8d62b749e1209ca7", size = 277840, upload-time = "2026-01-14T23:14:39.785Z" }, + { url = "https://files.pythonhosted.org/packages/78/84/d05f61142709474da3c0853222d91086d3e1372bcdab516c6fd8d80f3297/regex-2026.1.15-cp311-cp311-win_arm64.whl", hash = "sha256:41aef6f953283291c4e4e6850607bd71502be67779586a61472beacb315c97ec", size = 270374, upload-time = "2026-01-14T23:14:41.592Z" }, + { url = "https://files.pythonhosted.org/packages/92/81/10d8cf43c807d0326efe874c1b79f22bfb0fb226027b0b19ebc26d301408/regex-2026.1.15-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:4c8fcc5793dde01641a35905d6731ee1548f02b956815f8f1cab89e515a5bdf1", size = 489398, upload-time = "2026-01-14T23:14:43.741Z" }, + { url = "https://files.pythonhosted.org/packages/90/b0/7c2a74e74ef2a7c32de724658a69a862880e3e4155cba992ba04d1c70400/regex-2026.1.15-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:bfd876041a956e6a90ad7cdb3f6a630c07d491280bfeed4544053cd434901681", size = 291339, upload-time = "2026-01-14T23:14:45.183Z" }, + { url = "https://files.pythonhosted.org/packages/19/4d/16d0773d0c818417f4cc20aa0da90064b966d22cd62a8c46765b5bd2d643/regex-2026.1.15-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9250d087bc92b7d4899ccd5539a1b2334e44eee85d848c4c1aef8e221d3f8c8f", size = 289003, upload-time = "2026-01-14T23:14:47.25Z" }, + { url = "https://files.pythonhosted.org/packages/c6/e4/1fc4599450c9f0863d9406e944592d968b8d6dfd0d552a7d569e43bceada/regex-2026.1.15-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c8a154cf6537ebbc110e24dabe53095e714245c272da9c1be05734bdad4a61aa", size = 798656, upload-time = "2026-01-14T23:14:48.77Z" }, + { url = "https://files.pythonhosted.org/packages/b2/e6/59650d73a73fa8a60b3a590545bfcf1172b4384a7df2e7fe7b9aab4e2da9/regex-2026.1.15-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8050ba2e3ea1d8731a549e83c18d2f0999fbc99a5f6bd06b4c91449f55291804", size = 864252, upload-time = "2026-01-14T23:14:50.528Z" }, + { url = "https://files.pythonhosted.org/packages/6e/ab/1d0f4d50a1638849a97d731364c9a80fa304fec46325e48330c170ee8e80/regex-2026.1.15-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0bf065240704cb8951cc04972cf107063917022511273e0969bdb34fc173456c", size = 912268, upload-time = "2026-01-14T23:14:52.952Z" }, + { url = "https://files.pythonhosted.org/packages/dd/df/0d722c030c82faa1d331d1921ee268a4e8fb55ca8b9042c9341c352f17fa/regex-2026.1.15-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c32bef3e7aeee75746748643667668ef941d28b003bfc89994ecf09a10f7a1b5", size = 803589, upload-time = "2026-01-14T23:14:55.182Z" }, + { url = "https://files.pythonhosted.org/packages/66/23/33289beba7ccb8b805c6610a8913d0131f834928afc555b241caabd422a9/regex-2026.1.15-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d5eaa4a4c5b1906bd0d2508d68927f15b81821f85092e06f1a34a4254b0e1af3", size = 775700, upload-time = "2026-01-14T23:14:56.707Z" }, + { url = "https://files.pythonhosted.org/packages/e7/65/bf3a42fa6897a0d3afa81acb25c42f4b71c274f698ceabd75523259f6688/regex-2026.1.15-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:86c1077a3cc60d453d4084d5b9649065f3bf1184e22992bd322e1f081d3117fb", size = 787928, upload-time = "2026-01-14T23:14:58.312Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f5/13bf65864fc314f68cdd6d8ca94adcab064d4d39dbd0b10fef29a9da48fc/regex-2026.1.15-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:2b091aefc05c78d286657cd4db95f2e6313375ff65dcf085e42e4c04d9c8d410", size = 858607, upload-time = "2026-01-14T23:15:00.657Z" }, + { url = "https://files.pythonhosted.org/packages/a3/31/040e589834d7a439ee43fb0e1e902bc81bd58a5ba81acffe586bb3321d35/regex-2026.1.15-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:57e7d17f59f9ebfa9667e6e5a1c0127b96b87cb9cede8335482451ed00788ba4", size = 763729, upload-time = "2026-01-14T23:15:02.248Z" }, + { url = "https://files.pythonhosted.org/packages/9b/84/6921e8129687a427edf25a34a5594b588b6d88f491320b9de5b6339a4fcb/regex-2026.1.15-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:c6c4dcdfff2c08509faa15d36ba7e5ef5fcfab25f1e8f85a0c8f45bc3a30725d", size = 850697, upload-time = "2026-01-14T23:15:03.878Z" }, + { url = "https://files.pythonhosted.org/packages/8a/87/3d06143d4b128f4229158f2de5de6c8f2485170c7221e61bf381313314b2/regex-2026.1.15-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:cf8ff04c642716a7f2048713ddc6278c5fd41faa3b9cab12607c7abecd012c22", size = 789849, upload-time = "2026-01-14T23:15:06.102Z" }, + { url = "https://files.pythonhosted.org/packages/77/69/c50a63842b6bd48850ebc7ab22d46e7a2a32d824ad6c605b218441814639/regex-2026.1.15-cp312-cp312-win32.whl", hash = "sha256:82345326b1d8d56afbe41d881fdf62f1926d7264b2fc1537f99ae5da9aad7913", size = 266279, upload-time = "2026-01-14T23:15:07.678Z" }, + { url = "https://files.pythonhosted.org/packages/f2/36/39d0b29d087e2b11fd8191e15e81cce1b635fcc845297c67f11d0d19274d/regex-2026.1.15-cp312-cp312-win_amd64.whl", hash = "sha256:4def140aa6156bc64ee9912383d4038f3fdd18fee03a6f222abd4de6357ce42a", size = 277166, upload-time = "2026-01-14T23:15:09.257Z" }, + { url = "https://files.pythonhosted.org/packages/28/32/5b8e476a12262748851fa8ab1b0be540360692325975b094e594dfebbb52/regex-2026.1.15-cp312-cp312-win_arm64.whl", hash = "sha256:c6c565d9a6e1a8d783c1948937ffc377dd5771e83bd56de8317c450a954d2056", size = 270415, upload-time = "2026-01-14T23:15:10.743Z" }, +] + +[[package]] +name = "requests" +version = "2.34.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ac/c3/e2a2b89f2d3e2179abd6d00ebd70bff6273f37fb3e0cc209f48b39d00cbf/requests-2.34.2.tar.gz", hash = "sha256:f288924cae4e29463698d6d60bc6a4da69c89185ad1e0bcc4104f584e960b9ed", size = 142856, upload-time = "2026-05-14T19:25:27.735Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/f4/c67b0b3f1b9245e8d266f0f112c500d50e5b4e83cb6f3b71b6528104182a/requests-2.34.2-py3-none-any.whl", hash = "sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0", size = 73075, upload-time = "2026-05-14T19:25:26.443Z" }, +] + +[[package]] +name = "requests-oauthlib" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +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, 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, upload-time = "2024-03-22T20:32:28.055Z" }, +] + +[[package]] +name = "rich" +version = "14.3.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e9/67/cae617f1351490c25a4b8ac3b8b63a4dda609295d8222bad12242dfdc629/rich-14.3.4.tar.gz", hash = "sha256:817e02727f2b25b40ef56f5aa2217f400c8489f79ca8f46ea2b70dd5e14558a9", size = 230524, upload-time = "2026-04-11T02:57:45.419Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/76/6d163cfac87b632216f71879e6b2cf17163f773ff59c00b5ff4900a80fa3/rich-14.3.4-py3-none-any.whl", hash = "sha256:07e7adb4690f68864777b1450859253bed81a99a31ac321ac1817b2313558952", size = 310480, upload-time = "2026-04-11T02:57:47.484Z" }, +] + +[[package]] +name = "rpds-py" +version = "2026.6.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/aa/2a/9618a122aeb2a169a28b03889a2995fe297588964333d4a7d67bdf46e147/rpds_py-2026.6.3.tar.gz", hash = "sha256:1cebd1337c242e4ec2293e541f712b2da849b29f48f0c293684b71c0632625d4", size = 64051, upload-time = "2026-06-30T07:17:53.009Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/94/1f/a2dca5ffdbf1d475ffc4e80e4d5d720ff3a00f691795910116960ee12511/rpds_py-2026.6.3-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:7b689145a1485c335569bd056464f3243a29af7ed3871c7be31ad624ba239bc7", size = 342174, upload-time = "2026-06-30T07:14:54.821Z" }, + { url = "https://files.pythonhosted.org/packages/4d/dc/323d08583c0832911768663d1944f0107fcd4088704858d84b5e06d105a0/rpds_py-2026.6.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:db08f45aecde626498fb3df07bcf6d2ec040af42e859a4f5040d79c200342911", size = 345513, upload-time = "2026-06-30T07:14:56.515Z" }, + { url = "https://files.pythonhosted.org/packages/0b/2a/e31989834d18d2f26ec1d2774c5b1eb3331df4ea8ada525175294c94b48a/rpds_py-2026.6.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:acc992ab27b15f852c76755eb2ab7dce86585ddadba6fa5946e58556088845b4", size = 373783, upload-time = "2026-06-30T07:14:57.736Z" }, + { url = "https://files.pythonhosted.org/packages/87/fe/e80107ee3639585c9941c17d6a42cd65325022f656c023191fce78c324c8/rpds_py-2026.6.3-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7f88d653e7b3b779d71ae7454e20dcc9b6bae903f33c269db9f2be41bda3f261", size = 378316, upload-time = "2026-06-30T07:14:59.077Z" }, + { url = "https://files.pythonhosted.org/packages/22/6f/81e3adf81acfb6fa694de2a6e4e7d8863121e3e0799e0a7725e6cf5679c4/rpds_py-2026.6.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e52655eaf81e32593abedaa4bfe33170c8cfedf3365ed9be6e11e07f148f0278", size = 499423, upload-time = "2026-06-30T07:15:00.488Z" }, + { url = "https://files.pythonhosted.org/packages/2d/9a/41263969df0ce3d9af2a96d5005a288200af1989aed3354bfceb5fc0b21f/rpds_py-2026.6.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dfcc8b909769d19db55c7cc9541eb64b9b774b1057ffffb4f1048070475bb9f9", size = 386077, upload-time = "2026-06-30T07:15:01.911Z" }, + { url = "https://files.pythonhosted.org/packages/5e/19/7e98f468bd50346faff5b10e5297374b443bfdddacc8e9fbc65984539597/rpds_py-2026.6.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c1255b302953c86a486b81d330d5ee1d5bd937691ce271b6be0ef0e299eaab7", size = 371315, upload-time = "2026-06-30T07:15:03.317Z" }, + { url = "https://files.pythonhosted.org/packages/99/3c/2b973b4d371906a134b03decfea7f5d9835a2c6d263454392e15b64b5b18/rpds_py-2026.6.3-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:8d2294a31386bfa251d8c8a39472beee17db67d4f1a6eabea665d35c9a4461c3", size = 383502, upload-time = "2026-06-30T07:15:04.627Z" }, + { url = "https://files.pythonhosted.org/packages/98/2a/12e2799500af0a307bca76b63361c51f9fe479223561489c29eea1f2ee41/rpds_py-2026.6.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f8f23ead891a3b762f35ab3b04623da7056545b48aa60d59957e6789914545da", size = 402673, upload-time = "2026-06-30T07:15:05.856Z" }, + { url = "https://files.pythonhosted.org/packages/2d/e3/21e5872d165fe08be4f229e3d5ee9d90019c0bf0e5538de60dbd54009450/rpds_py-2026.6.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:421aba32367055614287a4292b6a17f1939c9452299f7a0209c117e990b646d4", size = 549964, upload-time = "2026-06-30T07:15:07.159Z" }, + { url = "https://files.pythonhosted.org/packages/1a/d0/5ee0fe36844297de8123bee27bc12078c1a7416ad9f1b8a8ca18d6b0c0ac/rpds_py-2026.6.3-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:1e5822dfc2f0d4ab7e745eaa6d85945069329beeccef965af3f3bb26058fcab6", size = 615446, upload-time = "2026-06-30T07:15:08.531Z" }, + { url = "https://files.pythonhosted.org/packages/b1/80/1ea5873cb683f2fbe5f21b23ea1f6d179ead19f3c5b249b7eb5dca568ef2/rpds_py-2026.6.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:83e35b57523816c8613fd0776b40cd8bb9f596b37ddd2692eb4a6bb5ab2f8c93", size = 576975, upload-time = "2026-06-30T07:15:09.97Z" }, + { url = "https://files.pythonhosted.org/packages/c9/e1/90ef639217a5ddb15b7f4f61b1c33911fd044ad03c311bafdd2bcab85582/rpds_py-2026.6.3-cp311-cp311-win32.whl", hash = "sha256:de3eceba0b683bcbb1ab93da016d0270df1f9ae7be716b40214c5dafac6ea45a", size = 204453, upload-time = "2026-06-30T07:15:11.324Z" }, + { url = "https://files.pythonhosted.org/packages/f2/b7/b7a1695d7af36f521fb11e80d6d3adbd744f73b921859bd3c2a2c0dc706f/rpds_py-2026.6.3-cp311-cp311-win_amd64.whl", hash = "sha256:2c54a076ca4d370980ab57bc0e31df57bbe8d41340436a90ef8b1219a3cbb127", size = 223219, upload-time = "2026-06-30T07:15:12.476Z" }, + { url = "https://files.pythonhosted.org/packages/d7/a2/145afacf796e4506062825941176ad9445c2dcf2b3b6a1f13d3030a15e19/rpds_py-2026.6.3-cp311-cp311-win_arm64.whl", hash = "sha256:168c733a7112e071bb7a66460e667edfcff06c017a3c523f7a8a8e08d0140804", size = 219137, upload-time = "2026-06-30T07:15:13.631Z" }, + { url = "https://files.pythonhosted.org/packages/5c/be/2e8974163072e7bab7df1a5acd54c4498e75e35d6d18b864d3a9d5dadc92/rpds_py-2026.6.3-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a0811d33247c3d6128a3001d763f2aa056bb3425204335400ac54f89eec3a0d0", size = 343691, upload-time = "2026-06-30T07:15:14.96Z" }, + { url = "https://files.pythonhosted.org/packages/a4/73/319dfa745dd668efe89309141ded489126461fcecd2b8f3a3cda185129b6/rpds_py-2026.6.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:538949e262e46caa31ac01bdb3c1e8f642622922cacbabbae6a8445d9dc33eaf", size = 338542, upload-time = "2026-06-30T07:15:16.267Z" }, + { url = "https://files.pythonhosted.org/packages/21/63/4239893be1c4d09b709b1a8f6be4188f0870084ff547f46606b8a75f1b03/rpds_py-2026.6.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:55927d532399c2c646100ff7feb48eaa940ad70f42cd68e1328f3ded9f81ca24", size = 368180, upload-time = "2026-06-30T07:15:17.62Z" }, + { url = "https://files.pythonhosted.org/packages/1c/ca/9c5de382225234ceb37b1844ebdb140db12b2a278bb9efe2fcd19f6c82ce/rpds_py-2026.6.3-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f56f1695bc5c0871cbc33dc0130fcf503aab0c57dcc5a6700a4f49eba4f2652e", size = 375067, upload-time = "2026-06-30T07:15:18.952Z" }, + { url = "https://files.pythonhosted.org/packages/87/dc/863f69d1bf04ade34b7fe0d59b9fdf6f0135fe2d7cbca74f1d665589559d/rpds_py-2026.6.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:270b293dae9058fc9fcedab50f13cebf46fb8ed1d1d54e0521a9da5d6b211975", size = 490509, upload-time = "2026-06-30T07:15:20.434Z" }, + { url = "https://files.pythonhosted.org/packages/ce/ef/eac16a12048b45ec7c7fa94f2be3438a5f26bf9cc8580b18a1cfd609b7f6/rpds_py-2026.6.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:127565fead0a10943b282957bd5447804ff3160ad79f2ad2635e6d249e380680", size = 382754, upload-time = "2026-06-30T07:15:21.831Z" }, + { url = "https://files.pythonhosted.org/packages/04/8f/d2f3f532616be4d06c316ef119683e832bd3d41e112bf3a88f4151c95b17/rpds_py-2026.6.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ecabd69db66de867690f9797f2f8fa27ba501bbc24540cbdbdc649cd15888ba6", size = 366189, upload-time = "2026-06-30T07:15:23.371Z" }, + { url = "https://files.pythonhosted.org/packages/e3/29/41a7b0e98a4b44cd676ab7598419623373eb43b20be68c084935c1a8cf88/rpds_py-2026.6.3-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:58eadac9cd119677b60e1cf8ac4052f35949d71b8a9e5556efccbe82533cf22a", size = 377750, upload-time = "2026-06-30T07:15:24.659Z" }, + { url = "https://files.pythonhosted.org/packages/2e/05/ecda0bec46f9a1565090bcdc941d023f6a25aff85fda28f89f8d19878152/rpds_py-2026.6.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7491ee23305ac3eb59e492b6945881f5cd77a6f731061a3f25b77fd40f9e99a4", size = 395576, upload-time = "2026-06-30T07:15:25.987Z" }, + { url = "https://files.pythonhosted.org/packages/68/a8/6ed52f03ee6cb854ce78785cc9a9a672eb880e83fd7224d471f667d151f1/rpds_py-2026.6.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2c99f7e8ccb3dd6e3e4bfeac657a7b208c9bac8075f4b078c02d7404c34107fa", size = 543807, upload-time = "2026-06-30T07:15:27.356Z" }, + { url = "https://files.pythonhosted.org/packages/8f/d6/156c0d3eea27ba09b92562ba2364ba124c0a061b199e17eac637cd25a5e2/rpds_py-2026.6.3-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:62698275682bf121181861295c9181e789030a2d516071f5b8f3c23c170cd0fc", size = 611187, upload-time = "2026-06-30T07:15:28.931Z" }, + { url = "https://files.pythonhosted.org/packages/f1/31/774212ed989c62f7f310220089f9b0a3fb8f40f5443d1727abd5d9f52bc9/rpds_py-2026.6.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a214c993455f99a89aaeadc9b21241900037adc9d97203e374d75513c5911822", size = 573030, upload-time = "2026-06-30T07:15:30.553Z" }, + { url = "https://files.pythonhosted.org/packages/c9/50/22f73127a41f1ce4f87fe39aadfb9a126345801c274aa93ae88456249327/rpds_py-2026.6.3-cp312-cp312-win32.whl", hash = "sha256:501f9f04a588d6a09179368c57071301445191767c64e4b52a6aa9871f1ef5ed", size = 202185, upload-time = "2026-06-30T07:15:32.027Z" }, + { url = "https://files.pythonhosted.org/packages/04/3a/f0ee4d4dde9d3b69dedf1b5f74e7a40017046d55052d173e418c6a94f960/rpds_py-2026.6.3-cp312-cp312-win_amd64.whl", hash = "sha256:2c958bf94822e9290a40aaf2a822d4bc5c88099093e3948ad6c571eca9272e5f", size = 220394, upload-time = "2026-06-30T07:15:33.359Z" }, + { url = "https://files.pythonhosted.org/packages/f3/83/3382fe37f809b59f02aac04dbc4e765b480b46ee0227ed516e3bdc4d3dfc/rpds_py-2026.6.3-cp312-cp312-win_arm64.whl", hash = "sha256:22bffe6042b9bcb0822bcd1955ec00e245daf17b4344e4ed8e9551b976b63e96", size = 215753, upload-time = "2026-06-30T07:15:34.778Z" }, + { url = "https://files.pythonhosted.org/packages/b4/9c/f0d19ac587fd0e4ab6b72cda355e9c5a6166b01ef7e064e437aef8eb9fef/rpds_py-2026.6.3-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:4cf2d36a2357e4d07bb5a4f98801265327b48256867816cfd2ceb001e9754a8f", size = 349791, upload-time = "2026-06-30T07:17:33.315Z" }, + { url = "https://files.pythonhosted.org/packages/38/c7/1d49d204c9fd2ee6c537601dc4c1ba921e03363ca576bfab94a00254ac9a/rpds_py-2026.6.3-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:30c6dc199b24a5e3e81d50da0f00858c5bbdb2617a750395687f4339c5818171", size = 352842, upload-time = "2026-06-30T07:17:34.897Z" }, + { url = "https://files.pythonhosted.org/packages/ac/e5/c0b5dc93cd0d4c06ce1f438907649514e2ea077bcd911e3154a51e96c38e/rpds_py-2026.6.3-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9891e594296ab9dada6551c8e7b387b2721f27a67eecd528412e8906247a7b90", size = 382094, upload-time = "2026-06-30T07:17:36.514Z" }, + { url = "https://files.pythonhosted.org/packages/0d/54/ec0e907b4ca8d541112db352409bd15f871c9b243e0c92c9b5a46ae96f01/rpds_py-2026.6.3-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b5c2dc92304aa48a4a60443b548bb12f12e119d4b72f314015e67b9e1be97fca", size = 388662, upload-time = "2026-06-30T07:17:38.235Z" }, + { url = "https://files.pythonhosted.org/packages/d3/f4/921c22a4fd0f1c1ac13a3996ffbf0aa67951e2c8ad0d1d9574938a2932e8/rpds_py-2026.6.3-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:127e08c0642d880cf32ca47ec2a4a77b901f7e2dd1ad9762adb13955d72ffcc9", size = 504896, upload-time = "2026-06-30T07:17:39.689Z" }, + { url = "https://files.pythonhosted.org/packages/0b/1b/a114b972cefa1ab1cdb3c7bb177cd3844a12826c507c722d3a73516dbbaf/rpds_py-2026.6.3-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8bb68f03f395eb793220b45c097bd4d8c32944393da0fad8b999efac0868fc8c", size = 391545, upload-time = "2026-06-30T07:17:41.336Z" }, + { url = "https://files.pythonhosted.org/packages/4e/98/af9b3db77d47fcbe6c8c1f36e2c2147ec70292819e99c325f871584a1c11/rpds_py-2026.6.3-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a3450b693fde92133e9f51060568a4c31fcca76d5e53bbd611e689ca446517e9", size = 380059, upload-time = "2026-06-30T07:17:42.857Z" }, + { url = "https://files.pythonhosted.org/packages/c9/ba/0efd8668b97c1d26a61566386c636a7a7a09829e474fdf807caa15a2c844/rpds_py-2026.6.3-pp311-pypy311_pp73-manylinux_2_31_riscv64.whl", hash = "sha256:5e8d07bddee435a2ff6f1920e18feff28d0bc4533e42f4bf6927fbd073312c41", size = 393235, upload-time = "2026-06-30T07:17:44.637Z" }, + { url = "https://files.pythonhosted.org/packages/62/90/8c139ee9690f73b0829f32647de6f40d826f8f443af6fa72644f96351aac/rpds_py-2026.6.3-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3a83ae6c67b7676b9878378547ca8e93ed77a580037bcbcd1d32f739e1e6089c", size = 413008, upload-time = "2026-06-30T07:17:46.225Z" }, + { url = "https://files.pythonhosted.org/packages/9c/97/0043896fdd7828ce09a1d9a8b06433714d0960fc4ff3fc4aa72b666b764e/rpds_py-2026.6.3-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:2bfd04c19ddbd6640de0b51894d764bd2758854d5b75bd102d2ef10cb9c293a9", size = 558118, upload-time = "2026-06-30T07:17:47.759Z" }, + { url = "https://files.pythonhosted.org/packages/f6/40/02355f0e134f783a8f9814c4680a1bd311d37671577a5964ea838573ff37/rpds_py-2026.6.3-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:ca6546b66be9dc4738b1b043d5ebd5488c66c578c5ff0fd0e8065313fe3afb76", size = 623138, upload-time = "2026-06-30T07:17:49.355Z" }, + { url = "https://files.pythonhosted.org/packages/10/85/48f0abdcef5cce4e034c7a5b0ceeceba0b01bf0d942824f4bb720afe2dec/rpds_py-2026.6.3-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:8e65860d238379ed982fd9ba690579b5e95af2f4840f99c772816dbe573cb826", size = 586486, upload-time = "2026-06-30T07:17:51.141Z" }, +] + +[[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, 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, 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, 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, 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, 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, upload-time = "2024-02-25T23:20:01.196Z" }, +] + +[[package]] +name = "soupsieve" +version = "2.8.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/47/2c/0a5f6f8ee0d5589e48c7640213ed5175d52cf540a06725b628cc1a45d6ce/soupsieve-2.8.4.tar.gz", hash = "sha256:e121fd02e975c695e4e9e8774a5ee35d74714b59307868dcc5319ad2d9e3328e", size = 121110, upload-time = "2026-05-24T13:55:57.154Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5e/f5/0c41cb68dcae6b7de4fac4188a3a9589e21fb31df21ea3a2e888db95e6c9/soupsieve-2.8.4-py3-none-any.whl", hash = "sha256:e7e6b0769c8f51ed59acab6e994b00621096cfb1c640a7509295987388fbaf65", size = 37304, upload-time = "2026-05-24T13:55:55.406Z" }, +] + +[[package]] +name = "sse-starlette" +version = "3.4.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "starlette" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d2/1b/bc9e3e7a72dcdad7dc7888758f5d00f56f8909ed5cfdff822bd72bb4c520/sse_starlette-3.4.5.tar.gz", hash = "sha256:83072538bc211a2f68b7b0422226c4af3e9b62e106e07034664b832ca019842a", size = 35249, upload-time = "2026-06-20T17:36:58.322Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/75/c88d3f5dafd59c791da1ce27650d30bf5b70cbf1cbf01cd00e5f9e360915/sse_starlette-3.4.5-py3-none-any.whl", hash = "sha256:e71bad53323f65573c3864a6c3bd0c1eb6e5f092b2e48082b0c35927d19ca296", size = 16518, upload-time = "2026-06-20T17:36:56.729Z" }, +] + +[[package]] +name = "starlette" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/eb/e3/7c1dc7381d9f8ab7d854328ebfa884e62cb3f3d8549ddfd37c7814f42afa/starlette-1.3.1.tar.gz", hash = "sha256:05d0213193f2fbaae60e2ecb593b4add4262ad4e46536b54abe36f11a71724e0", size = 2703240, upload-time = "2026-06-12T09:23:11.602Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/bb/2799cc2ede3ed41131f8975621e7213dfc7ef4acbbaadfa440f32500c370/starlette-1.3.1-py3-none-any.whl", hash = "sha256:c7372aae11c3c3f26a42df7bd626cec2f47d03483d261d369516a615a53714c6", size = 73632, upload-time = "2026-06-12T09:23:10.017Z" }, +] + +[[package]] +name = "tenacity" +version = "9.1.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/47/c6/ee486fd809e357697ee8a44d3d69222b344920433d3b6666ccd9b374630c/tenacity-9.1.4.tar.gz", hash = "sha256:adb31d4c263f2bd041081ab33b498309a57c77f9acf2db65aadf0898179cf93a", size = 49413, upload-time = "2026-02-07T10:45:33.841Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d7/c1/eb8f9debc45d3b7918a32ab756658a0904732f75e555402972246b0b8e71/tenacity-9.1.4-py3-none-any.whl", hash = "sha256:6095a360c919085f28c6527de529e76a06ad89b23659fa881ae0649b867a9d55", size = 28926, upload-time = "2026-02-07T10:45:32.24Z" }, +] + +[[package]] +name = "textual" +version = "8.2.8" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py", extra = ["linkify"] }, + { name = "mdit-py-plugins" }, + { name = "platformdirs" }, + { name = "pygments" }, + { name = "rich" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/00/21/39a76b01bd5eea82a04baaca7580e105d8c59450df03998345bb2cfb307b/textual-8.2.8.tar.gz", hash = "sha256:3f106a9fbc73e39dd266c9712432087de78a6d644084c7c241d6a25c3169115b", size = 1860502, upload-time = "2026-06-30T06:51:24.495Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fb/be/35261223d9416a0751cdff1c7b4a6f881387218a12d439fe22fefebc8c04/textual-8.2.8-py3-none-any.whl", hash = "sha256:267375fd402dc8d981457212efa71f0e3365fd17bba144ba9bb3ed7563cb374a", size = 731418, upload-time = "2026-06-30T06:51:26.364Z" }, +] + +[[package]] +name = "tiktoken" +version = "0.12.0" +source = { registry = "https://pypi.org/simple" } +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, upload-time = "2025-10-06T20:22:45.419Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/de/46/21ea696b21f1d6d1efec8639c204bdf20fde8bafb351e1355c72c5d7de52/tiktoken-0.12.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:6e227c7f96925003487c33b1b32265fad2fbcec2b7cf4817afb76d416f40f6bb", size = 1051565, upload-time = "2025-10-06T20:21:44.566Z" }, + { url = "https://files.pythonhosted.org/packages/c9/d9/35c5d2d9e22bb2a5f74ba48266fb56c63d76ae6f66e02feb628671c0283e/tiktoken-0.12.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c06cf0fcc24c2cb2adb5e185c7082a82cba29c17575e828518c2f11a01f445aa", size = 995284, upload-time = "2025-10-06T20:21:45.622Z" }, + { url = "https://files.pythonhosted.org/packages/01/84/961106c37b8e49b9fdcf33fe007bb3a8fdcc380c528b20cc7fbba80578b8/tiktoken-0.12.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:f18f249b041851954217e9fd8e5c00b024ab2315ffda5ed77665a05fa91f42dc", size = 1129201, upload-time = "2025-10-06T20:21:47.074Z" }, + { url = "https://files.pythonhosted.org/packages/6a/d0/3d9275198e067f8b65076a68894bb52fd253875f3644f0a321a720277b8a/tiktoken-0.12.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:47a5bc270b8c3db00bb46ece01ef34ad050e364b51d406b6f9730b64ac28eded", size = 1152444, upload-time = "2025-10-06T20:21:48.139Z" }, + { url = "https://files.pythonhosted.org/packages/78/db/a58e09687c1698a7c592e1038e01c206569b86a0377828d51635561f8ebf/tiktoken-0.12.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:508fa71810c0efdcd1b898fda574889ee62852989f7c1667414736bcb2b9a4bd", size = 1195080, upload-time = "2025-10-06T20:21:49.246Z" }, + { url = "https://files.pythonhosted.org/packages/9e/1b/a9e4d2bf91d515c0f74afc526fd773a812232dd6cda33ebea7f531202325/tiktoken-0.12.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a1af81a6c44f008cba48494089dd98cccb8b313f55e961a52f5b222d1e507967", size = 1255240, upload-time = "2025-10-06T20:21:50.274Z" }, + { url = "https://files.pythonhosted.org/packages/9d/15/963819345f1b1fb0809070a79e9dd96938d4ca41297367d471733e79c76c/tiktoken-0.12.0-cp311-cp311-win_amd64.whl", hash = "sha256:3e68e3e593637b53e56f7237be560f7a394451cb8c11079755e80ae64b9e6def", size = 879422, upload-time = "2025-10-06T20:21:51.734Z" }, + { url = "https://files.pythonhosted.org/packages/a4/85/be65d39d6b647c79800fd9d29241d081d4eeb06271f383bb87200d74cf76/tiktoken-0.12.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b97f74aca0d78a1ff21b8cd9e9925714c15a9236d6ceacf5c7327c117e6e21e8", size = 1050728, upload-time = "2025-10-06T20:21:52.756Z" }, + { url = "https://files.pythonhosted.org/packages/4a/42/6573e9129bc55c9bf7300b3a35bef2c6b9117018acca0dc760ac2d93dffe/tiktoken-0.12.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2b90f5ad190a4bb7c3eb30c5fa32e1e182ca1ca79f05e49b448438c3e225a49b", size = 994049, upload-time = "2025-10-06T20:21:53.782Z" }, + { url = "https://files.pythonhosted.org/packages/66/c5/ed88504d2f4a5fd6856990b230b56d85a777feab84e6129af0822f5d0f70/tiktoken-0.12.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:65b26c7a780e2139e73acc193e5c63ac754021f160df919add909c1492c0fb37", size = 1129008, upload-time = "2025-10-06T20:21:54.832Z" }, + { url = "https://files.pythonhosted.org/packages/f4/90/3dae6cc5436137ebd38944d396b5849e167896fc2073da643a49f372dc4f/tiktoken-0.12.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:edde1ec917dfd21c1f2f8046b86348b0f54a2c0547f68149d8600859598769ad", size = 1152665, upload-time = "2025-10-06T20:21:56.129Z" }, + { url = "https://files.pythonhosted.org/packages/a3/fe/26df24ce53ffde419a42f5f53d755b995c9318908288c17ec3f3448313a3/tiktoken-0.12.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:35a2f8ddd3824608b3d650a000c1ef71f730d0c56486845705a8248da00f9fe5", size = 1194230, upload-time = "2025-10-06T20:21:57.546Z" }, + { url = "https://files.pythonhosted.org/packages/20/cc/b064cae1a0e9fac84b0d2c46b89f4e57051a5f41324e385d10225a984c24/tiktoken-0.12.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:83d16643edb7fa2c99eff2ab7733508aae1eebb03d5dfc46f5565862810f24e3", size = 1254688, upload-time = "2025-10-06T20:21:58.619Z" }, + { url = "https://files.pythonhosted.org/packages/81/10/b8523105c590c5b8349f2587e2fdfe51a69544bd5a76295fc20f2374f470/tiktoken-0.12.0-cp312-cp312-win_amd64.whl", hash = "sha256:ffc5288f34a8bc02e1ea7047b8d041104791d2ddbf42d1e5fa07822cbffe16bd", size = 878694, upload-time = "2025-10-06T20:21:59.876Z" }, +] + +[[package]] +name = "tokenizers" +version = "0.23.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "huggingface-hub" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c1/60/21f715d9faba5f5407ff759472ade058ec4a507ad62bcea47cb847239a73/tokenizers-0.23.1.tar.gz", hash = "sha256:1feeeadf865a7915adc25445dea30e9933e593c31bb96c277cee36de227c8bfa", size = 365748, upload-time = "2026-04-27T14:43:25.606Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/87/39/b87a87d5bb9470610b80a2d31df42fcffeaf35118b8b97952b2aff598cc7/tokenizers-0.23.1-cp310-abi3-macosx_10_12_x86_64.whl", hash = "sha256:e03d6ffcbe0d56ee9c1ccd070e70a13fa750727c0277e138152acbc0252c2224", size = 3146732, upload-time = "2026-04-27T14:43:15.427Z" }, + { url = "https://files.pythonhosted.org/packages/e2/6a/068ed9f6e444c9d7e9d55ce134181325700f3d7f30410721bdc8f848d727/tokenizers-0.23.1-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:e0948bbb1ac1d7cdfc9fb6d62c596e3b7550036ad60ecd654a66ad273326324e", size = 3054954, upload-time = "2026-04-27T14:43:13.745Z" }, + { url = "https://files.pythonhosted.org/packages/6c/36/e006edf031154cba92b8416057d92c3abe3635e4c4b0aa0b5b9bb39dde70/tokenizers-0.23.1-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1bf13402aff9bc533c89cb849ec3b412dc3fbeacc9744840e423d7bf3f7dc0e3", size = 3374081, upload-time = "2026-04-27T14:43:01.241Z" }, + { url = "https://files.pythonhosted.org/packages/a2/ef/7735d226f9c7f874a6bee5e3f27fb25ecabdf207d37b8cf45286d0795893/tokenizers-0.23.1-cp310-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f836ca703b89ae07919a309f9651f7a88fd5a33d5f718ba5ad0870ec0256bad6", size = 3247641, upload-time = "2026-04-27T14:43:03.856Z" }, + { url = "https://files.pythonhosted.org/packages/b9/d9/24827036f6e21297bfffda0768e58eb6096a4f411e932964a01707857931/tokenizers-0.23.1-cp310-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ae848657742035523fdf261773630cb819a26995fcd3d9ecae0c1daf6e5a4959", size = 3585624, upload-time = "2026-04-27T14:43:10.664Z" }, + { url = "https://files.pythonhosted.org/packages/0c/9a/22f3582b3a4f49358293a5206e25317621ee4526bfe9cdaa0f07a12e770e/tokenizers-0.23.1-cp310-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:53b09e85775d5187941e7bab30e941b4134ab4a7dd8c68e783d231fb7ca27c51", size = 3844062, upload-time = "2026-04-27T14:43:05.643Z" }, + { url = "https://files.pythonhosted.org/packages/7e/65/b8f8814eef95800f20721384136d9a1d22241d50b2874357cb70542c392f/tokenizers-0.23.1-cp310-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ea5a0ce170074329faaa8ea3f6400ecde604b6678192688533af80980daae71a", size = 3460098, upload-time = "2026-04-27T14:43:08.854Z" }, + { url = "https://files.pythonhosted.org/packages/0d/d5/1353e5f677ec27c2494fb6a6725e82d56c985f53e90ec511369e7e4f02c6/tokenizers-0.23.1-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5075b405006415ea148a992d093699c66eb01952bf59f4d5727089a98bda45a4", size = 3346235, upload-time = "2026-04-27T14:43:12.377Z" }, + { url = "https://files.pythonhosted.org/packages/71/89/39b6b8fc073fb6d413d0147aa333dc7eff7be65639ac9d19930a0b21bf33/tokenizers-0.23.1-cp310-abi3-manylinux_2_31_riscv64.whl", hash = "sha256:56f3a77de629917652f876294dc9fe6bad4a0c43bc229dc72e59bb23a0f4729a", size = 3426398, upload-time = "2026-04-27T14:43:07.264Z" }, + { url = "https://files.pythonhosted.org/packages/0f/80/127c854da64827e5b79264ce524993a90dddcb320e5cd42412c5c02f9e8a/tokenizers-0.23.1-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:9d10a6d957ef01896dc274e890eee27d41bd0e74ef31e60616f0fc311345184e", size = 9823279, upload-time = "2026-04-27T14:43:17.222Z" }, + { url = "https://files.pythonhosted.org/packages/fe/ba/44c2502feb1a058f096ddfb4e0996ef3225a01a388e1a9b094e91689fe93/tokenizers-0.23.1-cp310-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:1974288a609c343774f1b897c8b482c791ab17b75ab5c8c2b1737565c1d82288", size = 9644986, upload-time = "2026-04-27T14:43:19.45Z" }, + { url = "https://files.pythonhosted.org/packages/9e/c1/464019a9fb059870bfe4eebb4ba12208f3042035e258bf5e782906bd3847/tokenizers-0.23.1-cp310-abi3-musllinux_1_2_i686.whl", hash = "sha256:120468fb4c24faf0543c835a4fabafa4deb3f20a035c9b6e83d0b553a97615d4", size = 9976181, upload-time = "2026-04-27T14:43:21.463Z" }, + { url = "https://files.pythonhosted.org/packages/79/94/3ac1432bda31626071e9b6a12709b97ae05131c804b94c8f3ac622c5da32/tokenizers-0.23.1-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:e3d8f40ea6268047de7046906326abed5134f27d4e8447b23763afe5808c8a96", size = 10113853, upload-time = "2026-04-27T14:43:23.617Z" }, + { url = "https://files.pythonhosted.org/packages/6a/dd/631b21433c771b1382535326f0eca80b9c9cee2e64961dd993bc9ac4669e/tokenizers-0.23.1-cp310-abi3-win32.whl", hash = "sha256:93120a930b919416da7cd10a2f606ac9919cc69cacae7980fa2140e277660948", size = 2536263, upload-time = "2026-04-27T14:43:29.888Z" }, + { url = "https://files.pythonhosted.org/packages/97/c9/2553f72aaf65a2797d4229e37fa7fbe38ffbf3e32912d31bdd78b3323e59/tokenizers-0.23.1-cp310-abi3-win_amd64.whl", hash = "sha256:e7bfaf995c1bdbbd21d13539decb6650967013759318627d85daeb7881af16b7", size = 2798223, upload-time = "2026-04-27T14:43:28.51Z" }, + { url = "https://files.pythonhosted.org/packages/cd/2b/2be299bab55fc595e3d38567edb1a87f86e594842968fa9515a07bdcf422/tokenizers-0.23.1-cp310-abi3-win_arm64.whl", hash = "sha256:a26197957d8e4425dfba746315f3c425ea00cfa8367c5fbc4ec73447893dcea9", size = 2664127, upload-time = "2026-04-27T14:43:26.949Z" }, +] + +[[package]] +name = "tomli" +version = "2.0.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/35/b9/de2a5c0144d7d75a57ff355c0c24054f965b2dc3036456ae03a51ea6264b/tomli-2.0.2.tar.gz", hash = "sha256:d46d457a85337051c36524bc5349dd91b1877838e2979ac5ced3e710ed8a60ed", size = 16096, upload-time = "2024-10-02T10:46:13.208Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cf/db/ce8eda256fa131af12e0a76d481711abe4681b6923c27efb9a255c9e4594/tomli-2.0.2-py3-none-any.whl", hash = "sha256:2ebe24485c53d303f690b0ec092806a085f07af5a5aa1464f3931eec36caaa38", size = 13237, upload-time = "2024-10-02T10:46:11.806Z" }, +] + +[[package]] +name = "tomli-w" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d4/19/b65f1a088ee23e37cdea415b357843eca8b1422a7b11a9eee6e35d4ec273/tomli_w-1.1.0.tar.gz", hash = "sha256:49e847a3a304d516a169a601184932ef0f6b61623fe680f836a2aa7128ed0d33", size = 6929, upload-time = "2024-10-08T11:13:29.279Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c4/ac/ce90573ba446a9bbe65838ded066a805234d159b4446ae9f8ec5bbd36cbd/tomli_w-1.1.0-py3-none-any.whl", hash = "sha256:1403179c78193e3184bfaade390ddbd071cba48a32a2e62ba11aae47490c63f7", size = 6440, upload-time = "2024-10-08T11:13:27.897Z" }, +] + +[[package]] +name = "tqdm" +version = "4.68.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ae/5f/57ff8b434839e70dab45601284ea413e947a63799891b7553e5960a793a8/tqdm-4.68.4.tar.gz", hash = "sha256:19829c9673638f2a0b8617da4cdcb927e831cd88bcfcb6e78d42a4d1af131520", size = 792418, upload-time = "2026-07-07T09:58:18.369Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/22/2a/5e5e750890ada51017d18d0d4c30da696e5b5bd3180947729927628fc3cb/tqdm-4.68.4-py3-none-any.whl", hash = "sha256:5168118b2368f48c561afda8020fd79195b1bdb0bdf8086b88442c267a315dc2", size = 676612, upload-time = "2026-07-07T09:58:16.256Z" }, +] + +[[package]] +name = "typer" +version = "0.26.8" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-doc" }, + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "rich" }, + { name = "shellingham" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7c/f7/68adc395201b20b872d68e975386832e8005ffeacedd43a1d837a32815be/typer-0.26.8.tar.gz", hash = "sha256:c244a6bd558886fe3f8780efb6bdd28bb9aff005a94eedebaa5cb32926fe2f7e", size = 202097, upload-time = "2026-06-26T09:22:45.705Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/80/87/b9fd69c92c6102a066e1b86a35243f53e70bd4c709f2a26d9f4fee4f4dc0/typer-0.26.8-py3-none-any.whl", hash = "sha256:3512ca79ac5c11113414b36e80281b872884477722440691c89d1112e321a49c", size = 122564, upload-time = "2026-06-26T09:22:44.72Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f6/cc/6253133b5bb138fc3306cebfbda2c520f545d36b5be2c7255cc528bb45d6/typing_extensions-4.16.0.tar.gz", hash = "sha256:dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5", size = 113555, upload-time = "2026-07-02T08:40:05.92Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/49/d3/b8441a820a491ddfc024b0b0cf0393375b75ea13866d9c66727e54c2fc80/typing_extensions-4.16.0-py3-none-any.whl", hash = "sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8", size = 45571, upload-time = "2026-07-02T08:40:04.659Z" }, +] + +[[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, 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, upload-time = "2025-10-01T02:14:40.154Z" }, +] + +[[package]] +name = "tzdata" +version = "2026.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/92/ff/5a28bdfd8c3ebec42564ac7d0e54ca3db65044a9314a97f9564fa7a1e926/tzdata-2026.3.tar.gz", hash = "sha256:4a1518b8993086a7982523e071643f3c0e5f213e75b21318e78bcabfff9d1415", size = 198674, upload-time = "2026-07-10T08:50:37.887Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e5/6d/b53b99a9f2766d095985947a5782f1702cabb129a34f7a802d7197af832f/tzdata-2026.3-py2.py3-none-any.whl", hash = "sha256:dc096730c87af6cab1b171c9d532be840741ff5d459015e7f6947bd7d7e54931", size = 348168, upload-time = "2026-07-10T08:50:36.46Z" }, +] + +[[package]] +name = "uc-micro-py" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/78/67/9a363818028526e2d4579334460df777115bdec1bb77c08f9db88f6389f2/uc_micro_py-2.0.0.tar.gz", hash = "sha256:c53691e495c8db60e16ffc4861a35469b0ba0821fe409a8a7a0a71864d33a811", size = 6611, upload-time = "2026-03-01T06:31:27.526Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/61/73/d21edf5b204d1467e06500080a50f79d49ef2b997c79123a536d4a17d97c/uc_micro_py-2.0.0-py3-none-any.whl", hash = "sha256:3603a3859af53e5a39bc7677713c78ea6589ff188d70f4fee165db88e22b242c", size = 6383, upload-time = "2026-03-01T06:31:26.257Z" }, +] + +[[package]] +name = "urllib3" +version = "2.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/53/0c/06f8b233b8fd13b9e5ee11424ef85419ba0d8ba0b3138bf360be2ff56953/urllib3-2.7.0.tar.gz", hash = "sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c", size = 433602, upload-time = "2026-05-07T16:13:18.596Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl", hash = "sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897", size = 131087, upload-time = "2026-05-07T16:13:17.151Z" }, +] + +[[package]] +name = "uv" +version = "0.11.28" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/a2/bfd6755b40682ef7e775ddb9d52823dea6551352f4244106da4bad37cd3c/uv-0.11.28.tar.gz", hash = "sha256:df86cfd135542a833e9f84708b3b8dbaa987a3b9db85b267062db49ab639d242", size = 5985690, upload-time = "2026-07-07T23:12:47.095Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/06/5d/507b829e79353fe3dcb2779cde8f64497d9c99f9b08b18b8f55ee3bf1786/uv-0.11.28-py3-none-linux_armv6l.whl", hash = "sha256:ae5bbdb6150adcd625f5fa720b04abf2014247d878d1035f19751bb0e7274543", size = 25893158, upload-time = "2026-07-07T23:11:55.77Z" }, + { url = "https://files.pythonhosted.org/packages/10/54/50c85a663ce723e061523ab4ac8b01b650077584e80950f9c93fd073979f/uv-0.11.28-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:bb11d94cb848ff58af79e0bb5e4037cd324d27dbe2dabb7746db698b724a9a20", size = 25041511, upload-time = "2026-07-07T23:11:58.885Z" }, + { url = "https://files.pythonhosted.org/packages/32/e1/49968cab72f16a7d6c45d095d319f9efbe8ce05f3f15c5f7104493694289/uv-0.11.28-py3-none-macosx_11_0_arm64.whl", hash = "sha256:e9eb317b1cdb249887df77ac232d8a9448f26858b2399f9f2949c6a7b9bedf88", size = 23570471, upload-time = "2026-07-07T23:12:01.832Z" }, + { url = "https://files.pythonhosted.org/packages/b8/4d/c9fe448dcd5cf65a5f054517aa42551b7f0920710a6891d98af321a06b22/uv-0.11.28-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:041e4b80bebc58d7142ac9394370cacd73185fd8d066d6675d14707d83408f6d", size = 25594677, upload-time = "2026-07-07T23:12:04.597Z" }, + { url = "https://files.pythonhosted.org/packages/07/aa/4c0c71075ba66cc594f856cbd98844058fcb53cb4dd8a6fccda8419562bb/uv-0.11.28-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.musllinux_1_1_armv7l.whl", hash = "sha256:185416a5316df8c5442b47178349f1f27fc1034468670ac1fb499eae3b25bd68", size = 25427944, upload-time = "2026-07-07T23:12:07.226Z" }, + { url = "https://files.pythonhosted.org/packages/6c/77/50fef66f4e26bf771429e1d88f849476d8ed21f0fb0708acaa53dc5772a5/uv-0.11.28-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a4a9fe246cb2882532277f5d5e5bd8a59462981462a2f98426f35ecfca82460e", size = 25448458, upload-time = "2026-07-07T23:12:10.894Z" }, + { url = "https://files.pythonhosted.org/packages/d0/93/720f45af65ebda460166dc64f3318acd65f7bd3a8e326fbd21810fd920ee/uv-0.11.28-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6f7ce6f6015a3e857bc6a663514afa62856b669ee5c1bd120e4c58ac2ef5513d", size = 26917218, upload-time = "2026-07-07T23:12:13.802Z" }, + { url = "https://files.pythonhosted.org/packages/cd/27/a9b68a15a5fe8db7103bea514c2adb79e9b1114fc8dc96fb39dbd7a5b898/uv-0.11.28-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6b3d0ea11e83b373a2166b82dd0864f5677fbadf98db64541ab2e59c42968905", size = 27771542, upload-time = "2026-07-07T23:12:16.519Z" }, + { url = "https://files.pythonhosted.org/packages/d4/fd/208607a7f5f86188775387fe0839ef97cf8d013e8d0e909140b7fdb7d0d1/uv-0.11.28-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8c60294e3be4fa203a04015fc02ac8a31d936e86fde06dcb43c7f8f22661dfff", size = 26972190, upload-time = "2026-07-07T23:12:19.191Z" }, + { url = "https://files.pythonhosted.org/packages/75/2e/62273ee6c9fbebccd8248c153b44870f81ebf5267c31edf4c095d78537fb/uv-0.11.28-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:49fe42df9f42056037473f3876adec1615709b57d3470ed39178ff420f3afb9f", size = 27127688, upload-time = "2026-07-07T23:12:22.43Z" }, + { url = "https://files.pythonhosted.org/packages/dd/8c/b15212904e6f0aa4a3709dc86838c6fa070fe97c7e96b3f10174a26b16e3/uv-0.11.28-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:fab3c31007a611866475824a666f5a721bf0c9335db806355a97fcfba2a6bbb7", size = 25715221, upload-time = "2026-07-07T23:12:25.144Z" }, + { url = "https://files.pythonhosted.org/packages/64/35/b83b7c599474aaf1277c2224c09679640c2320562155c4b6ece1c6f014c1/uv-0.11.28-py3-none-manylinux_2_31_riscv64.musllinux_1_1_riscv64.whl", hash = "sha256:2e91eb8a0b00d5f4427195fc818bcaa4d8bb4fccb79f4e973e74802419ab06ca", size = 26392793, upload-time = "2026-07-07T23:12:27.848Z" }, + { url = "https://files.pythonhosted.org/packages/81/49/8093318206dee51b5cfcabbf110892ff63cfd897a5df002e2d8b61350fe6/uv-0.11.28-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:47e3f12fe6f5c80a01639d8df36efde7bdddfc3bbc52250df623547d8d393105", size = 26522809, upload-time = "2026-07-07T23:12:30.757Z" }, + { url = "https://files.pythonhosted.org/packages/cf/c5/b26d82e9297c29c201f61698ee56bba956f94953b23089532d026a97d93f/uv-0.11.28-py3-none-musllinux_1_1_i686.whl", hash = "sha256:d01c7c665511c047f350e587b8b6557c96b61b2eddafbcd8964f0cc2f5b9afbe", size = 26156793, upload-time = "2026-07-07T23:12:33.449Z" }, + { url = "https://files.pythonhosted.org/packages/d0/c2/163e89424668d6c01499efbe85a854ad38f07834bde3f2b16df159eab1d5/uv-0.11.28-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:3fcfda468448093f4d5961ca8c068b0aeec2d02f7226d58ee8513321a929fe4f", size = 27327614, upload-time = "2026-07-07T23:12:36.252Z" }, + { url = "https://files.pythonhosted.org/packages/f0/c9/db4cb824777d013272ccfa77db07a4d12bf1584899458c1917a4b5a4069d/uv-0.11.28-py3-none-win32.whl", hash = "sha256:692edef9cf1d2dd69bb9d9fc01f281a82610547900ce227a3cb269cdf988b5ce", size = 24665179, upload-time = "2026-07-07T23:12:38.803Z" }, + { url = "https://files.pythonhosted.org/packages/40/bc/d67b18cddd54c503c7bad2b189a47fd7a1d07ea10b9212624f892b985498/uv-0.11.28-py3-none-win_amd64.whl", hash = "sha256:f4fcf2c8d9f1444b900e6b8dbbb828825fb76eca01acd18aeaa5c90240408cda", size = 27603677, upload-time = "2026-07-07T23:12:41.985Z" }, + { url = "https://files.pythonhosted.org/packages/57/94/dc31a771eac989973219c730552dbcf5bf7ea6652dba4ba89b1bbdc75a80/uv-0.11.28-py3-none-win_arm64.whl", hash = "sha256:e94560995737c50525d586da553521fbafe9ef06641e7d885db4b270f53ee84d", size = 25839294, upload-time = "2026-07-07T23:12:44.893Z" }, +] + +[[package]] +name = "uvicorn" +version = "0.51.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a2/65/b7c6c443ccc58678c91e1e973bbe2a878591538655d6e1d47f24ba1c51f3/uvicorn-0.51.0.tar.gz", hash = "sha256:f6f4b69b657c312f516dd2d268ab9ae6f254b11e4bac504f37b2ab58b24dd0b0", size = 94412, upload-time = "2026-07-08T10:59:05.962Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/45/ec/dbb7e5a6b91f86bfb9eb7d2988a2730907b6a729875b949c7f022e8b88fa/uvicorn-0.51.0-py3-none-any.whl", hash = "sha256:5d38af6cd620f2ae3849fb44fd4879e0890aa1febe8d47eb355fb45d93fe6a5b", size = 73219, upload-time = "2026-07-08T10:59:04.44Z" }, +] + +[package.optional-dependencies] +standard = [ + { 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, upload-time = "2025-10-16T22:17:19.342Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/d5/69900f7883235562f1f50d8184bb7dd84a2fb61e9ec63f3782546fdbd057/uvloop-0.22.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:c60ebcd36f7b240b30788554b6f0782454826a0ed765d8430652621b5de674b9", size = 1352420, upload-time = "2025-10-16T22:16:21.187Z" }, + { url = "https://files.pythonhosted.org/packages/a8/73/c4e271b3bce59724e291465cc936c37758886a4868787da0278b3b56b905/uvloop-0.22.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3b7f102bf3cb1995cfeaee9321105e8f5da76fdb104cdad8986f85461a1b7b77", size = 748677, upload-time = "2025-10-16T22:16:22.558Z" }, + { url = "https://files.pythonhosted.org/packages/86/94/9fb7fad2f824d25f8ecac0d70b94d0d48107ad5ece03769a9c543444f78a/uvloop-0.22.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:53c85520781d84a4b8b230e24a5af5b0778efdb39142b424990ff1ef7c48ba21", size = 3753819, upload-time = "2025-10-16T22:16:23.903Z" }, + { url = "https://files.pythonhosted.org/packages/74/4f/256aca690709e9b008b7108bc85fba619a2bc37c6d80743d18abad16ee09/uvloop-0.22.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:56a2d1fae65fd82197cb8c53c367310b3eabe1bbb9fb5a04d28e3e3520e4f702", size = 3804529, upload-time = "2025-10-16T22:16:25.246Z" }, + { url = "https://files.pythonhosted.org/packages/7f/74/03c05ae4737e871923d21a76fe28b6aad57f5c03b6e6bfcfa5ad616013e4/uvloop-0.22.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:40631b049d5972c6755b06d0bfe8233b1bd9a8a6392d9d1c45c10b6f9e9b2733", size = 3621267, upload-time = "2025-10-16T22:16:26.819Z" }, + { url = "https://files.pythonhosted.org/packages/75/be/f8e590fe61d18b4a92070905497aec4c0e64ae1761498cad09023f3f4b3e/uvloop-0.22.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:535cc37b3a04f6cd2c1ef65fa1d370c9a35b6695df735fcff5427323f2cd5473", size = 3723105, upload-time = "2025-10-16T22:16:28.252Z" }, + { 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, upload-time = "2025-10-16T22:16:29.436Z" }, + { 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, upload-time = "2025-10-16T22:16:30.493Z" }, + { 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, upload-time = "2025-10-16T22:16:31.644Z" }, + { 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, upload-time = "2025-10-16T22:16:32.917Z" }, + { 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, upload-time = "2025-10-16T22:16:34.015Z" }, + { 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, upload-time = "2025-10-16T22:16:35.149Z" }, +] + +[[package]] +name = "watchfiles" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cd/41/5e1a4bb12aac5f1493fa1bdc11154eca3b258ca4eba65d39c473fe19d8e9/watchfiles-1.2.0.tar.gz", hash = "sha256:c995fba777f1ea992f090f9236e9284cf7a5d1a0130dd5a3d82c598cacd76838", size = 108252, upload-time = "2026-05-18T04:32:04.251Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fc/3d/8024c801df84d1587740d0359e7fdd80afeae3d159011f3d5376dd82f18e/watchfiles-1.2.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:704fd259e332e01f9b9c178f4bce9e49027e5587cc2600eeeaf8e76e1c846201", size = 400242, upload-time = "2026-05-18T04:31:19.014Z" }, + { url = "https://files.pythonhosted.org/packages/87/5b/f4dfd45323e949984a3a7f9dc31d1cbb049921e7d98253488dda72ccdaa9/watchfiles-1.2.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6543cf55d170003296d185c0af981f3e1311564907e1f4e08671fc7693a890a5", size = 394562, upload-time = "2026-05-18T04:30:08.46Z" }, + { url = "https://files.pythonhosted.org/packages/98/d8/19483ef075d601c409bce8bcbb5c0f81a10876fff870400568f08ce484a1/watchfiles-1.2.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:89d8c2394a065ca86f5d2910ff263ae67c127e1376ccc4f9fc35c71db879f80a", size = 456611, upload-time = "2026-05-18T04:30:45.723Z" }, + { url = "https://files.pythonhosted.org/packages/b1/6a/cc81fbe7ee42f2f22e661a6e12def7807e01b14b2f39e0ff83fd373fd307/watchfiles-1.2.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:772b80df316480d894a0e3165fdd19cf77f5d17f9a787f94029465ad0e3529d1", size = 461379, upload-time = "2026-05-18T04:31:29.292Z" }, + { url = "https://files.pythonhosted.org/packages/b1/57/7e669002082c0a0f4fb5113bb70125f7110124b846b0a11bc5ae8e90eac1/watchfiles-1.2.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d158cd89df6053823533e06fb1d73c549133bff5f0396170c0e53d9559340717", size = 493556, upload-time = "2026-05-18T04:30:05.44Z" }, + { url = "https://files.pythonhosted.org/packages/45/7d/f60a2b19807b21fe8281f3a8da4f59eef0d5f96825ac4680ba2d4f2ebf91/watchfiles-1.2.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d516b3283a758e087841aedb8031549fb41ced08f3db10aa6d2bf32dc042525b", size = 575255, upload-time = "2026-05-18T04:30:40.568Z" }, + { url = "https://files.pythonhosted.org/packages/bd/49/77f5b5e6efbcd57482f74948ebb1b97e5c0046d6b61475042d830c84b3ff/watchfiles-1.2.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:53b2290c92e0506d102cd448fbc610d87079553f86caa39d67440856a8b8bba5", size = 467052, upload-time = "2026-05-18T04:31:17.942Z" }, + { url = "https://files.pythonhosted.org/packages/ee/5a/73e2959af1b97fd5d556f9a8bdba017be23ceeef731869d5eaa0a753d5a3/watchfiles-1.2.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a711b51aec4370d0dcda5b6c09463206f133a5759341d7744b953a7b62e1100e", size = 456858, upload-time = "2026-05-18T04:30:30.182Z" }, + { url = "https://files.pythonhosted.org/packages/50/57/1bc8c27fad7e6c19bddee15d276dbb6ab72480ec01c127afff1673aee417/watchfiles-1.2.0-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:e2ca07fa7d89195ec0865d3d285666286740bfa83d83e5cee204043a31ecc165", size = 467579, upload-time = "2026-05-18T04:32:15.897Z" }, + { url = "https://files.pythonhosted.org/packages/09/6c/3c2e44edba3553c5e3c3b8c8a2a6dee6b9e12ae2cf4bd2378bebf9dc3038/watchfiles-1.2.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e0618518f282c4ebff60f5e5b1247b6d91bb8b9f4476947563a1e74acc66f3c6", size = 633253, upload-time = "2026-05-18T04:31:37.123Z" }, + { url = "https://files.pythonhosted.org/packages/30/c2/d8c84a882ab39bbefcc4915ab3e91830b7a7e990c5570b0b69075aba3faf/watchfiles-1.2.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:0d191c054d0715c3c95c99df9b8dbf6fd096d8c1e021e8f212e1bd8bc444ccb5", size = 660713, upload-time = "2026-05-18T04:31:24.62Z" }, + { url = "https://files.pythonhosted.org/packages/a9/07/f97736a5fc605364fe67b25e9fa4a6965dfd4840d50c406ada507e9d735f/watchfiles-1.2.0-cp311-cp311-win32.whl", hash = "sha256:9342472aff9b093c5acd4f6d8f70ae0937964ab56542502bcf5579782da69ae8", size = 277222, upload-time = "2026-05-18T04:31:21.131Z" }, + { url = "https://files.pythonhosted.org/packages/cf/99/2b04981977fc2608afd60360d928c6aecf6b950292ca221d98f4005f6694/watchfiles-1.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:dbd6c97045dad81227c8d040173da044c1de08de64a5ea8b555da4aee1d5fa22", size = 290274, upload-time = "2026-05-18T04:31:45.966Z" }, + { url = "https://files.pythonhosted.org/packages/3c/74/f7f58a7075ee9cf612b0cfcddb78b8cd8234f0742d6f0075cf0da2dde1c6/watchfiles-1.2.0-cp311-cp311-win_arm64.whl", hash = "sha256:57a2d9fa4fb4c2ecae57b13dfff2c7ab53e21a2ba674fe9f05506680fcdcc0d7", size = 283460, upload-time = "2026-05-18T04:31:39.126Z" }, + { url = "https://files.pythonhosted.org/packages/b8/2f/e42c992d2afda3108ea1c02acecc991b9f31d05c14adc2a7cee9ee211fc4/watchfiles-1.2.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:bc13eb17538be00c874699dc0abe4ee2bc8d50bb1166a6b9e175ef3fd7eb8f26", size = 400115, upload-time = "2026-05-18T04:32:02.06Z" }, + { url = "https://files.pythonhosted.org/packages/5f/8f/6af2ea19065c91d8b0ea3516fdfc8c0d349f407e8e9fbf4e5a17360de8ad/watchfiles-1.2.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2d95ddc1eb6914154253d239089900813f6a767e174b8e6a50e7fdacb7e4236c", size = 393659, upload-time = "2026-05-18T04:30:50.951Z" }, + { url = "https://files.pythonhosted.org/packages/13/01/b32a967c56fb3e3e5be3db52c3d3b87fa4513aa367d8ed1ad96d42952e5f/watchfiles-1.2.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f70d8b291ef6e88d19b1f297a6905ddb978888d9272b0d05e6f53309856bcfc", size = 453207, upload-time = "2026-05-18T04:31:04.231Z" }, + { url = "https://files.pythonhosted.org/packages/04/98/97557a812180338cb1abd32e1cffcc4588f59b5f23e0cb006b2ba95ba64a/watchfiles-1.2.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:56d8641cf834c2836922899105bd3ce3d0dfc69291d52edf0b4d0436829b34c0", size = 459273, upload-time = "2026-05-18T04:31:50.377Z" }, + { url = "https://files.pythonhosted.org/packages/e8/a8/b4b08dcb7653b8087c6586f7ce649505900e866bbcfe40dc9587af02e686/watchfiles-1.2.0-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2581a94056e55d7d0a31a823ea92bf73749c489ca2285bfdc0fbe6b2bb49d50c", size = 489927, upload-time = "2026-05-18T04:31:42.485Z" }, + { url = "https://files.pythonhosted.org/packages/50/94/3dceea03545d2e5ddfd839f0ddd5e1cecbf1697b5a428d5ba11cef6af95d/watchfiles-1.2.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:41bc1199f7523b3f82843c88cbb979180c949caef0342cf90968f178e5d49b01", size = 570476, upload-time = "2026-05-18T04:31:03.071Z" }, + { url = "https://files.pythonhosted.org/packages/cc/f2/d39a5450c3532092b91f81d274360e613c2371bc874a89c7a1a3c5e8d138/watchfiles-1.2.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7571e4464cb6e434958f867f7f730b8ab0b75e3f8e5eac0499168486ab3c33a8", size = 465650, upload-time = "2026-05-18T04:30:12.701Z" }, + { url = "https://files.pythonhosted.org/packages/22/24/ed72f68cbc1333ca9b9f2200aa048bb6658ae41709bc1caad4310f4bdffd/watchfiles-1.2.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e53a384f76b631c3ae5334ce6a52f0baa3a911eb94a4eac7f160079868b716d5", size = 456398, upload-time = "2026-05-18T04:30:13.784Z" }, + { url = "https://files.pythonhosted.org/packages/0d/64/982ef4a4e5bab5b6e5b6becc8cd5e732f6130a78b855f0abec6439a9a135/watchfiles-1.2.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:d20029a60a71a052a24c4db7673bc4de39ab89adbaccbfb5d67987c5d73f424d", size = 465140, upload-time = "2026-05-18T04:31:52.111Z" }, + { url = "https://files.pythonhosted.org/packages/a0/0c/95282abf4ed680b6096010bcfc30c5fa7a041fc5aa5a2ad17a2cc6c75bba/watchfiles-1.2.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:2cb93af48550faf1cea04c303107c8b75833de7013e57ce27d3b8d21d8d0f58c", size = 630259, upload-time = "2026-05-18T04:31:25.676Z" }, + { url = "https://files.pythonhosted.org/packages/30/45/607c1de1530c4bdcf2cf1d1ecc2505ddba5d96bd43ba9f2b0e79876f850f/watchfiles-1.2.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:2995c176de7692b86a2e4c58d9ec718f753150a979cb4a754e2b4ffa38e70906", size = 659859, upload-time = "2026-05-18T04:30:24.333Z" }, + { url = "https://files.pythonhosted.org/packages/fa/08/d9e2e0f9e8e6791d33aefc694ad7eefa7f901f63caff84a81ded38692f9c/watchfiles-1.2.0-cp312-cp312-win32.whl", hash = "sha256:7a2cffd17d27d2ecbb310c2b1d8174f222a5495b1a721894afa88ec11e25b898", size = 275480, upload-time = "2026-05-18T04:30:31.307Z" }, + { url = "https://files.pythonhosted.org/packages/1c/e6/9d42569c0102645cc8cea5d8c7d8a1e9d4ada2cb7f05f75e554b8aa2202a/watchfiles-1.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:f155b3a1b2a5fc89cdc70d47ee5d54e3b75e88efa34982028a35daef9ba00379", size = 288718, upload-time = "2026-05-18T04:32:10.745Z" }, + { url = "https://files.pythonhosted.org/packages/0a/26/88e0dc6ee3898169d7fa22bb6a69cabf2502d2ee25cb8c876d1262d204f8/watchfiles-1.2.0-cp312-cp312-win_arm64.whl", hash = "sha256:8fa585ede612ee9f9e91b18bebf9ba11b9ae29a4e3a0d0cf6fca3e382133f0d5", size = 281026, upload-time = "2026-05-18T04:30:22.23Z" }, + { url = "https://files.pythonhosted.org/packages/23/f4/7513ef1e85fc4c6331b59479d6d72661fc391fbe543678052ac72c8b6c19/watchfiles-1.2.0-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:4674d49eb94706dfe666c069fc0a1b646ffcf920473492e209f6d5f60d3f0cc2", size = 403050, upload-time = "2026-05-18T04:30:36.753Z" }, + { url = "https://files.pythonhosted.org/packages/27/0b/a54103cfd732bb703c7a749222011a0483ef3705948dae3b203158601119/watchfiles-1.2.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:094b9b70103d4e963499bdea001ee3c2697b144cd9ae6218a62c0f89ec9e31db", size = 396629, upload-time = "2026-05-18T04:32:03.268Z" }, + { url = "https://files.pythonhosted.org/packages/5e/2c/73f31a3b893886206c3f54d73e8ad8dee58cdb2f69ad2622e0a8a9e07f4e/watchfiles-1.2.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b0ef001f8c25ad0fa9529f914c1600647ecd0f542d11c19b7894768c67b6acb7", size = 457318, upload-time = "2026-05-18T04:31:01.932Z" }, + { url = "https://files.pythonhosted.org/packages/e9/f9/45d021e4a5cc7b9dd567f7cbb06d3b75f751a690063fb6cc7ec60f4e46b7/watchfiles-1.2.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a88fc94e647bc4eec523f1caa540258eb71d14278b9daf72fa1e2658a98df0f0", size = 457771, upload-time = "2026-05-18T04:30:56.331Z" }, +] + +[[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, 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, upload-time = "2025-10-07T21:16:34.951Z" }, +] + +[[package]] +name = "websockets" +version = "16.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8c/02/b9a097e1e16fee4e2fd1ec8c39f6a9c5d6257bae8fa12640caf869f54436/websockets-16.1.tar.gz", hash = "sha256:299468cbe42e2b9981134c7c51d99387d8a7bf562b00183b3eec53f882846dad", size = 182530, upload-time = "2026-07-10T06:32:57.734Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9e/13/d47429afcc2c28616c32640009c84ea3f95660dab805766345b9682468e0/websockets-16.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:a9b1d7a63cba8e6b9b77e499a81eab29d31100298d090ad4507d1048c0b9cae0", size = 179770, upload-time = "2026-07-10T06:30:46.308Z" }, + { url = "https://files.pythonhosted.org/packages/6f/c7/2f0a722039a1e0107be73ed672ba604449b4956e48733e8e6b8a005aea42/websockets-16.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:bedbc5efeb96621aa2921d2d92608246691399418cac22acba427eb11877ea1f", size = 177455, upload-time = "2026-07-10T06:30:47.601Z" }, + { url = "https://files.pythonhosted.org/packages/43/6a/c26b0ae449e93d256ce5cdd50d5fe97b575a63e8dcd311a1faa972fd6bc6/websockets-16.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:fd847ab82133015afe65d778e7966ab42dba16bd7ad2e5b8a7918db6539f3f94", size = 177731, upload-time = "2026-07-10T06:30:49.102Z" }, + { url = "https://files.pythonhosted.org/packages/cc/3f/381550b344a02f0d2f84cda25e79b54575291bc7022128a41163fe8ba5b0/websockets-16.1-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:e2fb33ccb16ee40a95cc676d7b0ff451a9a2632f11a0dbc2e666326892b2e1de", size = 187066, upload-time = "2026-07-10T06:30:50.505Z" }, + { url = "https://files.pythonhosted.org/packages/4a/87/5ab1ec2086910f23cfb9ec0c1c29fbcc24a9d190b5198b1557c00ce4a47e/websockets-16.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:97f15b6d9ea9c2eaf6ccab964a082b09bfa6634a495bb0c2e9e7ee6943f58976", size = 188301, upload-time = "2026-07-10T06:30:51.835Z" }, + { url = "https://files.pythonhosted.org/packages/75/4b/bbbb8e6fac4cfc53d7aaa69a3d531bf10799354b0021f4b58914aced8c1a/websockets-16.1-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:638cf57c48b4ad8ac1ff1e453f4f97db2426b690ddc111e6da96b27b4a340bc3", size = 191594, upload-time = "2026-07-10T06:30:53.229Z" }, + { url = "https://files.pythonhosted.org/packages/5c/da/6c0c349443d6e999f481e3d9a0e57e7ac2956d75d6391bec24b92af3fe13/websockets-16.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2c1c85f61bc9d5eac57ce705d848dc2d2ce3680638300bf4e1da7d749e2cf4ce", size = 188862, upload-time = "2026-07-10T06:30:54.744Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ea/a368d37c010425a5451f42052fe804e754e23333e8448aef5d55c8a8d64f/websockets-16.1-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:eeab6d27f51c7e579023c971f5e6dff200deadf01faf6831beaecd32052dfaef", size = 187633, upload-time = "2026-07-10T06:30:56.055Z" }, + { url = "https://files.pythonhosted.org/packages/0d/4e/2ecd59add10d0855ec03dbdedfcdacdbd1aaabcd44b7dcbeda27538662e9/websockets-16.1-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:2ed64e5a97b0b97a0b66e18bfe281317a75fbbd5afe692f939ea8d14a4292f2c", size = 185089, upload-time = "2026-07-10T06:30:57.444Z" }, + { url = "https://files.pythonhosted.org/packages/6f/eb/c6c3dcd7a01097bb0d42f4e9ef21a2c2a491d36b77cd0870ab59f9e8e77f/websockets-16.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:9b3b021d0ed4bc16eea9775f62c9fa71acdacba0fc790b38581754dedf29ca60", size = 187790, upload-time = "2026-07-10T06:30:58.731Z" }, + { url = "https://files.pythonhosted.org/packages/9b/3e/775d36885d5e48ab8020aaf377de0ff5fbeb8bc2682a7e46419e4a14521c/websockets-16.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:6eb604a4167f0a0d53c2243dfc667a29f0b43c3436057184e070bb82a1000fa2", size = 186381, upload-time = "2026-07-10T06:31:00.355Z" }, + { url = "https://files.pythonhosted.org/packages/ad/90/6305c00812a92e47d0582604c02bd759db0118bbafc13f707d712dbcf898/websockets-16.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:9a3f125e44c3e34d61d111652e608e0f5b85ce08c225c8d56ad0eb822fa40030", size = 188193, upload-time = "2026-07-10T06:31:01.677Z" }, + { url = "https://files.pythonhosted.org/packages/f6/32/96bf8302c81d961585b4d34a2ddd3f229782f9b8c57bc78bbf98f1b1a4ac/websockets-16.1-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:8fdf0b00d0d1f30d1f06a92cab46fe542eec3eb302a7aee7163f142d0780f216", size = 185771, upload-time = "2026-07-10T06:31:03.062Z" }, + { url = "https://files.pythonhosted.org/packages/e8/1f/e8fe44b1d2dc417d740d9959d28fd2a846f268e7df38a686c04ac7dfe947/websockets-16.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:67b56828712f5fa7852de4c0265c28827311a657a4d275b7312ed0d1a918bee4", size = 186803, upload-time = "2026-07-10T06:31:04.34Z" }, + { url = "https://files.pythonhosted.org/packages/a5/29/b07d3a4e1eb2ab03e94e7f53f0c7a628e85fde6ad86011f7afd08f27b985/websockets-16.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:39c7e7730be33b8f0cd6f0aa8e8c82f9cdd1813f159765e073b2ece65f4824b5", size = 187041, upload-time = "2026-07-10T06:31:05.567Z" }, + { url = "https://files.pythonhosted.org/packages/a6/fd/e0abb8acc435642ac4a671490f6cf781c882f3fe682cdced9080ea455ab5/websockets-16.1-cp311-cp311-win32.whl", hash = "sha256:c54fe94fb2f11e11b48920c5f971e298cec73ac35db56efe57a49db63dfc95d4", size = 180158, upload-time = "2026-07-10T06:31:06.929Z" }, + { url = "https://files.pythonhosted.org/packages/81/06/85574d9458d3b913090087b817df0cc47b68e9a01dd0ab6ac04b77f49b0a/websockets-16.1-cp311-cp311-win_amd64.whl", hash = "sha256:f9f4fb9ae8b802e55609685db98382d48fd3feb1397804e1e774968dea0f28c7", size = 180456, upload-time = "2026-07-10T06:31:08.247Z" }, + { url = "https://files.pythonhosted.org/packages/a1/52/748c014f07f4e0e170c8932de7e647a1511d5ab3049cd978797136aee577/websockets-16.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:b6aa3f7ad345cf3862c21f4fbf2ef5e14d911348476c2845e137c091fe3a3f0b", size = 179798, upload-time = "2026-07-10T06:31:09.664Z" }, + { url = "https://files.pythonhosted.org/packages/8b/5e/2a2e64d977d084e49d37c187c26c056daaff41965be7300cd5dbde6f8b07/websockets-16.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b43fcfb521ac2f34ba80b7b8ea16303e4ad82dd8af667bf40839ad3a5d37b164", size = 177478, upload-time = "2026-07-10T06:31:11.072Z" }, + { url = "https://files.pythonhosted.org/packages/aa/12/5b85b4e75d697e548a94962ce5c036b05dd21cb9545759d555c5586422fc/websockets-16.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2bd3e12cd9afbe2baedae0b1eeade8ba64329b60fe2f9abdc966bd10fd2c2ef5", size = 177746, upload-time = "2026-07-10T06:31:12.386Z" }, + { url = "https://files.pythonhosted.org/packages/9d/62/79b1c8f0cee0da648b4899e1c5b0dbd3aa59846985136a54854db6827ab4/websockets-16.1-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:35f41979c8623df9bd30d949d82010a8fda5c56ff12cd8508a5b7272b6d4b53a", size = 187345, upload-time = "2026-07-10T06:31:13.754Z" }, + { url = "https://files.pythonhosted.org/packages/25/34/b7c5c52c2f24280e1c017acb7ad491a566750a5cceca7f3cf999373bba21/websockets-16.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a24d1f35aef07d794a16c853c688e74956c50239bec37b4f2de080056046419b", size = 188581, upload-time = "2026-07-10T06:31:15.075Z" }, + { url = "https://files.pythonhosted.org/packages/bc/37/604193bebcbeffe96fdf795960b83a15d600880c64dc17ec9c31c5b3427d/websockets-16.1-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0c64c024ddf7a35331b21fcddb562a039c275d2c82e8c2d12939e7da23997270", size = 191362, upload-time = "2026-07-10T06:31:16.395Z" }, + { url = "https://files.pythonhosted.org/packages/a5/b4/5ee27575b367d7110d4d13945e2a9de067ec84dc71e54b87f01e38550d9a/websockets-16.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c3e99757f5baafe20fc598e202ea6f5b0b265186ad38d0a17bd8beca16296955", size = 189216, upload-time = "2026-07-10T06:31:17.776Z" }, + { url = "https://files.pythonhosted.org/packages/7e/22/3e2dcc78d85fc5d9d814895ce6d07d0dfacc0f6aaa1d151f2b8c8d772299/websockets-16.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:353f3bc6e058ac1ccab4b3588e8598837a8c04cfc8351233e6d523be675d844c", size = 187971, upload-time = "2026-07-10T06:31:19.152Z" }, + { url = "https://files.pythonhosted.org/packages/9e/2f/cd271717b93d5ee19626cb5e38a85baab745c86e33db7c31a3ac729b31b8/websockets-16.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0352f5b38b40e857b6428d468fa21dbb4dd4a567d933c26d9831b4efe1b92f43", size = 185381, upload-time = "2026-07-10T06:31:20.665Z" }, + { url = "https://files.pythonhosted.org/packages/78/91/6ad6f2f1426317b5001bd490534208c7360636b35bac1dec2e0c22bfc40e/websockets-16.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:70bd789afab579602968c39f21cb925466505f3edff22f0ae852bca54978a4f9", size = 188015, upload-time = "2026-07-10T06:31:22.024Z" }, + { url = "https://files.pythonhosted.org/packages/c7/6d/533733132ab4c07540efd4a8f0b9a435d3a5059b2f26cc476ace1abf7f45/websockets-16.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:d0fb4b46f121eccd539353baebd1083a8767a9a351109453d1d1caecd1ba40c2", size = 186619, upload-time = "2026-07-10T06:31:23.376Z" }, + { url = "https://files.pythonhosted.org/packages/08/73/16c059f3d73b3331eba10793704afa4faa9939234fb08ef7dca35794e8f0/websockets-16.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c14b6634af01541e4efe2954fd8f263386f7aa6d37c01e55dd8109fd17661452", size = 188497, upload-time = "2026-07-10T06:31:25.024Z" }, + { url = "https://files.pythonhosted.org/packages/4d/89/9a8fae7dd2acdcfb1a8844c29fe42b518a04b64fce38a0923b6290e452f1/websockets-16.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:a58532c49a851bcb481e58c1be23b315c17fe2fbbed509d75aeea12f543d2c15", size = 186051, upload-time = "2026-07-10T06:31:26.291Z" }, + { url = "https://files.pythonhosted.org/packages/f6/40/b240c7dd6a0e0c59c1f68377cc3015263521080c327c15f5e753c1f6d378/websockets-16.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:4e969170c3b08e1d8dabd990fef1fa702c4233aeaabec33f871806e444f6a0e4", size = 187029, upload-time = "2026-07-10T06:31:27.605Z" }, + { url = "https://files.pythonhosted.org/packages/50/35/524e3fac40e47d6fdcf6c4b2c95ef1bc8a97e01593c90eff86621df7b716/websockets-16.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ff9b000064b88787ba9f7a3cb2af2b68a658ca5aad76458a46469e7124b678a0", size = 187308, upload-time = "2026-07-10T06:31:28.927Z" }, + { url = "https://files.pythonhosted.org/packages/00/13/56840cf62c8859af6ba22b9529da937332468c80f32b598753e8a66d3990/websockets-16.1-cp312-cp312-win32.whl", hash = "sha256:b9f5d83f80f4d7c4bba6d97f3755ac05850c784dce0fd2ab371c4e41172f53ff", size = 180161, upload-time = "2026-07-10T06:31:30.316Z" }, + { url = "https://files.pythonhosted.org/packages/d6/ff/87eb9eb44cb62424a8d729834f2b0515a47e2669fabec29820268f4d50a1/websockets-16.1-cp312-cp312-win_amd64.whl", hash = "sha256:6852c9f653966c16109d3b6f31181fd734f7914927e3f0fa1117af7a18c9aa21", size = 180462, upload-time = "2026-07-10T06:31:31.708Z" }, + { url = "https://files.pythonhosted.org/packages/4d/f4/84ef884775bbe77c46cce79bc7d705ea3bc6574cc00acf81af89754c077d/websockets-16.1-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:7289d899c79e763e6221c8dcb8959361cb43274418538d7c7ad16a43b01d12f9", size = 177387, upload-time = "2026-07-10T06:32:48.574Z" }, + { url = "https://files.pythonhosted.org/packages/d3/d9/6831ec6f65e1eeac770375f4f4b604f23df9bafaa1b47004bc5f9488d513/websockets-16.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:e22e9e3719f5131bd62da4db63c8da63eb8c91cc99e16c1cbd122f130e1ae07a", size = 177663, upload-time = "2026-07-10T06:32:50.043Z" }, + { url = "https://files.pythonhosted.org/packages/9d/d4/21d4922fa7fe855813a8b38f181a0ecf02a586e16c1f095fd05471f78cc2/websockets-16.1-pp311-pypy311_pp73-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:83bdabafef431247e6b11a9aab8a0893fd8e82e1ed95b32e0373625b03ffce4a", size = 178501, upload-time = "2026-07-10T06:32:51.439Z" }, + { url = "https://files.pythonhosted.org/packages/91/87/7a0320df854dacd09507ca972cb04a4dc5aae279583cc5b80ad5f5819533/websockets-16.1-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0b8d13ceabc5c60995f201b5211d76876e17e68706ebf5d3bc666b32eefff1a6", size = 179397, upload-time = "2026-07-10T06:32:52.892Z" }, + { url = "https://files.pythonhosted.org/packages/31/6a/0da1eb8c8da2ace7b578c8523d32618af85e62a9ebad56051d4a14a38a1c/websockets-16.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:81495f9c0085361c582efbc3207fb877174cfe03370f17d9cd70624404aa526f", size = 180546, upload-time = "2026-07-10T06:32:54.619Z" }, + { url = "https://files.pythonhosted.org/packages/66/58/bd83247f39ddc26ffc2c24eb05087a3b749e00cb4509fc6d19daa23c8495/websockets-16.1-py3-none-any.whl", hash = "sha256:c5149dfe490ec7e5ee5dbf624c642fb725f93a5575c7f00ab594ca9eddb8dd81", size = 174031, upload-time = "2026-07-10T06:32:56.079Z" }, +] + +[[package]] +name = "wrapt" +version = "2.2.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fe/a4/282c8e64300a59fc834518a54bf0afabb4ff9218b5fa76958b450459a844/wrapt-2.2.2.tar.gz", hash = "sha256:0788e321027c999bf221b667bd4a54aaefd1a36283749a860ac3eb77daed0302", size = 129068, upload-time = "2026-06-20T23:49:44.49Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/27/15/0c2d55168707465abfc41f33c0b23d792a5fa9b65c26983606940900a120/wrapt-2.2.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f1a2ff355ece6a111ca7a20dc86df6659c9205d3fcee674ca34f2a2854fd4e73", size = 80782, upload-time = "2026-06-20T23:47:44.367Z" }, + { url = "https://files.pythonhosted.org/packages/7d/b5/5c0b093eb48f8a062ef6267d3cb36e9bb1b88440181f6545a383c60efdf8/wrapt-2.2.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:55b9a899e6fff5444f229d30aa6e9ac92d2216d9d60f33c771b5d76a760d5f8e", size = 81678, upload-time = "2026-06-20T23:47:45.857Z" }, + { url = "https://files.pythonhosted.org/packages/34/f3/de70937472dd3e8a4e6811192f9c6075efdffd4a2cd9b4596bf160f89668/wrapt-2.2.2-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:a2d78c363f97d8bd718ee40432c66395685e9e98528ccaa423c3355d1715a26d", size = 159671, upload-time = "2026-06-20T23:47:47.345Z" }, + { url = "https://files.pythonhosted.org/packages/a5/ec/40aed2330e7f02ecf74386ffcfef9ccb7108c6a430f15b6a252b663b1bed/wrapt-2.2.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d619e1eed9bd4f6ed9f24cd61971aa086fa86505289628d464bcf8a2c2e3f328", size = 160785, upload-time = "2026-06-20T23:47:48.759Z" }, + { url = "https://files.pythonhosted.org/packages/45/04/aa5309beed5344b00220ae6b3b24055852192656194c27947bee1736306a/wrapt-2.2.2-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:518b0c5e323511ec56a38894802ddd5e1222626484e68efe63f201854ad788e5", size = 153699, upload-time = "2026-06-20T23:47:50.177Z" }, + { url = "https://files.pythonhosted.org/packages/01/df/2def7e99d1fe87eea413f95f671924cdddcb08823b1ffd212748dfa6d062/wrapt-2.2.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:4bccea5cdecffa9dd70e343741f0e41e0a16619313d04b72f78bb525162ebcd0", size = 159695, upload-time = "2026-06-20T23:47:51.602Z" }, + { url = "https://files.pythonhosted.org/packages/c7/f6/a906d01a2ce12157bad2404957b3e2140da354b8a70b2fa48bbf282871c0/wrapt-2.2.2-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:209112cafd963710a05d199aae431d79a28bc76eb8e6d1bbbb8ad24340722cae", size = 152813, upload-time = "2026-06-20T23:47:53.03Z" }, + { url = "https://files.pythonhosted.org/packages/02/49/bc0086292d239575b4c08f4cf8a4079fa58abbad58ec23abf84833a283ed/wrapt-2.2.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:e5a5290e4bf2f332fc29ce72ffb9a2fff678aaac047e2e9f5f7165cd7792e099", size = 158809, upload-time = "2026-06-20T23:47:54.391Z" }, + { url = "https://files.pythonhosted.org/packages/55/83/8fbd034de1f3e907edaa18786d5dd8f6932874edee0826c7cecb5cab03a1/wrapt-2.2.2-cp311-cp311-win32.whl", hash = "sha256:5499236ad1dc116012e2a5dd943f3f31af12fce452128e2bbcbd55a7d3d4d14c", size = 77414, upload-time = "2026-06-20T23:47:55.882Z" }, + { url = "https://files.pythonhosted.org/packages/7e/9c/23695baa331c6de4e874c3d78b8e0bed92e1d2a274e665b29858f6841672/wrapt-2.2.2-cp311-cp311-win_amd64.whl", hash = "sha256:8636809939152be6ae20a6cef0fed9fe60f411b47847d0426a826884b469e971", size = 80368, upload-time = "2026-06-20T23:47:57.237Z" }, + { url = "https://files.pythonhosted.org/packages/08/49/40cefc342bf89b234a4490d741290fce781774b831aefb39c25471da96c9/wrapt-2.2.2-cp311-cp311-win_arm64.whl", hash = "sha256:5d0a142f7af07caeb5e5da87493162a7b8efa19ba919e550a746f7446e13fb30", size = 79489, upload-time = "2026-06-20T23:47:58.56Z" }, + { url = "https://files.pythonhosted.org/packages/2a/85/180b40628b23772692a0c76e8030114e1c0ae068470ed531919f0a5f2a4a/wrapt-2.2.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:8417fd3c674d3c8023d080292d29301531a12daf8bd938dd419710dd2f464f2b", size = 81484, upload-time = "2026-06-20T23:47:59.924Z" }, + { url = "https://files.pythonhosted.org/packages/94/f2/21c90f2a16689702e2aaff45795b11018dff2c9b1242bac10d225483f676/wrapt-2.2.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0e7070c7472582e31af3dfc2622b2381a0df7435110a9388ed8db5ffbce67efb", size = 82151, upload-time = "2026-06-20T23:48:01.303Z" }, + { url = "https://files.pythonhosted.org/packages/5f/b3/7e6e9fcf4fe7e1b69a49fe6cc5a44e8224bab6283c5233c97e132f14908e/wrapt-2.2.2-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2e096c9d39a59b35b63c9aacfbbbec2088ff51ff1fc31051acc60a07f42f273a", size = 169828, upload-time = "2026-06-20T23:48:02.719Z" }, + { url = "https://files.pythonhosted.org/packages/0b/43/894f132d857ed5a9904d937baf368badcbe5ea9e436e2f1930fe21c9f1f0/wrapt-2.2.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6d1a6050405bf334be33bf66296f113563622972a34900ae6fa60fd283a1a900", size = 171544, upload-time = "2026-06-20T23:48:04.266Z" }, + { url = "https://files.pythonhosted.org/packages/29/de/3c833e03725b477e9ea34028224dd21a48781830101e4e036f77e8b6b102/wrapt-2.2.2-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:10adb01371408c6de504a6658b9886480f1a4919a83752748a387a504a21df79", size = 160663, upload-time = "2026-06-20T23:48:05.708Z" }, + { url = "https://files.pythonhosted.org/packages/33/be/27edce350b24e3054d9d047f65f16d4c4d4c1f3f31c4278a1f8a95c723c8/wrapt-2.2.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3442eee2a5798f9b451f1b2cd7518ce8b7e28a2a364696c414460a0e295c012a", size = 169387, upload-time = "2026-06-20T23:48:07.243Z" }, + { url = "https://files.pythonhosted.org/packages/e2/c4/9fd9679af8bf38e146652c7f47b6b352c3e5795b4ad1c0b7f94e15ac2aa7/wrapt-2.2.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:6c99012a22f735a85eed7c4b86a3e99c30fdd57d9e115b2b45f796264b58d0bf", size = 158849, upload-time = "2026-06-20T23:48:08.91Z" }, + { url = "https://files.pythonhosted.org/packages/bc/c2/aa6c0c2206803068c6859dabe01f8c84c43744da93d4c67b8946d21655ee/wrapt-2.2.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3b686cfc008776a3952d6213cb296ed7f45d782a8453936406faa89eac0835ab", size = 168147, upload-time = "2026-06-20T23:48:10.374Z" }, + { url = "https://files.pythonhosted.org/packages/42/63/3eb25da41049d20ae18fcab2dd8b056e02387c4bfa626cbdfb7c3b872e4f/wrapt-2.2.2-cp312-cp312-win32.whl", hash = "sha256:ef2cce266b5b0b07e19fa82e59673b81142b7a3607c8ed1254113d048ed668da", size = 77734, upload-time = "2026-06-20T23:48:11.769Z" }, + { url = "https://files.pythonhosted.org/packages/da/09/0390e008a305360948fa9ce69507d041ac12cb2ee5d28e34467e2ee79391/wrapt-2.2.2-cp312-cp312-win_amd64.whl", hash = "sha256:abf8c20a2d72ee69e16328b3c91342c446e723bfe48bfcc4dded3b9722ac027f", size = 80585, upload-time = "2026-06-20T23:48:13.117Z" }, + { url = "https://files.pythonhosted.org/packages/d3/b3/84c445c66969f2d3457276b183a48c91097d59bbef9af6c075366b0f8c36/wrapt-2.2.2-cp312-cp312-win_arm64.whl", hash = "sha256:c6c64c5d02578bc4c4bca4f0aef1504de933c1d5b4ac2710b9131111459506c8", size = 79553, upload-time = "2026-06-20T23:48:14.5Z" }, + { url = "https://files.pythonhosted.org/packages/6e/d2/6317eb6d4554855bbf12d61857774af34747bf88a42c19bf306de67e2fa3/wrapt-2.2.2-py3-none-any.whl", hash = "sha256:5bad217350f19ce99ca5b5e71d406765ea86fe541628426772b657375ee1c048", size = 61460, upload-time = "2026-06-20T23:49:42.966Z" }, +] + +[[package]] +name = "yarl" +version = "1.24.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, + { name = "multidict" }, + { name = "propcache" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/79/12/1e8f37460ea0f7eb59c221fdaf0ed75e7ac43e97f8093b9c6f411df50a78/yarl-1.24.2.tar.gz", hash = "sha256:9ac374123c6fd7abf64d1fec93962b0bd4ee2c19751755a762a72dd96c0378f8", size = 210798, upload-time = "2026-05-19T21:31:05.599Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c5/c5/1ce244152ff2839645e7cae92f90e7bafcb2c52bea7ff586ac714f14f5df/yarl-1.24.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:36348bebb147b83818b9d7e673ea4debc75970afc6ffdc7e3975ad05ce5a58c1", size = 128971, upload-time = "2026-05-19T21:28:20.543Z" }, + { url = "https://files.pythonhosted.org/packages/87/5a/00f36967203ed89cb3acd2c8ed526cc3fed9418eb70ce128160a911c8499/yarl-1.24.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1a97e42c8a2233f2f279ecadd9e4a037bcb5d813b78435e8eedd4db5a9e9708c", size = 91507, upload-time = "2026-05-19T21:28:22.556Z" }, + { url = "https://files.pythonhosted.org/packages/31/d0/1fb0c1cd27288f39f6974da4318c32768d72c9890984541fdf1e2e32a51d/yarl-1.24.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8d027d56f1035e339d1001ac33eceab5b2ec8e42e449787bb75e289fb9a5cd1d", size = 91343, upload-time = "2026-05-19T21:28:24.092Z" }, + { url = "https://files.pythonhosted.org/packages/03/ce/d4a646508bed2f8dec6435b40166fe9308dd191262033d3f307b2bbcaecd/yarl-1.24.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0a6377060e7927187a42b7eb202090cbe2b34933a4eeaf90e3bd9e33432e5cae", size = 105704, upload-time = "2026-05-19T21:28:25.872Z" }, + { url = "https://files.pythonhosted.org/packages/4b/07/b3278e82d8bc41485bcf6d856cd0433262593de615b1d3dc43bd3f5bead4/yarl-1.24.2-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:17076578bce0049a5ce57d14ad1bded391b68a3b213e9b81b0097b090244999a", size = 97281, upload-time = "2026-05-19T21:28:27.352Z" }, + { url = "https://files.pythonhosted.org/packages/17/5b/4cee6e7c92e487bebe7afc797da0aa54a248ab4e776a68fe369ec29665a5/yarl-1.24.2-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:50713f1d4d6be6375bb178bb43d140ee1acb8abe589cd723320b7925a275be1e", size = 114020, upload-time = "2026-05-19T21:28:29.458Z" }, + { url = "https://files.pythonhosted.org/packages/5c/82/111076571545a7d4f9cca3fbd5c6f40615af58642be09f12328f48022468/yarl-1.24.2-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:34263e2fa8fb5bb63a0d97706cda38edbad62fddb58c7f12d6acbc092812aa50", size = 111450, upload-time = "2026-05-19T21:28:31.262Z" }, + { url = "https://files.pythonhosted.org/packages/b6/ec/08f671f69a444d704aeecebf92af659b67b97a869942411d0a578b08c334/yarl-1.24.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:49016d82f032b1bd1e10b01078a7d29ae71bf468eeae0ea22df8bab691e60003", size = 106384, upload-time = "2026-05-19T21:28:32.856Z" }, + { url = "https://files.pythonhosted.org/packages/e5/86/ce41e7a7a199340b2330d52b60f25c4074b6636dd0e60b1a80d31a9db042/yarl-1.24.2-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3f6d2c216318f8f32038ca3f72501ba08536f0fd18a36e858836b121b2deed9f", size = 106153, upload-time = "2026-05-19T21:28:35.222Z" }, + { url = "https://files.pythonhosted.org/packages/c4/5d/31be8a729531ab3e55ac3e7e5c800be8c89ea98947f418b2f6ea259fb6ee/yarl-1.24.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:08d3a33218e0c64393e7610284e770409a9c31c429b078bcb24096ed0a783b8f", size = 105322, upload-time = "2026-05-19T21:28:36.642Z" }, + { url = "https://files.pythonhosted.org/packages/47/9b/b57afb22b386ae87ac9940f09878b98d8c333f89113e6fc96fcf4ca9eb64/yarl-1.24.2-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:5d699376c4ca3cba49bbfae3a05b5b70ded572937171ce1e0b8d87118e2ba294", size = 99057, upload-time = "2026-05-19T21:28:38.386Z" }, + { url = "https://files.pythonhosted.org/packages/a3/4f/06348c27c8389256c313e8a57d796808fc0264c915dd5e7cfd3c0e314dc7/yarl-1.24.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:a1cab588b4fa14bea2e55ebea27478adfb05372f47573738e1acc4a36c0b05d2", size = 113502, upload-time = "2026-05-19T21:28:40.091Z" }, + { url = "https://files.pythonhosted.org/packages/5f/1c/284f307b298e4a17b7943b07d9d7ecc4151537f8d137ba51f3bb6c31ca20/yarl-1.24.2-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:ec87ccc31bd21db7ad009d8572c127c1000f268517618a4cc09adba3c2a7f21c", size = 105253, upload-time = "2026-05-19T21:28:41.987Z" }, + { url = "https://files.pythonhosted.org/packages/c8/bf/0de123bec8619e45c80cbded9085f61b5b4a9eddb8abe6d25d28ee1ec866/yarl-1.24.2-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:d1dd47a22843b212baa8d74f37796815d43bd046b42a0f41e9da433386c3136b", size = 111345, upload-time = "2026-05-19T21:28:43.93Z" }, + { url = "https://files.pythonhosted.org/packages/90/af/0248eb065e51129d2a9b2436cd1b5c772c19a6b04e5b6a186955671e3319/yarl-1.24.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:7b54b9c67c2b06bd7b9a77253d242124b9c95d2c02def5a1144001ee547dd9d5", size = 106558, upload-time = "2026-05-19T21:28:45.806Z" }, + { url = "https://files.pythonhosted.org/packages/21/3c/f960d7a65ef97d8ba9b424fb5128796a4bc710fc6df2ddbbd7dfdc3bbd20/yarl-1.24.2-cp311-cp311-win_amd64.whl", hash = "sha256:f8fdbcff8b2c7c9284e60c196f693588598ddcee31e11c18e14949ce44519d45", size = 92808, upload-time = "2026-05-19T21:28:48.465Z" }, + { url = "https://files.pythonhosted.org/packages/03/1a/49fb03750e4de4d2284cd5b885a383133c34eef45bd59631b2bb8b7e81e8/yarl-1.24.2-cp311-cp311-win_arm64.whl", hash = "sha256:b32c37a7a337e90822c45797bf3d79d60875cfcccd3ecc80e9f453d87026c122", size = 87610, upload-time = "2026-05-19T21:28:50.07Z" }, + { url = "https://files.pythonhosted.org/packages/f0/da/866bcb01076ba49d2b42b309867bed3826421f1c479655eb7a607b44f20b/yarl-1.24.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:b975866c184564c827e0877380f0dae57dcca7e52782128381b72feff6dfceb8", size = 129957, upload-time = "2026-05-19T21:28:51.695Z" }, + { url = "https://files.pythonhosted.org/packages/bf/1d/fcefb70922ea2268a8971d8e5874d9a8218644200fb8465f1dcad55e6851/yarl-1.24.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3b075301a2836a0e297b1b658cb6d6135df535d62efefdd60366bd589c2c82f2", size = 92164, upload-time = "2026-05-19T21:28:53.242Z" }, + { url = "https://files.pythonhosted.org/packages/29/b6/170e2b8d4e3bc30e6bfdcca53556537f5bf595e938632dfcb059311f3ff6/yarl-1.24.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8ae44649b00947634ab0dab2a374a638f52923a6e67083f2c156cd5cbd1a881d", size = 91688, upload-time = "2026-05-19T21:28:54.865Z" }, + { url = "https://files.pythonhosted.org/packages/fe/a5/c9f655d5553ea0b99fdac9d6a99ad3f9b3e73b8e5758bb46f58c9831f74c/yarl-1.24.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:507cc19f0b45454e2d6dcd62ff7d062b9f77a2812404e62dbdaec05b50faa035", size = 102902, upload-time = "2026-05-19T21:28:56.963Z" }, + { url = "https://files.pythonhosted.org/packages/5d/bc/6b9664d815d79af4ee553337f9d606c56bbf269186ada9172de45f1b5f60/yarl-1.24.2-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c4c17bad5a530912d2111825d3f05e89bab2dd376aaa8cbc77e449e6db63e576", size = 97931, upload-time = "2026-05-19T21:28:58.56Z" }, + { url = "https://files.pythonhosted.org/packages/98/ec/32ba48acae30fecd60928f5791188b80a9d6ee3840507ffda29fecd37b71/yarl-1.24.2-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f5f0cbb112838a4a293985b6ed73948a547dadcc1ba6d2089938e7abdedceef8", size = 111030, upload-time = "2026-05-19T21:29:00.148Z" }, + { url = "https://files.pythonhosted.org/packages/82/5a/6f4cd081e5f4934d2ae3a8ef4abe3afacc010d26f0035ee91b35cd7d7c37/yarl-1.24.2-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5ec8356b8a6afcf81fc7aeeef13b1ff7a49dec00f313394bbb9e83830d32ccd7", size = 110392, upload-time = "2026-05-19T21:29:02.155Z" }, + { url = "https://files.pythonhosted.org/packages/7a/da/323a01c349bd5fb01bb6652e314d9bb218cee630a736bdb810ad50e4013f/yarl-1.24.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7e7ebcdef69dec6c6451e616f32b622a6d4a2e92b445c992f7c8e5274a6bbc4c", size = 105612, upload-time = "2026-05-19T21:29:04.247Z" }, + { url = "https://files.pythonhosted.org/packages/7c/80/264ab684f181e1a876389374519ff05d10248725535ae2ac4e8ac4e563d6/yarl-1.24.2-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:47a55d6cf6db2f401017a9e96e5288844e5051911fb4e0c8311a3980f5e59a7d", size = 104487, upload-time = "2026-05-19T21:29:06.491Z" }, + { url = "https://files.pythonhosted.org/packages/41/07/efabe5df87e96d7ad5959760b888344be48cd6884db127b407c6b5503adc/yarl-1.24.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3065657c80a2321225e804048597ad55658a7e76b32d6f5ee4074d04c50401db", size = 102333, upload-time = "2026-05-19T21:29:08.267Z" }, + { url = "https://files.pythonhosted.org/packages/44/0c/bcf7c42603e1009295f586d8890f2ba032c8b53310e815adf0a202c73d9f/yarl-1.24.2-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:cb84b80d88e19ede158619b80813968713d8d008b0e2497a576e6a0557d50712", size = 99025, upload-time = "2026-05-19T21:29:10.682Z" }, + { url = "https://files.pythonhosted.org/packages/4f/82/84482ab1a57a0f21a08afe6a7004c61d741f8f2ecc3b05c321577c612164/yarl-1.24.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:990de4f680b1c217e77ff0d6aa0029f9eb79889c11fb3e9a3942c7eba29c1996", size = 110507, upload-time = "2026-05-19T21:29:12.954Z" }, + { url = "https://files.pythonhosted.org/packages/c4/8d/a546ba1dfe1b0f290e05fef145cd07614c0f15df1a707195e512d1e39d1d/yarl-1.24.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:abb8ec0323b80161e3802da3150ef660b41d0e9be2048b76a363d93eee992c2b", size = 103719, upload-time = "2026-05-19T21:29:14.893Z" }, + { url = "https://files.pythonhosted.org/packages/1a/b6/267f2a09213138473adfce6b8a6e17791d7fee70bd4d9003218e4dec58b0/yarl-1.24.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:e7977781f83638a4c73e0f88425563d70173e0dfd90ac006a45c65036293ee3c", size = 110438, upload-time = "2026-05-19T21:29:16.485Z" }, + { url = "https://files.pythonhosted.org/packages/48/2d/1c8d89c7c5f9cad9fb2902445d94e2ab1d7aa35de029afbb8ae95c42d00f/yarl-1.24.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e30dd55825dc554ec5b66a94953b8eda8745926514c5089dfcacecb9c99b5bd1", size = 105719, upload-time = "2026-05-19T21:29:18.367Z" }, + { url = "https://files.pythonhosted.org/packages/a7/25/722e3b93bd687009afb2d59a35e13d30ddd8f80571445bb0c4e4ce26ec66/yarl-1.24.2-cp312-cp312-win_amd64.whl", hash = "sha256:7dafe10c12ddd4d120d528c4b5599c953bd7b12845347d507b95451195bb6cad", size = 92901, upload-time = "2026-05-19T21:29:20.014Z" }, + { url = "https://files.pythonhosted.org/packages/39/47/4486ccfb674c04854a1ef8aa77868b6a6f765feaf69633409d7ca4f02cb8/yarl-1.24.2-cp312-cp312-win_arm64.whl", hash = "sha256:044a09d8401fcf8681977faef6d286b8ade1e2d2e9dceda175d1cfa5ca496f30", size = 87229, upload-time = "2026-05-19T21:29:22.1Z" }, + { url = "https://files.pythonhosted.org/packages/fd/4d/4b880086bd0d3e034d25647be1d830afc3e3f610e98c4ab3490af6b1b6d5/yarl-1.24.2-py3-none-any.whl", hash = "sha256:2783d9226db8797636cd6896e4de81feed252d1db72265686c9558d97a4d94b9", size = 53576, upload-time = "2026-05-19T21:31:03.909Z" }, +] + +[[package]] +name = "youtube-transcript-api" +version = "1.2.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "defusedxml" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/60/43/4104185a2eaa839daa693b30e15c37e7e58795e8e09ec414f22b3db54bec/youtube_transcript_api-1.2.4.tar.gz", hash = "sha256:b72d0e96a335df599d67cee51d49e143cff4f45b84bcafc202ff51291603ddcd", size = 469839, upload-time = "2026-01-29T09:09:17.088Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/be/95/129ea37efd6cd6ed00f62baae6543345c677810b8a3bf0026756e1d3cf3c/youtube_transcript_api-1.2.4-py3-none-any.whl", hash = "sha256:03878759356da5caf5edac77431780b91448fb3d8c21d4496015bdc8a7bc43ff", size = 485227, upload-time = "2026-01-29T09:09:15.427Z" }, +] + +[[package]] +name = "zipp" +version = "4.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b9/d8/eab98a517c14134c0b2eb4e2387bc5f457334293ec5d2dd3857ec2966802/zipp-4.1.0.tar.gz", hash = "sha256:4cb57381f544315db7688e976e922a2b18cdb513d21cc194eb42232ba2a3e602", size = 26214, upload-time = "2026-05-18T20:08:57.967Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3a/13/547360d81e6d88d58492968ffda9f9542854f11310ee556fef14260cc886/zipp-4.1.0-py3-none-any.whl", hash = "sha256:25ad4e16390cd314347dd8f1de67a2ac538ae658ed4ab9db16029c07c188e97f", size = 10238, upload-time = "2026-05-18T20:08:57.045Z" }, +] diff --git a/aiac/demo/assets/INSTALL.md b/aiac/demo/assets/INSTALL.md index 4d2849988..4ef2e65f9 100644 --- a/aiac/demo/assets/INSTALL.md +++ b/aiac/demo/assets/INSTALL.md @@ -1,16 +1,16 @@ # Installing the demo assets (`github-tool` + `github-agent`) Single install guide for both reusable demo workloads under `demo/assets/`, deployed to a -Kagenti/Kind cluster in namespace `team1`. Consolidates what used to be split across +rossoctl/Kind cluster in namespace `team1`. Consolidates what used to be split across [`docs/specs/demo/github-tool.md`](../../docs/specs/demo/github-tool.md) §7–8 and the agent -README's former "Deploying to Kagenti" section, so the two installation paths can't drift apart +README's former "Deploying to Rossoctl" section, so the two installation paths can't drift apart again. Prefer [`install.sh`](install.sh) over doing this by hand; the steps below are what it automates. ## Prerequisites -- A running Kagenti/Kind cluster with the kagenti-operator installed. -- Namespace **`team1`** already exists — created by the Rossoctl/Kagenti installer, not by +- A running rossoctl/Kind cluster with the rossoctl-operator installed. +- Namespace **`team1`** already exists — created by the Rossoctl installer, not by anything in this repo. Nothing here creates cluster-owned resources. - `kubectl`, `kind`, and a container runtime (`docker` or `podman`) on `PATH`. @@ -28,11 +28,11 @@ apply the manifests → `kubectl rollout status`. These each fail far from their cause — read before editing the manifests or install path. -- **The tool `Service` must carry `protocol.kagenti.io/mcp: "true"`.** This is a *deploy-time* - label that the kagenti operator does **not** add. Without it, UC-1's `analyze_tool` returns +- **The tool `Service` must carry `protocol.rossoctl.io/mcp: "true"`.** This is a *deploy-time* + label that the rossoctl operator does **not** add. Without it, UC-1's `analyze_tool` returns **502** — during onboarding, long after deployment looked fine. It is already present in the committed manifest; don't drop it when editing. -- **`kagenti.io/type` is applied by the operator** from the `AgentRuntime` CR. Don't hand-set it +- **`rossoctl.io/type` is applied by the operator** from the `AgentRuntime` CR. Don't hand-set it on the pod. - **The tool's declared `PORT` must not be `9090`.** The AuthBridge sidecar reuses the declared `PORT` as its own reverse-proxy listener and shifts the app's real listen port to `PORT+1`. @@ -54,7 +54,7 @@ These each fail far from their cause — read before editing the manifests or in `/.well-known/agent-card.json` with no MCP server present, which is all UC-1 discovery needs. **Do not add `github-tool-mcp` to this install path.** - **Namespace `team1` is a precondition, not an output.** No manifest here creates it; the - Rossoctl/Kagenti installer owns it (and any labels it carries). `install.sh` fails fast with a + Rossoctl installer owns it (and any labels it carries). `install.sh` fails fast with a pointer to the installer if the namespace is missing. ## Manual steps (what `install.sh` automates) @@ -63,7 +63,7 @@ These each fail far from their cause — read before editing the manifests or in ```bash cd tools/github_tool podman build -t localhost/github-tool:latest . # or docker; the localhost/ prefix must match the manifest's image ref -kind load docker-image localhost/github-tool:latest --name kagenti +kind load docker-image localhost/github-tool:latest --name rossoctl kubectl apply -f k8s/github-tool-deployment.yaml kubectl rollout status deployment/github-tool -n team1 ``` @@ -72,7 +72,7 @@ kubectl rollout status deployment/github-tool -n team1 ```bash cd agents/github_agent podman build -t localhost/github-agent:latest . # or docker; the localhost/ prefix must match the manifest's image ref -kind load docker-image localhost/github-agent:latest --name kagenti +kind load docker-image localhost/github-agent:latest --name rossoctl kubectl apply -f k8s/configmaps.yaml kubectl apply -f k8s/github-agent-deployment.yaml kubectl rollout status deployment/github-agent -n team1 @@ -81,18 +81,19 @@ kubectl rollout status deployment/github-agent -n team1 ## Verifying ```bash -# Tool got its MCP label and the operator stamped kagenti.io/type -kubectl get svc github-tool -n team1 -o jsonpath='{.metadata.labels.protocol\.kagenti\.io/mcp}' -kubectl get pod -l app=github-tool -n team1 -o jsonpath='{.items[0].metadata.labels.kagenti\.io/type}' +# Tool got its MCP label and the operator stamped rossoctl.io/type +kubectl get svc github-tool -n team1 -o jsonpath='{.metadata.labels.protocol\.rossoctl\.io/mcp}' +kubectl get pod -l app=github-tool -n team1 -o jsonpath='{.items[0].metadata.labels.rossoctl\.io/type}' # Agent serves its card with no MCP server present -kubectl port-forward svc/github-agent 8080:8080 -n team1 & -curl -s http://localhost:8080/.well-known/agent-card.json | python3 -m json.tool +# (local port 18080, not 8080 -- rossoctl's Gateway already binds host:8080 on a Kind cluster) +kubectl port-forward svc/github-agent 18080:8080 -n team1 & +curl -s http://localhost:18080/.well-known/agent-card.json | python3 -m json.tool ``` ## Out of scope -- Deploying `github-tool-mcp`, Keycloak, SPIRE, the kagenti operator, or the cluster itself. +- Deploying `github-tool-mcp`, Keycloak, SPIRE, the rossoctl operator, or the cluster itself. - Deploying the AIAC stack (`k8s/`) — see `k8s/aiac-deployment-guide.md`. - Waiting for Keycloak client registration — that needs Keycloak credentials this install path has no business holding; it belongs to whatever use-case demo consumes the client (e.g. UC-1's diff --git a/aiac/demo/assets/agents/github_agent/README.md b/aiac/demo/assets/agents/github_agent/README.md index 74463642e..cb1a1b23f 100644 --- a/aiac/demo/assets/agents/github_agent/README.md +++ b/aiac/demo/assets/agents/github_agent/README.md @@ -1,6 +1,6 @@ # github-agent -An autonomous A2A agent that acts on a user's behalf against GitHub **source repositories** and an **issue/PR tracker**, using the [`github-tool-mcp`](https://github.com/kagenti/kagenti-extensions) MCP server. +An autonomous A2A agent that acts on a user's behalf against GitHub **source repositories** and an **issue/PR tracker**, using the [`github-tool-mcp`](https://github.com/rossoctl/cortex) MCP server. This agent implements the canonical `github-agent` used by the AIAC policy-pipeline integration test — the two skills match the policy scenario's `source_operations` and `issue_operations` roles. @@ -68,7 +68,7 @@ curl -s localhost:8000/.well-known/agent-card.json | python3 -m json.tool Optionally, run `expect -f test_startup.exp` instead to check startup automatically. -## Deploying to Kagenti (Kind cluster) +## Deploying to Rossoctl (Kind cluster) See [`../../INSTALL.md`](../../INSTALL.md) — the single install guide for this agent and the `github_tool` stub together (build, `kind load`, manifests, invariants, verification). diff --git a/aiac/demo/assets/agents/github_agent/k8s/configmaps.yaml b/aiac/demo/assets/agents/github_agent/k8s/configmaps.yaml index 362bcfb70..abfbbd6c6 100644 --- a/aiac/demo/assets/agents/github_agent/k8s/configmaps.yaml +++ b/aiac/demo/assets/agents/github_agent/k8s/configmaps.yaml @@ -1,7 +1,7 @@ # Demo-specific ConfigMap overrides for GitHub Issue Agent + AuthBridge # -# The Kagenti installer already creates correct defaults for authbridge-config, -# spiffe-helper-config, and envoy-config (with the kagenti realm). This file +# The Rossoctl installer already creates correct defaults for authbridge-config, +# spiffe-helper-config, and envoy-config (with the rossoctl realm). This file # overrides: # - authbridge-config: Keycloak connection, inbound validation, and outbound exchange settings # - authproxy-routes: per-target token exchange routes @@ -15,7 +15,7 @@ # # Note: These manifests are configured for namespace "team1" (installer-provided # and enrolled for AuthBridge sidecar injection) and the installer's -# "kagenti" Keycloak realm (set via KEYCLOAK_REALM below). +# "rossoctl" Keycloak realm (set via KEYCLOAK_REALM below). --- # authbridge-config ConfigMap - Unified config for both client-registration and envoy-proxy @@ -35,12 +35,12 @@ metadata: namespace: team1 data: KEYCLOAK_URL: "http://keycloak-service.keycloak.svc:8080" - KEYCLOAK_REALM: "kagenti" + KEYCLOAK_REALM: "rossoctl" # TOKEN_URL: Auto-derived from KEYCLOAK_URL + KEYCLOAK_REALM. Only set explicitly # when the token endpoint differs from the standard Keycloak path. - # TOKEN_URL: "http://keycloak-service.keycloak.svc:8080/realms/kagenti/protocol/openid-connect/token" + # TOKEN_URL: "http://keycloak-service.keycloak.svc:8080/realms/rossoctl/protocol/openid-connect/token" # ISSUER: Set explicitly because the internal KEYCLOAK_URL differs from the frontend URL. - ISSUER: "http://keycloak.localtest.me:8080/realms/kagenti" + ISSUER: "http://keycloak.localtest.me:8080/realms/rossoctl" # Audience validation is automatic — uses CLIENT_ID from /shared/client-id.txt # (written by client-registration or operator). No manual configuration needed. diff --git a/aiac/demo/assets/agents/github_agent/k8s/github-agent-deployment.yaml b/aiac/demo/assets/agents/github_agent/k8s/github-agent-deployment.yaml index 96596a38e..24bb34e0d 100644 --- a/aiac/demo/assets/agents/github_agent/k8s/github-agent-deployment.yaml +++ b/aiac/demo/assets/agents/github_agent/k8s/github-agent-deployment.yaml @@ -1,6 +1,6 @@ # GitHub Agent Deployment with AuthBridge # -# This deployment uses the kagenti-operator webhook to automatically inject +# This deployment uses the rossoctl-operator webhook to automatically inject # a single combined AuthBridge sidecar (post-kagenti-extensions#411). The exact # container shape depends on the resolved AuthBridge mode: # - proxy-sidecar (default): one container "authbridge-proxy" from the @@ -13,7 +13,7 @@ # exchange (HTTP), and HTTPS passthrough. # # Keycloak client registration is operator-managed (no in-pod sidecar); -# the operator creates a kagenti-keycloak-client-credentials- Secret +# the operator creates a rossoctl-keycloak-client-credentials- Secret # and the webhook mounts it at /shared/client-{id,secret}.txt. # # The agent container: @@ -22,10 +22,10 @@ # - Token exchange to the tool is handled transparently by the AuthBridge sidecar # # Labels (on Pod template): -# kagenti.io/inject: enabled - Enables AuthBridge sidecar injection -# kagenti.io/spire: enabled - Enables SPIRE-based identity (set to "disabled" if no SPIRE) +# rossoctl.io/inject: enabled - Enables AuthBridge sidecar injection +# rossoctl.io/spire: enabled - Enables SPIRE-based identity (set to "disabled" if no SPIRE) # -# kagenti.io/type is applied automatically by the operator when the +# rossoctl.io/type is applied automatically by the operator when the # AgentRuntime CR (at the end of this file) is created. # # Prerequisites (NOT created by this file — must exist before applying): @@ -57,7 +57,7 @@ metadata: namespace: team1 labels: app.kubernetes.io/name: github-agent - protocol.kagenti.io/a2a: "" + protocol.rossoctl.io/a2a: "" spec: replicas: 1 selector: @@ -67,9 +67,9 @@ spec: metadata: labels: app.kubernetes.io/name: github-agent - kagenti.io/inject: enabled - kagenti.io/spire: enabled - protocol.kagenti.io/a2a: "" + rossoctl.io/inject: enabled + rossoctl.io/spire: enabled + protocol.rossoctl.io/a2a: "" spec: serviceAccountName: github-agent containers: @@ -125,7 +125,7 @@ spec: # Agent service settings # PORT tells the agent where to listen for A2A traffic. - # The kagenti operator may override this via proxy-sidecar + # The rossoctl operator may override this via proxy-sidecar # port-stealing to relocate the agent to a free port. - name: PORT value: "8000" @@ -141,7 +141,7 @@ spec: # JWKS URI for validating incoming tokens from Keycloak - name: JWKS_URI - value: "http://keycloak-service.keycloak.svc:8080/realms/kagenti/protocol/openid-connect/certs" + value: "http://keycloak-service.keycloak.svc:8080/realms/rossoctl/protocol/openid-connect/certs" resources: limits: cpu: 500m @@ -195,11 +195,11 @@ spec: --- # AgentRuntime triggers operator-managed Keycloak client registration -# (creates kagenti-keycloak-client-credentials- Secret with +# (creates rossoctl-keycloak-client-credentials- Secret with # client-id.txt and client-secret.txt that the authbridge sidecar -# mounts at /shared/). The operator also applies kagenti.io/type=agent +# mounts at /shared/). The operator also applies rossoctl.io/type=agent # to the Deployment and Pod template, enabling webhook sidecar injection. -apiVersion: agent.kagenti.dev/v1alpha1 +apiVersion: agent.rossoctl.dev/v1alpha1 kind: AgentRuntime metadata: name: github-agent diff --git a/aiac/demo/assets/install.sh b/aiac/demo/assets/install.sh index 08afe66a1..af86a0d21 100755 --- a/aiac/demo/assets/install.sh +++ b/aiac/demo/assets/install.sh @@ -1,6 +1,6 @@ #!/usr/bin/env bash # Idempotent installer for the demo/assets workloads (github-tool, github-agent) into a -# Kagenti/Kind cluster. See INSTALL.md for the manual steps this automates and the +# rossoctl/Kind cluster. See INSTALL.md for the manual steps this automates and the # non-obvious invariants (MCP service label, async Keycloak registration, etc). # # This script does NOT wait for Keycloak client registration — that needs Keycloak @@ -11,7 +11,7 @@ set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -CLUSTER_NAME="${CLUSTER_NAME:-kagenti}" +CLUSTER_NAME="${CLUSTER_NAME:-rossoctl}" NAMESPACE="${NAMESPACE:-team1}" # Tags must carry the localhost/ prefix to match the Deployment manifests' image refs # (image: localhost/github-*:latest, imagePullPolicy: IfNotPresent). docker does not auto-prefix @@ -69,12 +69,12 @@ preflight() { if ! kubectl get namespace "$NAMESPACE" >/dev/null 2>&1; then log "ERROR: namespace '$NAMESPACE' does not exist." - log "This script does not create cluster-owned resources — run the Rossoctl/Kagenti installer first." + log "This script does not create cluster-owned resources — run the Rossoctl installer first." exit 1 fi - if ! kubectl get crd agentruntimes.agent.kagenti.dev >/dev/null 2>&1; then - log "ERROR: AgentRuntime CRD not found. Is the kagenti-operator installed?" + if ! kubectl get crd agentruntimes.agent.rossoctl.dev >/dev/null 2>&1; then + log "ERROR: AgentRuntime CRD not found. Is the rossoctl-operator installed?" exit 1 fi } @@ -90,9 +90,9 @@ load_image_to_kind() { if [ "$RUNTIME" = "podman" ]; then local tar_file tar_file="$(mktemp "${TMPDIR:-/tmp}/install-image.XXXXXX")" - trap 'rm -f "$tar_file"' RETURN "$RUNTIME" save "$image" -o "$tar_file" kind load image-archive "$tar_file" --name "$CLUSTER_NAME" + rm -f "$tar_file" else kind load docker-image "$image" --name "$CLUSTER_NAME" fi diff --git a/aiac/demo/assets/tools/github_tool/Dockerfile b/aiac/demo/assets/tools/github_tool/Dockerfile index c9c1dbb09..b6539abcd 100644 --- a/aiac/demo/assets/tools/github_tool/Dockerfile +++ b/aiac/demo/assets/tools/github_tool/Dockerfile @@ -2,10 +2,13 @@ FROM python:3.12-slim@sha256:d764629ce0ddd8c71fd371e9901efb324a95789d2315a47db7e WORKDIR /app -COPY requirements.txt . +# Create the non-root user up front so copied files can be owned by it. +RUN useradd --no-create-home --uid 10001 aiac + +COPY --chown=10001:10001 requirements.txt . RUN pip install --no-cache-dir -r requirements.txt -COPY server.py . +COPY --chown=10001:10001 server.py . ENV PORT=9090 ENV LOG_LEVEL=INFO @@ -13,7 +16,6 @@ ENV LOG_LEVEL=INFO EXPOSE 9090 # Drop privileges. -RUN useradd --no-create-home --uid 10001 aiac USER 10001 CMD ["python", "server.py"] diff --git a/aiac/demo/assets/tools/github_tool/k8s/github-tool-deployment.yaml b/aiac/demo/assets/tools/github_tool/k8s/github-tool-deployment.yaml index 7413b0ed8..663094805 100644 --- a/aiac/demo/assets/tools/github_tool/k8s/github-tool-deployment.yaml +++ b/aiac/demo/assets/tools/github_tool/k8s/github-tool-deployment.yaml @@ -10,7 +10,7 @@ # workload name == Service name == "github-tool" # analyze_tool endpoint: http://github-tool.team1.svc.cluster.local:9090/mcp # -# kagenti.io/type=tool is applied by the kagenti-operator when it reconciles +# rossoctl.io/type=tool is applied by the rossoctl-operator when it reconciles # the AgentRuntime below; do not hand-set it here. # # In-cluster PORT override (9095, not the image's default 9090): the @@ -44,6 +44,7 @@ spec: metadata: labels: app: github-tool + app.kubernetes.io/name: github-tool spec: serviceAccountName: github-tool # The image runs as non-root UID 10001 (see the github_tool Dockerfile). @@ -99,7 +100,7 @@ spec: emptyDir: {} --- # Service — exposes /mcp as the first (and only) port. -# The protocol.kagenti.io/mcp label is a deploy-time prerequisite: +# The protocol.rossoctl.io/mcp label is a deploy-time prerequisite: # analyze_tool returns 502 if this label is absent. # The operator does NOT stamp it; it must be set explicitly here. apiVersion: v1 @@ -108,7 +109,7 @@ metadata: name: github-tool namespace: team1 labels: - protocol.kagenti.io/mcp: "true" + protocol.rossoctl.io/mcp: "true" spec: selector: app: github-tool @@ -118,11 +119,11 @@ spec: targetPort: 9095 type: ClusterIP --- -# AgentRuntime — enrolls the workload so the kagenti-operator: -# - applies kagenti.io/type=tool to the pod (read by classify_service), and +# AgentRuntime — enrolls the workload so the rossoctl-operator: +# - applies rossoctl.io/type=tool to the pod (read by classify_service), and # - registers a Keycloak client with client.name = "team1/github-tool" # (so analyze_tool derives scopes github-tool.{source-read,...}). -apiVersion: agent.kagenti.dev/v1alpha1 +apiVersion: agent.rossoctl.dev/v1alpha1 kind: AgentRuntime metadata: name: github-tool diff --git a/aiac/demo/tools/github_tool/Dockerfile b/aiac/demo/tools/github_tool/Dockerfile new file mode 100644 index 000000000..bb276b978 --- /dev/null +++ b/aiac/demo/tools/github_tool/Dockerfile @@ -0,0 +1,15 @@ +FROM python:3.12-slim + +WORKDIR /app + +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +COPY server.py . + +ENV PORT=9090 +ENV LOG_LEVEL=INFO + +EXPOSE 9090 + +CMD ["python", "server.py"] diff --git a/aiac/demo/tools/github_tool/k8s/github-tool-deployment.yaml b/aiac/demo/tools/github_tool/k8s/github-tool-deployment.yaml new file mode 100644 index 000000000..69093a5fa --- /dev/null +++ b/aiac/demo/tools/github_tool/k8s/github-tool-deployment.yaml @@ -0,0 +1,85 @@ +--- +# github-tool — simplified 4-scope MCP stub for UC-1 onboarding. +# +# DISTINCT from the production 44-tool github-tool-mcp used by the github-agent +# for live GitHub operations. These two coexist under different Service names: +# github-tool (this file, port 9090) — UC-1 discovery, client.name = "team1/github-tool" +# github-tool-mcp (authbridge/demos/github-issue/k8s/) — agent execution +# +# Naming invariants (do not rename): +# workload name == Service name == "github-tool" +# analyze_tool endpoint: http://github-tool.team1.svc.cluster.local:9090/mcp +# +# kagenti.io/type=tool is applied by the kagenti-operator when it reconciles +# the AgentRuntime below; do not hand-set it here. +--- +apiVersion: v1 +kind: ServiceAccount +metadata: + name: github-tool + namespace: team1 + +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: github-tool + namespace: team1 +spec: + replicas: 1 + selector: + matchLabels: + app: github-tool + template: + metadata: + labels: + app: github-tool + spec: + serviceAccountName: github-tool + containers: + - name: github-tool + image: localhost/github-tool:latest + imagePullPolicy: IfNotPresent + ports: + - containerPort: 9090 + env: + - name: PORT + value: "9090" + - name: LOG_LEVEL + value: "INFO" +--- +# Service — exposes /mcp as the first (and only) port. +# The protocol.kagenti.io/mcp label is a deploy-time prerequisite: +# analyze_tool returns 502 if this label is absent. +# The operator does NOT stamp it; it must be set explicitly here. +apiVersion: v1 +kind: Service +metadata: + name: github-tool + namespace: team1 + labels: + protocol.kagenti.io/mcp: "true" +spec: + selector: + app: github-tool + ports: + - name: mcp + port: 9090 + targetPort: 9090 + type: ClusterIP +--- +# AgentRuntime — enrolls the workload so the kagenti-operator: +# - applies kagenti.io/type=tool to the pod (read by classify_service), and +# - registers a Keycloak client with client.name = "team1/github-tool" +# (so analyze_tool derives scopes github-tool.{source-read,...}). +apiVersion: agent.kagenti.dev/v1alpha1 +kind: AgentRuntime +metadata: + name: github-tool + namespace: team1 +spec: + type: tool + targetRef: + apiVersion: apps/v1 + kind: Deployment + name: github-tool diff --git a/aiac/demo/tools/github_tool/pytest.ini b/aiac/demo/tools/github_tool/pytest.ini new file mode 100644 index 000000000..eea2c1802 --- /dev/null +++ b/aiac/demo/tools/github_tool/pytest.ini @@ -0,0 +1 @@ +[pytest] diff --git a/aiac/demo/tools/github_tool/requirements.txt b/aiac/demo/tools/github_tool/requirements.txt new file mode 100644 index 000000000..e814f2ecf --- /dev/null +++ b/aiac/demo/tools/github_tool/requirements.txt @@ -0,0 +1,5 @@ +mcp[cli]>=1.9.0,<2 +uvicorn[standard]>=0.34.0 +httpx>=0.28.0 +pytest>=8.0.0 +pytest-asyncio>=0.25.0 diff --git a/aiac/demo/tools/github_tool/server.py b/aiac/demo/tools/github_tool/server.py new file mode 100644 index 000000000..c6f2da79e --- /dev/null +++ b/aiac/demo/tools/github_tool/server.py @@ -0,0 +1,36 @@ +import os + +from mcp.server.fastmcp import FastMCP + +# Single editable registry — one entry per canonical UC-1 scope. +# Names match scenario.py::TOOL_SCOPES keys; descriptions are verbatim copies. +TOOLS = [ + ("source-read", "Read source repository contents: file listings and file bodies. Read-only."), + ("source-write", "Create, modify, or delete source repository contents; commit file changes."), + ("issues-read", "Read issues and their comment threads. Read-only."), + ("issues-write", "Create and update issues: open, edit, comment, and close."), +] + +mcp = FastMCP("github-tool", host="0.0.0.0", json_response=True, stateless_http=True) + +for _tool_name, _tool_desc in TOOLS: + def _make_stub(name: str): + def stub() -> str: + return f"stub: {name} not implemented in phase-1 demo" + + stub.__name__ = name.replace("-", "_") + return stub + + mcp.add_tool(fn=_make_stub(_tool_name), name=_tool_name, description=_tool_desc) + +app = mcp.streamable_http_app() + +if __name__ == "__main__": + import uvicorn + + uvicorn.run( + app, + host="0.0.0.0", + port=int(os.getenv("PORT", "9090")), + log_level=os.getenv("LOG_LEVEL", "info").lower(), + ) diff --git a/aiac/src/aiac/policy/store/__init__.py b/aiac/demo/tools/github_tool/test/__init__.py similarity index 100% rename from aiac/src/aiac/policy/store/__init__.py rename to aiac/demo/tools/github_tool/test/__init__.py diff --git a/aiac/demo/tools/github_tool/test/conftest.py b/aiac/demo/tools/github_tool/test/conftest.py new file mode 100644 index 000000000..3f93ad297 --- /dev/null +++ b/aiac/demo/tools/github_tool/test/conftest.py @@ -0,0 +1,8 @@ +import sys +from pathlib import Path + +# server.py importable as `server` +sys.path.insert(0, str(Path(__file__).parents[1])) + +# scenario.py importable as `scenario` +sys.path.insert(0, str(Path(__file__).parents[4] / "test" / "integration")) diff --git a/aiac/demo/tools/github_tool/test/test_server.py b/aiac/demo/tools/github_tool/test/test_server.py new file mode 100644 index 000000000..8f36aa9e9 --- /dev/null +++ b/aiac/demo/tools/github_tool/test/test_server.py @@ -0,0 +1,59 @@ +import pytest +from starlette.testclient import TestClient + +from scenario import TOOL_SCOPES + + +@pytest.fixture(scope="module") +def client(): + from server import app + + with TestClient(app, raise_server_exceptions=True) as c: + yield c + + +def _tools_list(client: TestClient) -> list[dict]: + response = client.post( + "/mcp", + json={"jsonrpc": "2.0", "id": 1, "method": "tools/list", "params": {}}, + headers={"Accept": "application/json"}, + ) + assert response.status_code == 200 + return response.json()["result"]["tools"] + + +class TestToolsList: + def test_returns_exactly_four_tool_names(self, client): + tools = _tools_list(client) + assert {t["name"] for t in tools} == set(TOOL_SCOPES.keys()) + + def test_descriptions_match_scenario(self, client): + tools = _tools_list(client) + by_name = {t["name"]: t["description"] for t in tools} + for name, expected in TOOL_SCOPES.items(): + assert by_name[name] == expected, f"description mismatch for {name!r}" + + def test_input_schema_is_minimal(self, client): + tools = _tools_list(client) + for tool in tools: + schema = tool["inputSchema"] + assert schema["type"] == "object" + assert schema["properties"] == {} + + +class TestToolsCall: + def test_valid_tool_returns_stub_content(self, client): + response = client.post( + "/mcp", + json={ + "jsonrpc": "2.0", + "id": 2, + "method": "tools/call", + "params": {"name": "source-read", "arguments": {}}, + }, + headers={"Accept": "application/json"}, + ) + assert response.status_code == 200 + body = response.json() + assert "result" in body + assert body["result"]["content"] diff --git a/aiac/demo/use-cases/uc1-onboarding/Makefile b/aiac/demo/use-cases/uc1-onboarding/Makefile index a8ccdbc2f..8aa00f106 100644 --- a/aiac/demo/use-cases/uc1-onboarding/Makefile +++ b/aiac/demo/use-cases/uc1-onboarding/Makefile @@ -2,14 +2,26 @@ # policy, and generates enforceable least-privilege authorization. Strictly live: every target # talks to a real cluster/Keycloak/LLM, no offline replay and no fixtures. # -# make demo # prereqs -> clear -> setup -> onboard-agent -> show -> onboard-tool -> show +# make demo # init (00-03) -> onboard (04-05) -> run (dev/test/devops) # make dev # drive dev-user's intents through the after-tool snapshot # -# Prereqs: a Kagenti/kind cluster with SPIRE + Keycloak + the kagenti operator (see -# ../../assets/INSTALL.md and ../../../k8s/aiac-deployment-guide.md), and KEYCLOAK_URL / -# KEYCLOAK_ADMIN_USERNAME / KEYCLOAK_ADMIN_PASSWORD in the environment. +# How the demo sources generated Rego (see demo.md "How the demo sources the generated Rego"): +# the reworked PDP Policy Writer is CR-backed — production writes AuthorizationPolicy CRs only, no +# /rego file dump. So `make agent`/`tool` read the agent's Rego straight from its +# `authorizationpolicies.agent.rossoctl.dev/github-agent` CR (spec.policies[].content) into +# generated//team1/github-agent/{inbound,outbound}/request.rego, and `make clear` deletes +# that CR. No demo-only deployment overlay is needed (we do NOT enable POLICY_WRITER_DUMP_REGO). +# +# Prereqs: a rossoctl/kind cluster with SPIRE + Keycloak + the rossoctl operator (see +# ../../assets/INSTALL.md and ../../../k8s/aiac-deployment-guide.md). KEYCLOAK_URL / +# KEYCLOAK_ADMIN_USERNAME / KEYCLOAK_ADMIN_PASSWORD are discovered automatically by the +# `keycloak` target (init/00-discover-keycloak.sh); no need to export them by hand. To target a +# Keycloak this can't reach that way, export the three vars yourself and they win. + +# The discover script is bash (arrays, [[, BASH_SOURCE) — make's default /bin/sh can't source it. +SHELL := /bin/bash -.PHONY: help prereqs clear setup onboard-agent onboard-tool show diff dev test devops demo +.PHONY: help keycloak prereqs clear setup agent tool show diff dev test devops init onboard run demo .DEFAULT_GOAL := help @@ -19,44 +31,62 @@ AIAC_ROOT := ../../.. VENV := $(AIAC_ROOT)/.venv PYTHON ?= $(VENV)/bin/python +# Every Keycloak-touching recipe sources the discovery script first. A make recipe can't export +# env into the caller's shell (nor into the next recipe), so each recipe that needs the Keycloak +# vars self-sources. The script is idempotent — it reuses an existing port-forward — so repeated +# sourcing across a `make demo` chain sets the forward up once and reuses it thereafter. +KC_ENV := . init/00-discover-keycloak.sh && + help: ## Show this menu @printf "\nuc1-onboarding demo — live AIAC onboarding of a real agent + tool.\n\n" - @printf "Run:\n \033[1mmake demo\033[0m (prereqs -> clear -> setup -> onboard-agent -> show -> onboard-tool -> show)\n" + @printf "Run:\n \033[1mmake demo\033[0m (init 00-03 -> onboard 04-05 -> run)\n" + @printf " \033[1mmake init / make onboard / make run\033[0m (one phase at a time)\n" @printf " \033[1mmake dev / make test / make devops\033[0m (drive one user's intents)\n\n" @printf "Targets:\n" @awk 'BEGIN{FS=":.*## "} /^[a-zA-Z_-]+:.*## /{printf " \033[36m%-16s\033[0m %s\n",$$1,$$2}' $(MAKEFILE_LIST) @printf "\nVars: PYTHON(=%s)\n\n" "$(PYTHON)" -prereqs: ## Verify (and install if absent) everything the demo needs before touching Keycloak - $(PYTHON) init/00-prereqs.py +keycloak: ## Port-forward the in-cluster Keycloak and discover admin creds (runs before prereqs) + $(KC_ENV) true + +prereqs: keycloak ## Verify (and install if absent) everything the demo needs before touching Keycloak + $(KC_ENV) $(PYTHON) init/01-prereqs.py -clear: ## Reset to a clean slate: Keycloak roles/scopes, Policy Store, writer's /rego, generated/ - $(PYTHON) init/01-clear.py +clear: ## Reset to a clean slate: Keycloak roles/scopes, Policy Store, the agent's AuthorizationPolicy CR, generated/ + $(KC_ENV) $(PYTHON) init/02-clear.py setup: ## Provision users/roles, mount policy.md, configure token exchange; prints both UUIDs - $(PYTHON) init/02-setup.py + $(KC_ENV) $(PYTHON) init/03-setup.py -onboard-agent: ## POST /apply/service on github-agent; captures generated/01-after-agent/ - $(PYTHON) onboard/03-onboard-agent.py +agent: ## POST /apply/service on github-agent; captures generated/01-after-agent/ + $(KC_ENV) $(PYTHON) onboard/04-onboard-agent.py -onboard-tool: ## POST /apply/service on github-tool; captures generated/02-after-tool/ - $(PYTHON) onboard/04-onboard-tool.py +tool: ## POST /apply/service on github-tool; captures generated/02-after-tool/ + $(KC_ENV) $(PYTHON) onboard/05-onboard-tool.py show: ## Show live Keycloak roles/scopes + the latest generated .rego (or "no policy yet") - $(PYTHON) show-state.py + $(KC_ENV) $(PYTHON) show-state.py diff: ## Show the latest snapshot, diffed against generated/ (e.g. make diff PRIOR=01-after-agent) - $(PYTHON) show-state.py --diff $(PRIOR) + $(KC_ENV) $(PYTHON) show-state.py --diff $(PRIOR) dev: ## Drive dev-user's intents against generated/02-after-tool/ - $(PYTHON) run/run-developer.py + $(KC_ENV) $(PYTHON) run/run-developer.py test: ## Drive test-user's intents against generated/02-after-tool/ - $(PYTHON) run/run-tester.py + $(KC_ENV) $(PYTHON) run/run-tester.py devops: ## Drive devops-user — denied at the inbound gate (the intended story) - $(PYTHON) run/run-devops.py + $(KC_ENV) $(PYTHON) run/run-devops.py + +# --- grouped phases (each aggregates the numbered steps above) -------------------------------- + +init: keycloak prereqs clear setup ## Phase 1 (steps 00-03): discover Keycloak, verify prereqs, reset, provision + +onboard: agent tool ## Phase 2 (steps 04-05): onboard the agent, then the tool + +run: dev test devops ## Phase 3: drive all three users (developer, tester, devops) -demo: prereqs clear setup onboard-agent show onboard-tool show ## Full end-to-end: provision through both onboardings +demo: init onboard run ## Full end-to-end: init -> onboard -> run @echo - @echo "Onboarding complete. Now: make dev / make test / make devops" + @echo "Demo complete. Re-drive a single user with: make dev / make test / make devops" diff --git a/aiac/demo/use-cases/uc1-onboarding/demo.md b/aiac/demo/use-cases/uc1-onboarding/demo.md index da239281e..fa37752e3 100644 --- a/aiac/demo/use-cases/uc1-onboarding/demo.md +++ b/aiac/demo/use-cases/uc1-onboarding/demo.md @@ -18,56 +18,75 @@ Grant access on a least-privilege basis: allow only what this policy states; den ## What comes out the other side -AIAC turns that into two Rego files per agent — one gating who may call it, one gating what it may -do downstream — derived from the policy text plus the realm-role descriptions already in Keycloak -and the tool's own discovered capabilities. An excerpt of the generated outbound gate: +AIAC turns that into two Rego policies per agent — one gating who may call it, one gating what it +may do downstream — derived from the policy text plus the realm-role descriptions already in +Keycloak and the tool's own discovered capabilities. Both use the fixed AuthBridge packages +(`authbridge.client.{inbound,outbound}.request`) the live OPA plugin evaluates, keyed on the +plugin's real input shape (`input.identity.subject`, `input.identity.service_id`, +`input.mcp.params.name`). An excerpt of the generated outbound gate: ```rego -package authz.team1_github_agent.outbound +package authbridge.client.outbound.request +import rego.v1 subject_role_scopes := { - "developer": ["github-tool.issues-read", "github-tool.source-write", "github-tool.source-read"], - "tester": ["github-tool.issues-write", "github-tool.issues-read"], + "developer": ["issues-read", "source-write", "source-read"], + "tester": ["issues-read", "issues-write"], +} +target_scopes := { + "spiffe://localtest.me/ns/team1/sa/github-tool": ["source-read", "source-write", "issues-read", "issues-write"], } subject_ok if { - some role in subject_roles[input.subject] - input.function_name in subject_role_scopes[role] + some role in subject_roles[input.identity.subject] + input.mcp.params.name in subject_role_scopes[role] } target_ok if { - input.function_name in target_scopes[input.target] + input.mcp.params.name in target_scopes[input.identity.service_id] } default allow := false allow if { subject_ok; target_ok } ``` -Every access decision is a two-gate AND: the calling user's role must be granted the scope -(`subject_ok`), *and* the agent's own discovered capabilities must reach it (`target_ok`). A -developer can read and write source and read issues; a tester can read and write issues but never -touches source — exactly the two-line policy, and nothing it didn't say. +Every access decision is a two-gate AND on the same invoked tool (`input.mcp.params.name`, the +**bare** MCP tool name such as `source-read`): the calling user's role must be granted the scope +(`subject_ok`), *and* the target service the exchanged token was minted for must expose it +(`target_ok`, keyed by the full `input.identity.service_id` SPIFFE id). A developer can read and +write source and read issues; a tester can read and write issues but never touches source — exactly +the two-line policy, and nothing it didn't say. ## Running it Everything below is a real cluster, a real Keycloak, a real LLM call, and a real RFC 8693 token -exchange — there is no offline mode. Bring up a Kagenti cluster with SPIRE + Keycloak + the -kagenti operator first (see [../../assets/INSTALL.md](../../assets/INSTALL.md) and +exchange — there is no offline mode. Bring up a rossoctl cluster with SPIRE + Keycloak + the +rossoctl operator first (see [../../assets/INSTALL.md](../../assets/INSTALL.md) and [../../../k8s/aiac-deployment-guide.md](../../../k8s/aiac-deployment-guide.md) for reference, not as a -manual checklist — `make prereqs` below verifies and, where safe, installs what's missing) and -export `KEYCLOAK_URL` / `KEYCLOAK_ADMIN_USERNAME` / `KEYCLOAK_ADMIN_PASSWORD`. +manual checklist — `make prereqs` below verifies and, where safe, installs what's missing). + +You do **not** need to export the Keycloak variables by hand. `make keycloak` +(`init/00-discover-keycloak.sh`) port-forwards the in-cluster Keycloak to a local port and reads the +admin credentials from the `keycloak-admin-secret`, exporting `KEYCLOAK_URL` / +`KEYCLOAK_ADMIN_USERNAME` / `KEYCLOAK_ADMIN_PASSWORD` for the step that runs it. Every +Keycloak-touching target self-runs it first, so you rarely call it directly — the forward is set up +once and reused across the run. If you already export those three variables (e.g. to point at a +Keycloak this can't reach), your values win. ```bash +make keycloak # (optional) port-forward Keycloak + discover admin creds; the targets below self-run it make prereqs # verify/install cluster + AIAC stack + demo workloads; wait for Keycloak registration make clear # reset to a clean slate make setup # provision demo users/roles, mount policy.md, configure token exchange ``` +To tear down the port-forward afterwards: `pkill -f 'port-forward .*keycloak-service'`. + **Pause 1 — baseline.** `make show` reports three users with roles, no `github-*` roles or scopes yet, and no generated `.rego` at all. Nothing has been onboarded; there is nothing to enforce yet. ```bash -make onboard-agent # AIAC discovers github-agent, reads policy.md, generates the inbound gate +make agent # AIAC discovers github-agent, reads policy.md, generates the inbound gate make show ``` @@ -76,7 +95,7 @@ the agent's discovered scopes. The outbound gate exists but every map in it is s there's no tool yet for the agent to act on. ```bash -make onboard-tool # AIAC discovers github-tool's capabilities and completes the agent's outbound gate +make tool # AIAC discovers github-tool's capabilities and completes the agent's outbound gate make show ``` @@ -98,8 +117,31 @@ Each target does a real `grant_type=password` login, checks the inbound gate, pe result table. `devops-user`'s inbound denial is the intended story, not a failure: nothing in the policy grants devops-user access to the agent at all. -Run `make demo` for the whole provisioning ladder (`prereqs` through the second `show`) in one -shot, then drive the three users separately. +### One-shot: `make demo` + +The individual targets above are grouped into three phase aggregates, so you can run a whole phase +at once or the entire demo end to end: + +| Phase target | Steps | What it does | +|--------------|-------|--------------| +| `make init` | `00`–`03` | discover Keycloak → verify prereqs → clear → setup | +| `make onboard` | `04`–`05` | onboard the agent, then the tool | +| `make run` | — | drive all three users (developer, tester, devops) | + +`make demo` chains all three (`init → onboard → run`) with no narrated pauses — use it when you just +want the full run: + +```bash +make demo # init (00-03) -> onboard (04-05) -> run (dev/test/devops) +``` + +Or drive one phase — or one user — at a time: + +```bash +make init # or step-by-step: make prereqs / clear / setup +make onboard # or: make agent / make tool +make run # or a single user: make dev / make test / make devops +``` ## Architecture @@ -119,23 +161,51 @@ shot, then drive the three users separately. └──────────────────────────────────┴──► github-tool ``` -The gates are plain Rego, evaluated with `opa eval` against the files AIAC's Policy Computation -Engine writes — this demo runs them the same way a live enforcement point would query them, but -does not itself sit in the request path (see the appendix). +The gates are plain Rego, evaluated with `opa eval` against the policy content AIAC's writer +produces — this demo runs them the same way a live enforcement point would query them, but does +not itself sit in the request path (see the appendix). + +### How the demo sources the generated Rego + +The reworked PDP Policy Writer is **CR-backed**: for each onboarded agent it server-side-applies a +single `AuthorizationPolicy` custom resource (`agent.rossoctl.dev/v1alpha1`, named +`` in namespace `` — here `github-agent` in `team1`) whose `spec.policies[]` carry the +inbound and outbound Rego as `content`. In production it writes **CRs only** — no `.rego` files on +disk (`k8s/pdp-interface-deployment.yaml` keeps `POLICY_WRITER_DUMP_REGO` off and mounts no +`/rego`). + +So this demo reads its Rego **straight from the CR** — the same artifact a live enforcement point +consumes — rather than from a debug file dump: + +```bash +kubectl get authorizationpolicies.agent.rossoctl.dev github-agent -n team1 -o json +``` + +`onboard/04`/`05` fetch that CR and write each `spec.policies[].content` into +`generated//team1/github-agent/{inbound,outbound}/request.rego` (mirroring the CR's +`policies[].path`), then `opa eval` those files. `make clear` deletes the CR (a re-onboard +server-side-applies a fresh one). This keeps the demo honest against the real artifact and needs no +demo-only deployment overlay to re-enable the optional `.rego` dump. + +> The committed snapshots under `generated/` are produced from the **real** reworked generator +> (`src/aiac/pdp/service/policy/opa/rego.py`) against a hand-built model of this scenario, so they +> match the CR content the live writer emits (byte-for-byte with `docs/examples/opa-team1-policy.yaml` +> for the after-tool state, modulo the cluster's actual trust domain). A live `make onboard` against +> a cluster running the reworked writer overwrites them and is the authoritative source of truth. ## Troubleshooting - **`make prereqs` hangs waiting on client registration** — Keycloak client registration is async after the operator injects a workload; give it a couple of minutes, then check the operator's webhook logs. -- **`make onboard-agent`/`make onboard-tool` times out** — onboarding drives the Policy Rules +- **`make agent`/`make tool` times out** — onboarding drives the Policy Rules Builder's LLM calls and can genuinely take minutes; re-run with a larger `AIAC_ONBOARD_TIMEOUT` if your LLM endpoint is slow. - **`make setup` / `make dev` fails with a Keycloak profile error** — Keycloak 26's declarative user - profile requires `email`/`firstName`/`lastName` before `grant_type=password` succeeds; `02-setup.py` + profile requires `email`/`firstName`/`lastName` before `grant_type=password` succeeds; `03-setup.py` sets these, so this points at a realm that was provisioned some other way. - **A `run-*` target aborts with "no policy found"** — the drivers always run against - `generated/02-after-tool/`; run `make onboard-agent && make onboard-tool` first. + `generated/02-after-tool/`; run `make agent && make tool` first. ## Appendix: known gaps diff --git a/aiac/demo/use-cases/uc1-onboarding/init/00-discover-keycloak.sh b/aiac/demo/use-cases/uc1-onboarding/init/00-discover-keycloak.sh new file mode 100644 index 000000000..b34db776f --- /dev/null +++ b/aiac/demo/use-cases/uc1-onboarding/init/00-discover-keycloak.sh @@ -0,0 +1,87 @@ +# shellcheck shell=bash +# 00-discover-keycloak.sh — set up the environment the uc1-onboarding demo needs. +# +# source init/00-discover-keycloak.sh # from demo/use-cases/uc1-onboarding/ +# source ./00-discover-keycloak.sh # from within init/ +# +# Exports KEYCLOAK_URL / KEYCLOAK_ADMIN_USERNAME / KEYCLOAK_ADMIN_PASSWORD and +# port-forwards the in-cluster keycloak-service to a local port so the demo's +# `make prereqs` / `make clear` (and every other target) can reach Keycloak. +# +# MUST be sourced, not executed — a child process cannot export vars back into +# your shell. It leaves a background `kubectl port-forward` running; its PID is +# stashed in $AIAC_KC_PF_PID. Tear it down with: kill "$AIAC_KC_PF_PID" +# +# Overridable knobs (all have sane defaults): +# KC_NAMESPACE (keycloak) namespace holding keycloak-service +# KC_SERVICE (svc/keycloak-service) port-forward target +# KC_REMOTE_PORT (8080) service port +# KC_LOCAL_PORT (18080) local port (8080 is usually taken +# by the kind/rootless node) +# KC_SECRET_NS (aiac-system) namespace holding the admin secret +# KC_SECRET (keycloak-admin-secret) secret with the admin creds + +# --- guard: refuse to run as a subprocess ------------------------------------ +if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then + echo "ERROR: source this script — don't execute it: source init/00-discover-keycloak.sh" >&2 + exit 1 +fi + +# Wrapped in a function so we can `return` on error without killing the shell. +_aiac_load_env() { + local ns="${KC_NAMESPACE:-keycloak}" + local svc="${KC_SERVICE:-svc/keycloak-service}" + local rport="${KC_REMOTE_PORT:-8080}" + local lport="${KC_LOCAL_PORT:-18080}" + local secret_ns="${KC_SECRET_NS:-aiac-system}" + local secret="${KC_SECRET:-keycloak-admin-secret}" + local url="http://localhost:${lport}" + local wellknown="${url}/realms/master/.well-known/openid-configuration" + + command -v kubectl >/dev/null 2>&1 || { echo "ABORT: kubectl not on PATH" >&2; return 1; } + kubectl cluster-info >/dev/null 2>&1 || { echo "ABORT: kubectl cannot reach a cluster" >&2; return 1; } + + # Reuse an existing reachable forward if one is already up on this port. + if curl -sf -o /dev/null --max-time 3 "$wellknown"; then + echo " Keycloak already reachable at ${url} — reusing it" + else + echo " Starting port-forward ${ns}/${svc} ${lport}:${rport} ..." + kubectl port-forward -n "$ns" "$svc" "${lport}:${rport}" >/dev/null 2>&1 & + export AIAC_KC_PF_PID=$! + # Wait for readiness (up to ~30s). + local i + for i in $(seq 1 30); do + curl -sf -o /dev/null --max-time 2 "$wellknown" && break + # If the port-forward died, stop waiting. + kill -0 "$AIAC_KC_PF_PID" 2>/dev/null || { echo "ABORT: port-forward exited early" >&2; return 1; } + sleep 1 + done + if ! curl -sf -o /dev/null --max-time 2 "$wellknown"; then + echo "ABORT: Keycloak not reachable at ${url} after 30s" >&2 + kill "$AIAC_KC_PF_PID" 2>/dev/null + unset AIAC_KC_PF_PID + return 1 + fi + echo " Port-forward ready (pid ${AIAC_KC_PF_PID})" + fi + + # Pull admin creds straight from the secret (never echoed). + local user pass + user="$(kubectl get secret "$secret" -n "$secret_ns" -o jsonpath='{.data.KEYCLOAK_ADMIN_USERNAME}' 2>/dev/null | base64 -d)" + pass="$(kubectl get secret "$secret" -n "$secret_ns" -o jsonpath='{.data.KEYCLOAK_ADMIN_PASSWORD}' 2>/dev/null | base64 -d)" + if [[ -z "$user" || -z "$pass" ]]; then + echo "ABORT: could not read KEYCLOAK_ADMIN_USERNAME/PASSWORD from secret ${secret_ns}/${secret}" >&2 + return 1 + fi + + export KEYCLOAK_URL="$url" + export KEYCLOAK_ADMIN_USERNAME="$user" + export KEYCLOAK_ADMIN_PASSWORD="$pass" + + echo " KEYCLOAK_URL=${KEYCLOAK_URL}" + echo " KEYCLOAK_ADMIN_USERNAME=${KEYCLOAK_ADMIN_USERNAME}" + echo " KEYCLOAK_ADMIN_PASSWORD=******** (set)" + echo "Environment ready. Run: make prereqs / make clear" +} + +_aiac_load_env diff --git a/aiac/demo/use-cases/uc1-onboarding/init/00-prereqs.py b/aiac/demo/use-cases/uc1-onboarding/init/01-prereqs.py similarity index 88% rename from aiac/demo/use-cases/uc1-onboarding/init/00-prereqs.py rename to aiac/demo/use-cases/uc1-onboarding/init/01-prereqs.py index 5ad5227eb..3146fdb30 100644 --- a/aiac/demo/use-cases/uc1-onboarding/init/00-prereqs.py +++ b/aiac/demo/use-cases/uc1-onboarding/init/01-prereqs.py @@ -9,7 +9,7 @@ Then the real readiness condition: poll Keycloak until both the ``team1/github-agent`` and ``team1/github-tool`` clients exist (registration is async — "rollout complete" is not "ready to -onboard"), and assert the tool Service carries the ``protocol.kagenti.io/mcp`` LABEL (the handoff's +onboard"), and assert the tool Service carries the ``protocol.rossoctl.io/mcp`` LABEL (the handoff's own spec text calls it an annotation; it is not). """ @@ -34,10 +34,10 @@ AIAC_IMAGES = [ ("localhost/aiac-pdp-config:local", "src/aiac/idp/service/configuration/keycloak/Dockerfile", "src/aiac/idp/service/configuration/keycloak"), ("localhost/aiac-pdp-policy-opa:local", "src/aiac/pdp/service/policy/opa/Dockerfile", "src"), - ("localhost/aiac-policy-store:local", "src/aiac/policy/store/service/Dockerfile", "src"), + ("localhost/aiac-policy-model-store:local", "src/aiac/policy/model_store/service/Dockerfile", "src"), ("localhost/aiac-agent:local", "src/aiac/agent/controller/Dockerfile", "src"), ] -AIAC_MANIFESTS = ["pdp-interface-deployment.yaml", "policy-store-statefulset.yaml", "agent-deployment.yaml"] +AIAC_MANIFESTS = ["pdp-interface-deployment.yaml", "policy-model-store-statefulset.yaml", "agent-deployment.yaml"] # --- class 1: verify only --------------------------------------------------------------------- @@ -47,16 +47,16 @@ def verify_cluster_reachable() -> None: try: kubectl("cluster-info", timeout=15) except (subprocess.CalledProcessError, subprocess.TimeoutExpired): - abort("kubectl cannot reach a cluster. Set up a Kagenti/Kind cluster first (see the Rossoctl/Kagenti installer).") + abort("kubectl cannot reach a cluster. Set up a rossoctl/Kind cluster first (see the Rossoctl installer).") ok("cluster reachable") def verify_crds() -> None: - for crd in ("agentruntimes.agent.kagenti.dev", "agentcards.agent.kagenti.dev"): + for crd in ("agentruntimes.agent.rossoctl.dev", "agentcards.agent.rossoctl.dev"): try: kubectl("get", "crd", crd, timeout=15) except subprocess.CalledProcessError: - abort(f"CRD {crd!r} not found — is the kagenti-operator installed?") + abort(f"CRD {crd!r} not found — is the rossoctl-operator installed?") ok("agentruntimes/agentcards CRDs present") @@ -64,7 +64,7 @@ def verify_namespace(namespace: str) -> None: try: kubectl("get", "namespace", namespace, timeout=15) except subprocess.CalledProcessError: - abort(f"namespace {namespace!r} does not exist — run the Rossoctl/Kagenti installer first.") + abort(f"namespace {namespace!r} does not exist — run the Rossoctl installer first.") ok(f"namespace {namespace!r} exists") @@ -123,7 +123,7 @@ def ensure_aiac_deployed(cfg: Config) -> None: note("AIAC stack not found — building images, loading into kind, and applying manifests") runtime = _container_runtime() - cluster_name = "kagenti" + cluster_name = "rossoctl" for image, dockerfile, context in AIAC_IMAGES: if _image_present(image): @@ -136,7 +136,7 @@ def ensure_aiac_deployed(cfg: Config) -> None: kubectl("apply", "-f", str(AIAC_ROOT / "k8s" / manifest)) kubectl("wait", "deployment/aiac-interface", "-n", AIAC_NAMESPACE, "--for=condition=Available", "--timeout=120s") - kubectl("wait", "statefulset/aiac-policy-store", "-n", AIAC_NAMESPACE, "--for=jsonpath={.status.readyReplicas}=1", "--timeout=120s") + kubectl("wait", "statefulset/aiac-policy-model-store", "-n", AIAC_NAMESPACE, "--for=jsonpath={.status.readyReplicas}=1", "--timeout=120s") kubectl("wait", "deployment/aiac-agent", "-n", AIAC_NAMESPACE, "--for=condition=Available", "--timeout=120s") ok("AIAC stack deployed") @@ -186,15 +186,15 @@ def wait_for_client_registration(cfg: Config, timeout: float = 180.0) -> None: def verify_mcp_label(namespace: str) -> None: label = kubectl( "get", "service", scn.TOOL_WORKLOAD, "-n", namespace, - "-o", "jsonpath={.metadata.labels.protocol\\.kagenti\\.io/mcp}", + "-o", "jsonpath={.metadata.labels.protocol\\.rossoctl\\.io/mcp}", ).strip() if label != "true": abort( f"Service {scn.TOOL_WORKLOAD!r} in {namespace!r} is missing the " - f"protocol.kagenti.io/mcp='true' LABEL (found: {label!r}) — UC-1's analyze_tool will " + f"protocol.rossoctl.io/mcp='true' LABEL (found: {label!r}) — UC-1's analyze_tool will " f"502 during onboarding. See demo/assets/INSTALL.md." ) - ok(f"Service {scn.TOOL_WORKLOAD!r} carries protocol.kagenti.io/mcp='true'") + ok(f"Service {scn.TOOL_WORKLOAD!r} carries protocol.rossoctl.io/mcp='true'") def main() -> None: diff --git a/aiac/demo/use-cases/uc1-onboarding/init/01-clear.py b/aiac/demo/use-cases/uc1-onboarding/init/02-clear.py similarity index 69% rename from aiac/demo/use-cases/uc1-onboarding/init/01-clear.py rename to aiac/demo/use-cases/uc1-onboarding/init/02-clear.py index d9eda5cd2..77fd29d22 100644 --- a/aiac/demo/use-cases/uc1-onboarding/init/01-clear.py +++ b/aiac/demo/use-cases/uc1-onboarding/init/02-clear.py @@ -1,9 +1,10 @@ #!/usr/bin/env python3 """Reset the demo to a clean slate: delete UC-1's provisioned Keycloak roles/scopes, clear the Policy Store (non-optional — its SQLite survives on a PV and onboarding appends with -``override=False``), and clear both the writer pod's ``/rego`` and the local ``generated/`` copy. +``override=False``), delete the agent's ``AuthorizationPolicy`` CR (the reworked writer is +CR-backed — there is no ``/rego`` file to wipe), and clear the local ``generated/`` copy. -Kept separate from ``02-setup.py`` so a presenter can re-run just the reset between takes. +Kept separate from ``03-setup.py`` so a presenter can re-run just the reset between takes. """ from __future__ import annotations @@ -14,7 +15,7 @@ sys.path.insert(0, str(Path(__file__).resolve().parent.parent / "lib")) -from _lib import GENERATED, cleanup_provisioned, clear_policy_store, clear_writer_rego, connect_admin, load_config, ok, say, writer_pod +from _lib import GENERATED, cleanup_provisioned, clear_policy_store, clear_writer_rego, connect_admin, load_config, ok, say def main() -> None: @@ -29,10 +30,9 @@ def main() -> None: clear_policy_store(cfg) ok("Policy Store cleared") - say("3", "4", "Clear writer pod's /rego") - pod = writer_pod(cfg) - clear_writer_rego(cfg, pod) - ok(f"cleared /rego on pod {pod!r}") + say("3", "4", "Delete the agent's AuthorizationPolicy CR") + clear_writer_rego(cfg) + ok(f"deleted AuthorizationPolicy {cfg.cr_name!r} in {cfg.namespace!r} (if present)") say("4", "4", "Clear local generated/ snapshots") if GENERATED.exists(): diff --git a/aiac/demo/use-cases/uc1-onboarding/init/02-setup.py b/aiac/demo/use-cases/uc1-onboarding/init/03-setup.py similarity index 96% rename from aiac/demo/use-cases/uc1-onboarding/init/02-setup.py rename to aiac/demo/use-cases/uc1-onboarding/init/03-setup.py index 1bedeab6d..5eeffb0ea 100644 --- a/aiac/demo/use-cases/uc1-onboarding/init/02-setup.py +++ b/aiac/demo/use-cases/uc1-onboarding/init/03-setup.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 """Provision the demo's Keycloak users/roles, mount the PRB's policy.md on the Controller, run the token-exchange Keycloak setup, and resolve + print both workloads' internal client UUIDs (the -trigger ids ``03-onboard-agent.py``/``04-onboard-tool.py`` need — the ``clientId`` is a slash-bearing +trigger ids ``04-onboard-agent.py``/``05-onboard-tool.py`` need — the ``clientId`` is a slash-bearing SPIFFE URI the single-segment ``/apply/service/{id}`` route can't carry). """ diff --git a/aiac/demo/use-cases/uc1-onboarding/lib/_lib.py b/aiac/demo/use-cases/uc1-onboarding/lib/_lib.py index 055cc99f8..db9831324 100644 --- a/aiac/demo/use-cases/uc1-onboarding/lib/_lib.py +++ b/aiac/demo/use-cases/uc1-onboarding/lib/_lib.py @@ -30,6 +30,15 @@ HERE = Path(__file__).resolve().parent.parent # lib/ -> uc1-onboarding/ GENERATED = HERE / "generated" +# The reworked PDP Policy Writer (OPA) is CR-backed: it server-side-applies one +# per-agent AuthorizationPolicy CR (agent.rossoctl.dev/v1alpha1) and, in production, writes +# NO .rego files (k8s/pdp-interface-deployment.yaml keeps POLICY_WRITER_DUMP_REGO off and mounts +# no /rego). So this demo sources its rego straight from the CR's spec.policies[].content — the +# same artifact a live enforcement point consumes — rather than kubectl-cp-ing a dumped file. +# Fully qualified to disambiguate from Istio's security.istio.io AuthorizationPolicy (same short +# name): `kubectl get authorizationpolicies.agent.rossoctl.dev/ -n `. +AUTHZ_POLICY_RESOURCE = "authorizationpolicies.agent.rossoctl.dev" + # ====================================================================================== # Narration — terminal output in the authbridge/demos house style @@ -112,27 +121,29 @@ class Config: onboard_timeout: float - opa_namespace: str - opa_selector: str - opa_container: str - opa_pod: str | None - opa_rego_path: str - keycloak_url: str keycloak_admin_username: str keycloak_admin_password: str @property - def agent_slug(self) -> str: - return f"{self.namespace}_{scn.AGENT_WORKLOAD}".replace("-", "_") + def cr_name(self) -> str: + """The demo agent's ``AuthorizationPolicy`` CR name. The reworked writer places a CR at + ``identity_ref(agent_id) -> (namespace, name)`` and bundle-service matches it by + name+namespace; for this demo that name is just ``AGENT_WORKLOAD`` (``github-agent``) in + ``cfg.namespace`` (``team1``). No per-agent underscore slug any more — the package name is + fixed (``authbridge.client.{inbound,outbound}.request``), never per-agent.""" + return scn.AGENT_WORKLOAD @property def inbound_rego(self) -> str: - return f"{self.agent_slug}.inbound.rego" + """Snapshot-relative path of the inbound rego, mirroring the CR ``policies[].path`` under + the writer's nested ``//`` layout (``rego_dir / cfg.inbound_rego`` is the local + file ``capture_rego`` writes).""" + return f"{self.namespace}/{scn.AGENT_WORKLOAD}/inbound/request.rego" @property def outbound_rego(self) -> str: - return f"{self.agent_slug}.outbound.rego" + return f"{self.namespace}/{scn.AGENT_WORKLOAD}/outbound/request.rego" def require_env(*names: str) -> dict[str, str]: @@ -153,18 +164,13 @@ def load_config() -> Config: controller_local_port=int(os.environ.get("AIAC_CONTROLLER_LOCAL_PORT", "7070")), controller_remote_port=int(os.environ.get("AIAC_CONTROLLER_REMOTE_PORT", "7070")), store_namespace=os.environ.get("AIAC_STORE_NAMESPACE", "aiac-system"), - store_target=os.environ.get("AIAC_STORE_TARGET", "svc/aiac-policy-store-service"), + store_target=os.environ.get("AIAC_STORE_TARGET", "svc/aiac-policy-model-store-service"), store_local_port=int(os.environ.get("AIAC_STORE_LOCAL_PORT", "7074")), store_remote_port=int(os.environ.get("AIAC_STORE_REMOTE_PORT", "7074")), controller_deployment=os.environ.get("AIAC_CONTROLLER_DEPLOYMENT", "aiac-agent"), policy_configmap=os.environ.get("AIAC_POLICY_CONFIGMAP", "aiac-policy"), policy_mount_path=os.environ.get("AIAC_POLICY_MOUNT_PATH", "/etc/aiac"), onboard_timeout=float(os.environ.get("AIAC_ONBOARD_TIMEOUT", "900")), - opa_namespace=os.environ.get("AIAC_OPA_NAMESPACE", "aiac-system"), - opa_selector=os.environ.get("AIAC_OPA_SELECTOR", "app=aiac-interface"), - opa_container=os.environ.get("AIAC_OPA_CONTAINER", "aiac-pdp-policy-opa"), - opa_pod=os.environ.get("AIAC_OPA_POD"), - opa_rego_path=os.environ.get("AIAC_OPA_REGO_PATH", "/rego"), keycloak_url=creds["KEYCLOAK_URL"], keycloak_admin_username=creds["KEYCLOAK_ADMIN_USERNAME"], keycloak_admin_password=creds["KEYCLOAK_ADMIN_PASSWORD"], @@ -198,20 +204,6 @@ def kubectl_rollout_status(resource: str, *, namespace: str, timeout: float = 18 kubectl("rollout", "status", resource, "-n", namespace, f"--timeout={int(timeout)}s", timeout=timeout + 10) -def kubectl_cp(pod: str, remote_path: str, local_path: Path, *, namespace: str, container: str | None = None) -> None: - args = ["cp", f"{namespace}/{pod}:{remote_path}", str(local_path)] - if container: - args += ["-c", container] - kubectl(*args, timeout=120.0) - - -def resolve_pod(selector: str, *, namespace: str) -> str: - out = kubectl("get", "pods", "-n", namespace, "-l", selector, "-o", "jsonpath={.items[0].metadata.name}") - if not out.strip(): - abort(f"no pod matches selector {selector!r} in namespace {namespace!r}") - return out.strip() - - def terminate(proc: subprocess.Popen) -> None: proc.terminate() try: @@ -427,24 +419,34 @@ def onboard(cfg: Config, base_url: str, service_id: str) -> None: abort(f"onboard {service_id!r} at {base_url}: HTTP {resp.status_code} — {resp.text[:500]}") -def writer_pod(cfg: Config) -> str: - return cfg.opa_pod or resolve_pod(cfg.opa_selector, namespace=cfg.opa_namespace) +def clear_writer_rego(cfg: Config) -> None: + """Reset the writer's state for the demo agent by deleting its AuthorizationPolicy CR. - -def clear_writer_rego(cfg: Config, pod: str) -> None: + The reworked writer is CR-backed (no ``/rego`` file dump in production), so "clear the + writer's rego" means "delete the CR" — the next onboard server-side-applies a fresh one. + Idempotent: ``--ignore-not-found`` treats an already-absent CR as success.""" kubectl( - "exec", "-n", cfg.opa_namespace, pod, "-c", cfg.opa_container, "--", - "sh", "-c", f"rm -f {cfg.opa_rego_path.rstrip('/')}/*.rego", + "delete", AUTHZ_POLICY_RESOURCE, cfg.cr_name, + "-n", cfg.namespace, "--ignore-not-found", ) -def capture_rego(cfg: Config, pod: str, rego_dir: Path) -> None: - rego_dir.mkdir(parents=True, exist_ok=True) - for filename in (cfg.inbound_rego, cfg.outbound_rego): - kubectl_cp( - pod, f"{cfg.opa_rego_path.rstrip('/')}/{filename}", rego_dir / filename, - namespace=cfg.opa_namespace, container=cfg.opa_container, +def capture_rego(cfg: Config, rego_dir: Path) -> None: + """Fetch the agent's generated rego straight from its AuthorizationPolicy CR and write each + ``spec.policies[].content`` into ``rego_dir`` under the writer's nested ``//`` + layout, so ``opa eval`` / ``show-state.py`` read the very bytes a live enforcement point would + (the CR is production's source of truth — there is no ``/rego`` file to ``kubectl cp``).""" + cr = kubectl_get_json(f"{AUTHZ_POLICY_RESOURCE}/{cfg.cr_name}", namespace=cfg.namespace) + policies = cr.get("spec", {}).get("policies", []) + if not policies: + abort( + f"AuthorizationPolicy {cfg.cr_name!r} in namespace {cfg.namespace!r} has no " + "spec.policies — did the onboard call actually write the CR?" ) + for policy in policies: + dest = rego_dir / cfg.namespace / cfg.cr_name / policy["path"] + dest.parent.mkdir(parents=True, exist_ok=True) + dest.write_text(policy["content"]) # ====================================================================================== @@ -523,7 +525,13 @@ def drive(username: str) -> None: ok("logged in") say("2", "3", "Inbound gate: may this user call the agent?") - inbound_allowed = bool(opa_eval([inbound_rego], f"data.authz.{cfg.agent_slug}.inbound.allow", {"subject": username})) + # Fixed package + live-plugin input shape. This demo path is an end-user ROPC login with no + # platform source client, so we send NO input.identity.client_id — source_ok is satisfied by + # the writer's `source_ok if { not input.identity.client_id }` rule. + inbound_allowed = bool(opa_eval( + [inbound_rego], "data.authbridge.client.inbound.request.allow", + {"identity": {"subject": username}}, + )) expected_in = scn.expected_inbound(username) if inbound_allowed != expected_in: abort(f"inbound mismatch for subject={username!r}: opa said {inbound_allowed}, expected {expected_in}") @@ -540,7 +548,9 @@ def drive(username: str) -> None: agent_client_id = admin.get_client(agent_uuid)["clientId"] secret = client_secret(admin, cfg, agent_uuid) - target_scopes = opa_eval([outbound_rego], f"data.authz.{cfg.agent_slug}.outbound.target_scopes", {}) or {} + # target_scopes is now keyed by the FULL target service id (a SPIFFE id), with bare + # de-prefixed scope values. next(iter(...)) still yields the id to exchange for. + target_scopes = opa_eval([outbound_rego], "data.authbridge.client.outbound.request.target_scopes", {}) or {} if not target_scopes: abort(f"outbound rego at {outbound_rego} has no target_scopes — is the tool onboarded?") target_uri = next(iter(target_scopes)) @@ -551,8 +561,9 @@ def drive(username: str) -> None: rows: list[tuple[str, ...]] = [] for intent in scn.INTENTS[username]: allowed = bool(opa_eval( - [outbound_rego], f"data.authz.{cfg.agent_slug}.outbound.allow", - {"subject": username, "function_name": intent.function_name, "target": target_uri}, + [outbound_rego], "data.authbridge.client.outbound.request.allow", + {"identity": {"subject": username, "service_id": target_uri}, + "mcp": {"params": {"name": intent.function_name}}}, )) expected_out = scn.expected_outbound(username, intent.function_name) if allowed != expected_out: diff --git a/aiac/demo/use-cases/uc1-onboarding/lib/scenario.py b/aiac/demo/use-cases/uc1-onboarding/lib/scenario.py index 34371bb69..f84f0d7fb 100644 --- a/aiac/demo/use-cases/uc1-onboarding/lib/scenario.py +++ b/aiac/demo/use-cases/uc1-onboarding/lib/scenario.py @@ -15,7 +15,7 @@ # --- Realm + deployment identifiers --------------------------------------------------------- -REALM_DEFAULT = "kagenti" +REALM_DEFAULT = "rossoctl" DEMO_NAMESPACE_DEFAULT = "team1" AGENT_WORKLOAD = "github-agent" TOOL_WORKLOAD = "github-tool" @@ -76,6 +76,11 @@ AGENT_ROLES: dict[str, str] = dict(AGENT_SCOPES) +# The provisioned Keycloak client-scope NAMES stay owner-prefixed (that is what Keycloak holds). +# The reworked writer de-prefixes them into the outbound Rego / CR — so the BARE tail +# (``source-read`` …) is the value that arrives in ``input.mcp.params.name`` and the value the +# outbound maps below key on. Keep this distinction in mind: prefixed here (Keycloak), bare in the +# oracle pair-lists and INTENTS (Rego / the invoked MCP tool name). TOOL_SCOPES: dict[str, str] = { "github-tool.source-read": "Read source repository contents: file listings and file bodies. Read-only.", "github-tool.source-write": "Create, modify, or delete source repository contents; commit file changes.", @@ -91,19 +96,22 @@ ("tester", "github-agent.issue_operations"), ] +# Tool scopes here are BARE (de-prefixed) — they must match the outbound Rego's de-prefixed map +# values and the bare ``input.mcp.params.name`` the drivers send. (Agent-side operator roles keep +# their owner prefix; only the tool-scope value de-prefixes.) OUTBOUND_SUBJECT_PAIRS: list[tuple[str, str]] = [ - ("developer", "github-tool.source-read"), - ("developer", "github-tool.source-write"), - ("developer", "github-tool.issues-read"), - ("tester", "github-tool.issues-read"), - ("tester", "github-tool.issues-write"), + ("developer", "source-read"), + ("developer", "source-write"), + ("developer", "issues-read"), + ("tester", "issues-read"), + ("tester", "issues-write"), ] OUTBOUND_TARGET_PAIRS: list[tuple[str, str]] = [ - ("github-agent.source_operations", "github-tool.source-read"), - ("github-agent.source_operations", "github-tool.source-write"), - ("github-agent.issue_operations", "github-tool.issues-read"), - ("github-agent.issue_operations", "github-tool.issues-write"), + ("github-agent.source_operations", "source-read"), + ("github-agent.source_operations", "source-write"), + ("github-agent.issue_operations", "issues-read"), + ("github-agent.issue_operations", "issues-write"), ] # --- The single abstract policy.md the PRB reads (verbatim; also shown in demo.md) ----------- @@ -121,20 +129,22 @@ @dataclass(frozen=True) class Intent: label: str # what the driver script prints before asking the tool - function_name: str | None # the tool scope this intent maps to (None = inbound-only denial case) + # The BARE invoked MCP tool name this intent maps to — the value that arrives in + # ``input.mcp.params.name`` (None = inbound-only denial case, no tool call reached). + function_name: str | None INTENTS: dict[str, list[Intent]] = { "dev-user": [ - Intent("read a file from the repo", "github-tool.source-read"), - Intent("commit a small fix", "github-tool.source-write"), - Intent("check an issue for repro steps", "github-tool.issues-read"), - Intent("close out the issue", "github-tool.issues-write"), + Intent("read a file from the repo", "source-read"), + Intent("commit a small fix", "source-write"), + Intent("check an issue for repro steps", "issues-read"), + Intent("close out the issue", "issues-write"), ], "test-user": [ - Intent("check an issue for repro steps", "github-tool.issues-read"), - Intent("file a new bug", "github-tool.issues-write"), - Intent("read a file from the repo", "github-tool.source-read"), + Intent("check an issue for repro steps", "issues-read"), + Intent("file a new bug", "issues-write"), + Intent("read a file from the repo", "source-read"), ], "devops-user": [ Intent("ask the agent anything", None), # inbound denial — no function_name reached diff --git a/aiac/demo/use-cases/uc1-onboarding/lib/setup_keycloak.py b/aiac/demo/use-cases/uc1-onboarding/lib/setup_keycloak.py index 06cb70a2b..36cb61c8b 100644 --- a/aiac/demo/use-cases/uc1-onboarding/lib/setup_keycloak.py +++ b/aiac/demo/use-cases/uc1-onboarding/lib/setup_keycloak.py @@ -5,7 +5,7 @@ demo runs against both the freshly-installed cluster (where none of this exists yet) and the already-configured one this was developed against (where all of it is already true). -Not a standalone script — imported by ``02-setup.py``, its sole caller (unlike ``scenario.py``/ +Not a standalone script — imported by ``03-setup.py``, its sole caller (unlike ``scenario.py``/ ``_lib.py``, which are shared across ``init/``/``onboard/``/``run/`` and live in ``lib/``). """ @@ -57,8 +57,8 @@ def ensure_default_audience_scope(admin, cfg: Config, client_uuid: str, client_n explicitly. Returns ``True`` when the scope is now assigned (added here or already present) and ``False`` - when the scope does not exist yet, so the scope was skipped. The ``02-setup.py`` path calls this - before the tool is onboarded, where ``False`` is expected; the ``04-onboard-tool.py`` path calls + when the scope does not exist yet, so the scope was skipped. The ``03-setup.py`` path calls this + before the tool is onboarded, where ``False`` is expected; the ``05-onboard-tool.py`` path calls it afterwards, where ``False`` means the token-exchange audience is missing and onboarding must not report success.""" admin.change_current_realm(cfg.realm) diff --git a/aiac/demo/use-cases/uc1-onboarding/onboard/03-onboard-agent.py b/aiac/demo/use-cases/uc1-onboarding/onboard/04-onboard-agent.py similarity index 70% rename from aiac/demo/use-cases/uc1-onboarding/onboard/03-onboard-agent.py rename to aiac/demo/use-cases/uc1-onboarding/onboard/04-onboard-agent.py index 1fa564b13..01f534def 100644 --- a/aiac/demo/use-cases/uc1-onboarding/onboard/03-onboard-agent.py +++ b/aiac/demo/use-cases/uc1-onboarding/onboard/04-onboard-agent.py @@ -1,7 +1,8 @@ #!/usr/bin/env python3 """Onboard the ``github-agent`` workload: ``POST /apply/service/{uuid}`` behind a port-forward to -the Controller, then capture the generated ``.rego`` into ``generated/01-after-agent/`` — the first -pause's evidence (before the tool exists, the agent's outbound gate is still empty).""" +the Controller, then capture the generated rego from the agent's ``AuthorizationPolicy`` CR into +``generated/01-after-agent/`` — the first pause's evidence (before the tool exists, the agent's +outbound gate is still empty).""" from __future__ import annotations @@ -11,7 +12,7 @@ sys.path.insert(0, str(Path(__file__).resolve().parent.parent / "lib")) import scenario as scn -from _lib import GENERATED, capture_rego, connect_admin, load_config, note, ok, onboard, port_forward, resolve_service_id, say, writer_pod +from _lib import GENERATED, capture_rego, connect_admin, load_config, note, ok, onboard, port_forward, resolve_service_id, say def main() -> None: @@ -23,14 +24,13 @@ def main() -> None: note(f"service id: {service_id}") say("2", "3", "Onboard (POST /apply/service/{id}) — this drives the PRB and can take minutes") - with port_forward(cfg.controller_target, namespace=cfg.controller_namespace, local_port=cfg.controller_local_port, remote_port=cfg.controller_remote_port) as base_url: + with port_forward(cfg.controller_target, namespace=cfg.controller_namespace, local_port=cfg.controller_local_port, remote_port=cfg.controller_remote_port, ready_url=f"http://127.0.0.1:{cfg.controller_local_port}/health") as base_url: onboard(cfg, base_url, service_id) ok("onboarding call returned 200") - say("3", "3", "Capture generated Rego") + say("3", "3", "Capture generated Rego (from the AuthorizationPolicy CR)") rego_dir = GENERATED / "01-after-agent" - pod = writer_pod(cfg) - capture_rego(cfg, pod, rego_dir) + capture_rego(cfg, rego_dir) for f in (cfg.inbound_rego, cfg.outbound_rego): ok(f"{rego_dir / f}") diff --git a/aiac/demo/use-cases/uc1-onboarding/onboard/04-onboard-tool.py b/aiac/demo/use-cases/uc1-onboarding/onboard/05-onboard-tool.py similarity index 78% rename from aiac/demo/use-cases/uc1-onboarding/onboard/04-onboard-tool.py rename to aiac/demo/use-cases/uc1-onboarding/onboard/05-onboard-tool.py index 5b7029a11..216141941 100644 --- a/aiac/demo/use-cases/uc1-onboarding/onboard/04-onboard-tool.py +++ b/aiac/demo/use-cases/uc1-onboarding/onboard/05-onboard-tool.py @@ -1,9 +1,10 @@ #!/usr/bin/env python3 """Onboard the ``github-tool`` workload: ``POST /apply/service/{uuid}`` behind a port-forward to -the Controller, then capture the agent's ``.rego`` into ``generated/02-after-tool/`` — the second -pause's evidence. Onboarding the tool retroactively completes the agent's outbound gate (the tool -is a pure target: no ``.rego`` is emitted for it directly), so only the agent's two files are -copied, into a directory separate from ``01-after-agent/`` — the before/after diff is the point.""" +the Controller, then capture the agent's rego from its ``AuthorizationPolicy`` CR into +``generated/02-after-tool/`` — the second pause's evidence. Onboarding the tool retroactively +completes the agent's outbound gate (the tool is a pure target: no CR is emitted for it directly), +so only the agent's CR is captured, into a directory separate from ``01-after-agent/`` — the +before/after diff is the point.""" from __future__ import annotations @@ -14,7 +15,7 @@ import scenario as scn import setup_keycloak -from _lib import GENERATED, abort, capture_rego, connect_admin, load_config, note, ok, onboard, port_forward, resolve_service_id, say, writer_pod +from _lib import GENERATED, abort, capture_rego, connect_admin, load_config, note, ok, onboard, port_forward, resolve_service_id, say def main() -> None: @@ -26,24 +27,23 @@ def main() -> None: note(f"service id: {service_id}") say("2", "4", "Onboard (POST /apply/service/{id}) — this drives the PRB and can take minutes") - with port_forward(cfg.controller_target, namespace=cfg.controller_namespace, local_port=cfg.controller_local_port, remote_port=cfg.controller_remote_port) as base_url: + with port_forward(cfg.controller_target, namespace=cfg.controller_namespace, local_port=cfg.controller_local_port, remote_port=cfg.controller_remote_port, ready_url=f"http://127.0.0.1:{cfg.controller_local_port}/health") as base_url: onboard(cfg, base_url, service_id) ok("onboarding call returned 200") - say("3", "4", "Capture generated Rego (agent's, retroactively completed)") + say("3", "4", "Capture generated Rego (agent's, retroactively completed — from the CR)") rego_dir = GENERATED / "02-after-tool" - pod = writer_pod(cfg) - capture_rego(cfg, pod, rego_dir) + capture_rego(cfg, rego_dir) for f in (cfg.inbound_rego, cfg.outbound_rego): ok(f"{rego_dir / f}") - # The tool's ``*-aud`` audience client scope only exists once the tool is onboarded, so 02-setup.py + # The tool's ``*-aud`` audience client scope only exists once the tool is onboarded, so 03-setup.py # could not yet assign it as a default scope on the agent client. Do it now, so an exchanged token's # ``aud`` reaches the tool without the caller requesting the scope explicitly. Idempotent. say("4", "4", "Assign the tool-audience default scope to the agent client") agent_uuid = resolve_service_id(admin, cfg, f"{cfg.namespace}/{scn.AGENT_WORKLOAD}") tool_aud_scope = f"agent-{cfg.namespace}-{scn.TOOL_WORKLOAD}-aud" - # Unlike 02-setup.py (which runs before the tool exists, so a missing scope is expected), here the + # Unlike 03-setup.py (which runs before the tool exists, so a missing scope is expected), here the # tool has just been onboarded — the ``*-aud`` scope must exist now. If it doesn't, the exchanged # token would lack the tool audience and downstream calls would silently fail, so abort rather # than report a success the token exchange can't back up. diff --git a/aiac/demo/use-cases/uc1-onboarding/show-state.py b/aiac/demo/use-cases/uc1-onboarding/show-state.py index 9a305d6b5..969e98b57 100644 --- a/aiac/demo/use-cases/uc1-onboarding/show-state.py +++ b/aiac/demo/use-cases/uc1-onboarding/show-state.py @@ -56,11 +56,14 @@ def show_keycloak(admin, cfg) -> None: def grant_sets(cfg, rego_dir: Path) -> tuple[set[tuple[str, str]], set[tuple[str, str]]]: inbound_rego = rego_dir / cfg.inbound_rego outbound_rego = rego_dir / cfg.outbound_rego - role_scopes = opa_eval([inbound_rego], f"data.authz.{cfg.agent_slug}.inbound.role_scopes", {}) or {} - agent_scopes = set(opa_eval([inbound_rego], f"data.authz.{cfg.agent_slug}.inbound.agent_scopes", {}) or []) + # Inbound values stay FULL agent-scope names (not de-prefixed) — the inbound gate compares + # role_scopes against agent_scopes internally, never against input.mcp.params.name. + role_scopes = opa_eval([inbound_rego], "data.authbridge.client.inbound.request.role_scopes", {}) or {} + agent_scopes = set(opa_eval([inbound_rego], "data.authbridge.client.inbound.request.agent_scopes", {}) or []) inbound = {(role, scope) for role, scopes in role_scopes.items() for scope in scopes if scope in agent_scopes} - subj_scopes = opa_eval([outbound_rego], f"data.authz.{cfg.agent_slug}.outbound.subject_role_scopes", {}) or {} + # Outbound subject_role_scopes values are now BARE de-prefixed tool scopes. + subj_scopes = opa_eval([outbound_rego], "data.authbridge.client.outbound.request.subject_role_scopes", {}) or {} outbound = {(role, scope) for role, scopes in subj_scopes.items() for scope in scopes} return inbound, outbound diff --git a/aiac/docs/agents/domain.md b/aiac/docs/agents/domain.md new file mode 100644 index 000000000..83dcff9d8 --- /dev/null +++ b/aiac/docs/agents/domain.md @@ -0,0 +1,56 @@ +# Domain Docs + +How the engineering skills should consume this repo's domain documentation +when exploring the codebase. This is scoped to `aiac/` — treated as its own +single context, separate from other components in the `cortex` monorepo (e.g. +`authbridge/`). + +## Before exploring, read these + +- **`CONTEXT.md`** at the `aiac/` root (this directory). +- **`docs/adr/`** (under `aiac/`) — read ADRs that touch the area you're about + to work in. + +If any of these files don't exist, **proceed silently**. Don't flag their +absence; don't suggest creating them upfront. The producer skill +(`/grill-with-docs`) creates them lazily when terms or decisions actually get +resolved. + +## File structure + +``` +aiac/ +├── CONTEXT.md +├── docs/ +│ ├── adr/ +│ │ ├── 0001-....md +│ │ └── 0002-....md +│ ├── specs/ ← PRD.md + components/ (see CLAUDE.md) +│ └── agents/ ← this file and its siblings +└── src/aiac/ +``` + +## Use the glossary's vocabulary + +When your output names a domain concept (in an issue title, a refactor +proposal, a hypothesis, a test name), use the term as defined in `CONTEXT.md`. +Don't drift to synonyms the glossary explicitly avoids. + +If the concept you need isn't in the glossary yet, that's a signal — either +you're inventing language the project doesn't use (reconsider) or there's a +real gap (note it for `/grill-with-docs`). + +## Flag ADR conflicts + +If your output contradicts an existing ADR, surface it explicitly rather than +silently overriding: + +> _Contradicts ADR-0007 (event-sourced orders) — but worth reopening because…_ + +## Relationship to `docs/specs/` + +`aiac/CLAUDE.md` already documents `docs/specs/PRD.md` and +`docs/specs/components/` as the requirements source, with a link-following +policy for cross-references. `CONTEXT.md` and `docs/adr/` are a different +layer — domain vocabulary and past architectural decisions, not requirements — +and don't replace the `docs/specs/` link-following policy. diff --git a/aiac/docs/agents/issue-tracker.md b/aiac/docs/agents/issue-tracker.md new file mode 100644 index 000000000..33935ff4e --- /dev/null +++ b/aiac/docs/agents/issue-tracker.md @@ -0,0 +1,44 @@ +# Issue tracker: GitHub (AIAC convention) + +Issues live as GitHub issues on **`s-and-p-team/cortex`**, organized in the +org-level **AIAC** Project (Projects v2): +. Use the `gh` CLI for all +operations, always scoped with `-R s-and-p-team/cortex` (this repo's `origin` +remote points here, but explicit `-R` avoids ambiguity since `upstream` also +exists). + +This is the same convention already documented in `CLAUDE.md` under "Issue +tracking" — this file exists so the engineering skills (`to-issues`, `triage`, +`to-prd`, `qa`) have a single place to read it from. + +## Conventions + +- **Create an issue**: `gh issue create -R s-and-p-team/cortex --title "..." --body "..." --label aiac`. + Use a heredoc for multi-line bodies. Always include the `aiac` label plus the + relevant cumulative `area:` label(s) for the component being touched. +- **Read an issue**: `gh issue view -R s-and-p-team/cortex --comments`. +- **List issues**: `gh issue list -R s-and-p-team/cortex --label aiac --state all`, + narrowing with additional `--label area:` or `--label aiac-status:` + filters as needed. +- **Comment on an issue**: `gh issue comment -R s-and-p-team/cortex --body "..."`. +- **Apply / remove labels**: `gh issue edit -R s-and-p-team/cortex --add-label "..."` / `--remove-label "..."`. +- **Close**: `gh issue close -R s-and-p-team/cortex --comment "..."`. + +## Hierarchy + +The Project groups **Feature**-typed container issues (one per component area, +nested via GitHub **native sub-issues**) over **Task**-typed leaf issues. Every +issue carries the `aiac` label plus cumulative `area:` labels. See +`docs/agents/triage-labels.md` for how triage state is represented. + +## When a skill says "publish to the issue tracker" + +Create a GitHub issue on `s-and-p-team/cortex` with the `aiac` label and the +appropriate `area:` label(s). + +## When a skill says "fetch the relevant ticket" + +Run `gh issue view -R s-and-p-team/cortex --comments`. + +Filtered web list: + diff --git a/aiac/docs/agents/triage-labels.md b/aiac/docs/agents/triage-labels.md new file mode 100644 index 000000000..1dc4a282c --- /dev/null +++ b/aiac/docs/agents/triage-labels.md @@ -0,0 +1,32 @@ +# Triage Labels + +The skills speak in terms of five canonical triage roles. This file maps those +roles to the actual label strings used in this repo's issue tracker. + +| Label in mattpocock/skills | Label in our tracker | Meaning | +| --------------------------- | ------------------------------ | ----------------------------------------- | +| `needs-triage` | `aiac-status:needs-triage` | Maintainer needs to evaluate this issue | +| `needs-info` | `aiac-status:needs-info` | Waiting on reporter for more information | +| `ready-for-agent` | `aiac-status:ready-for-agent` | Fully specified, ready for an AFK agent | +| `ready-for-human` | `aiac-status:ready-for-human` | Requires human implementation | +| `wontfix` | `aiac-status:wontfix` | Will not be actioned | + +When a skill mentions a role (e.g. "apply the AFK-ready triage label"), apply +the corresponding `aiac-status:` label from the right-hand column via +`gh issue edit -R s-and-p-team/cortex --add-label "aiac-status:..." --remove-label "aiac-status:"`. + +## Two separate things named "status" + +Don't confuse these: + +- **`aiac-status:` label** — the source of truth this table maps to. + Set it directly with `gh issue edit --add-label` / `--remove-label`. +- **`AIAC Status` Project field** — a separate field on the org-level AIAC + Project (Projects v2) board. It's a display/board-column field, not a label, + and `gh issue edit` does not touch it. If a skill needs to keep the board in + sync, that requires `gh project item-edit` against the AIAC Status field — + don't assume setting the label alone moves the card. + +Every open issue also carries the `aiac` label plus cumulative `area:` +labels (see `docs/agents/issue-tracker.md`) — those are unrelated to triage +state and should not be touched by triage operations. diff --git a/aiac/docs/analysis/discover_mcp_services.py b/aiac/docs/analysis/discover_mcp_services.py index 172953659..eee9c82ba 100644 --- a/aiac/docs/analysis/discover_mcp_services.py +++ b/aiac/docs/analysis/discover_mcp_services.py @@ -29,7 +29,7 @@ # The label value is "" (empty string) — the selector matches on key presence. mcp_services = v1.list_namespaced_service( namespace="team1", - label_selector="protocol.kagenti.io/mcp", + label_selector="protocol.rossoctl.io/mcp", ) for svc in mcp_services.items: diff --git a/aiac/docs/analysis/keycloak-access-control-analysis.md b/aiac/docs/analysis/keycloak-access-control-analysis.md index 3386ce717..760adb732 100644 --- a/aiac/docs/analysis/keycloak-access-control-analysis.md +++ b/aiac/docs/analysis/keycloak-access-control-analysis.md @@ -1,4 +1,4 @@ -# Keycloak Access Control in Kagenti: U → Agent A → Tool T +# Keycloak Access Control in Rossoctl: U → Agent A → Tool T > Analysis of the minimum Keycloak configuration required for the > scenario where a user U calls agent A, which in turn calls tool T — @@ -15,13 +15,13 @@ ## 1. Architecture Overview -The Kagenti platform uses a **three-layer trust chain** enforced by: +The Rossoctl platform uses a **three-layer trust chain** enforced by: - **Keycloak** — OAuth2/OIDC token issuer and policy decision point (PDP) - **AuthBridge** — transparent sidecar proxy acting as pure policy enforcement point (PEP) - **SPIFFE/SPIRE** — workload identity via SPIFFE IDs used as Keycloak `clientId` -Every workload (agent or tool) has an AuthBridge sidecar injected by the kagenti-operator. The sidecar: +Every workload (agent or tool) has an AuthBridge sidecar injected by the rossoctl-operator. The sidecar: - **Inbound**: validates incoming JWTs (signature, issuer, audience) — returns `401` on failure - **Outbound**: intercepts HTTP calls to other services and performs RFC 8693 token exchange transparently @@ -54,7 +54,7 @@ Tool T pod | # | Object | Type | Key Properties | Purpose | |---|--------|------|----------------|---------| -| 1 | `kagenti` | Realm | `enabled: true` | Hosts all principals | +| 1 | `rossoctl` | Realm | `enabled: true` | Hosts all principals | | 2 | Agent A client | Keycloak Client | `clientId=A-SPIFFE`, `standard.token.exchange.enabled=true` | A's service identity; acting party in exchange | | 3 | Tool T client | Keycloak Client | `clientId=T-SPIFFE`, `standard.token.exchange.enabled=true` | Exchange target; T's audience anchor | | 4 | `agent-A-aud` scope | Client Scope + `oidc-audience-mapper` | `aud += A-SPIFFE`; assigned as **default** on U's client | Lets U obtain a token that A's inbound will accept | @@ -543,7 +543,7 @@ client_audience_targets: ## 15. RBAC End-to-End: Full Object Map ``` -Keycloak Realm: kagenti +Keycloak Realm: rossoctl │ ├── Realm Roles │ ├── UR ──composite──► agent-A:github-agent @@ -690,7 +690,7 @@ The following Keycloak objects and configuration steps from the RBAC model (§9 ## 20. Minimal Keycloak Configuration (OPA Model) ``` -Realm: kagenti +Realm: rossoctl │ ├── Clients │ ├── kagenti-ui (or per-user client) diff --git a/aiac/docs/examples/opa-team1-policy.yaml b/aiac/docs/examples/opa-team1-policy.yaml new file mode 100644 index 000000000..fdc14afa6 --- /dev/null +++ b/aiac/docs/examples/opa-team1-policy.yaml @@ -0,0 +1,137 @@ +# Example Rossoctl OPA AuthorizationPolicy (client-scoped, github-agent) +# +# A single, client-scoped policy CR that enforces one rule for exactly one +# workload in the `team1` namespace — the github-agent. +# +# INBOUND — deny user "alice" from reaching the github-agent. Every other +# authenticated user (e.g. "dev-user") stays allowed. +# +# OUTBOUND — gate the agent's token-exchanged call to a tool on both the +# delegated user's role and the exchange target audience. +# +# It carries a rego file per tier (inbound/request + outbound/request). The +# bundle-service composes each with the shipped global policy: for a tier the +# global combiner allows only when `ns_ok AND client_ok`, and `client_ok` +# comes from THIS package's `allow`. (A tier left undefined instead falls back +# to allow via the combiner's `client_ok if not data.authbridge.client.` +# rule — that is what happens when only one tier is present.) +# +# NOTE — outbound gating fields: the outbound rego below keys on +# `input.identity.subject`, `input.identity.service_id`, and +# `input.mcp.params.name` — all of which the live AuthBridge OPA plugin +# populates on the outbound leg (see opa-kind-runbook.md Part B.5). +# `input.identity.service_id` is the downstream service the exchanged token was +# minted for (the last delegation hop's target audience — here the github-tool +# SPIFFE ID). `input.mcp.params.name` is the specific tool being invoked, so the +# rule gates PER TOOL: it allows only when the delegated user's role AND the +# target service both admit the invoked tool. The `subject_role_scopes` / +# `target_scopes` maps below are keyed by the actual MCP tool names exposed by +# the deployed github-tool (aiac/demo/assets/tools/github_tool): `source-read`, +# `source-write`, `issues-read`, `issues-write`. Note that MCP methods which do +# not invoke a specific tool (e.g. a `tools/list` discovery request) carry no +# `params.name`, so they never match and are denied. +# +# IMPORTANT — how client-scope targeting actually works: bundle-service looks +# up a client-scope CR by **metadata.name + metadata.namespace**, matched +# against the ServiceAccount segment of the caller's SPIFFE ID (`spiffe:// +# /ns//sa/` -> namespace=, +# name=) — see operator/internal/bundleservice/identity/identity.go and +# watcher.go's `GetPolicy(name, namespace)`. `spec.clientID` is NOT consulted +# by that lookup (it's print-column metadata only); this CR is named +# `github-agent` — matching the `sa/github-agent` segment of +# `spiffe://localtest.me/ns/team1/sa/github-agent` — because that's what +# actually scopes it to this one workload. `clientID` below is set to the +# same short name purely for documentation/`kubectl get` display, and must +# satisfy the CRD's DNS-label-style validation regex (no `spiffe://`, no +# `/`). +# +# Identity note: on the INBOUND leg the OPA plugin exposes +# input.identity.{subject, client_id, scopes}, plus audience when the validated +# JWT carries it. On the OUTBOUND leg there is no validated JWT, so the plugin +# synthesizes input.identity from the token-exchange delegation hop: subject +# (delegated caller), client_id (this agent), scopes, and service_id (the +# downstream service the exchanged token targets — the last hop's audience). +# `subject` is the JWT `sub` claim. In this cluster the +# rossoctl-realm tokens carry the username in `sub` (via a username->sub +# protocol mapper on the `rossoctl` client — see A.1 in opa-kind-runbook.md, +# already applied cluster-wide). +apiVersion: agent.rossoctl.dev/v1alpha1 +kind: AuthorizationPolicy +metadata: + name: github-agent + namespace: team1 +spec: + scope: client + clientID: "github-agent" + policies: + - path: "inbound/request.rego" + content: | + package authbridge.client.inbound.request + import rego.v1 + + agent_scopes := ["github-agent.issue_operations", "github-agent.source_operations"] + + subject_roles := { + "dev-user": ["developer"], + "test-user": ["tester"], + } + + source_roles := {} + + role_scopes := { + "developer": ["github-agent.issue_operations", "github-agent.source_operations"], + "tester": ["github-agent.issue_operations"], + } + + subject_ok if { + some role in subject_roles[input.identity.subject] + some scope in role_scopes[role] + scope in agent_scopes + } + + source_ok if { not input.identity.client_id } + source_ok if { input.identity.client_id == "rossoctl"} + source_ok if { + some role in source_roles[input.identity.client_id] + some scope in role_scopes[role] + scope in agent_scopes + } + + default allow := false + allow if { subject_ok; source_ok } + + - path: "outbound/request.rego" + content: | + package authbridge.client.outbound.request + import rego.v1 + + agent_roles := ["github-agent.issue_operations", "github-agent.source_operations"] + subject_roles := { + "dev-user": ["developer"], + "test-user": ["tester"] + } + # The deployed github-tool (aiac/demo/assets/tools/github_tool) exposes + # exactly four MCP tools — source-read, source-write, issues-read, + # issues-write — one per skill. These names ARE the values that arrive in + # input.mcp.params.name when a specific tool is invoked, so the maps + # below key on them. + subject_role_scopes := { + "developer": ["issues-read", "source-write", "source-read"], + "tester": ["issues-read", "issues-write"], + } + agent_role_scopes := { + "github-agent.issue_operations": ["issues-read", "issues-write"], + "github-agent.source_operations": ["source-write", "source-read"], + } + target_scopes := { + "spiffe://localtest.me/ns/team1/sa/github-tool": ["source-read", "source-write", "issues-read", "issues-write"], + } + subject_ok if { + some role in subject_roles[input.identity.subject] + input.mcp.params.name in subject_role_scopes[role] + } + target_ok if { + input.mcp.params.name in target_scopes[input.identity.service_id] + } + default allow := false + allow if { subject_ok; target_ok } \ No newline at end of file diff --git a/aiac/docs/specs/PRD.md b/aiac/docs/specs/PRD.md index 339ee2a53..a50432c8f 100644 --- a/aiac/docs/specs/PRD.md +++ b/aiac/docs/specs/PRD.md @@ -2,7 +2,7 @@ ## Abstract -AI-based Access Control (AIAC) is a Kagenti platform extension that automates RBAC/ABAC policy +AI-based Access Control (AIAC) is a Rossoctl platform extension that automates RBAC/ABAC policy enforcement for AI agents running on Kubernetes. A LangGraph-based AI agent continuously translates a natural-language access control policy — stored in a vector knowledge base — into concrete permission configurations in the active Policy Decision Point (PDP), eliminating manual policy @@ -14,7 +14,7 @@ management (subjects, roles, services). ## 1. Problem Description -Kagenti AI agents call services across a shared platform. Every call must carry a token scoped to +Rossoctl AI agents call services across a shared platform. Every call must carry a token scoped to exactly the permissions the caller's role entitles on the target service. Without a dedicated policy management layer, access policy ends up scattered across per-deployment configuration, creating three compounding problems: @@ -102,7 +102,7 @@ Manually granted entitlements are flagged as policy-agnostic and surfaced during ## 5. Architecture Overview -Eight components across five Kubernetes Pods plus a Python library layer, all implemented in Python 3.12. External dependencies: Keycloak Admin API, an LLM API, and an embedding API. The Keycloak SPI listener is defined in a separate PRD. +Nine components across five Kubernetes Pods plus a Python library layer, all implemented in Python 3.12. External dependencies: Keycloak Admin API, an LLM API, and an embedding API. The Keycloak SPI listener is defined in a separate PRD. ### Component Summary @@ -110,12 +110,13 @@ Eight components across five Kubernetes Pods plus a Python library layer, all im |---|-----------|-------------| | 1 | **IdP Configuration Service** | REST service that exposes IdP entity data (subjects, roles, services, scopes) for read and write operations. Read methods enrich services with assigned roles/scopes and enrich roles with child roles. Backed by Keycloak. Python library: `aiac.idp.configuration`. | | 2 | **PDP Policy Writer** | REST service that applies LLM-generated Rego rules to the OPA backend. Writes derived Rego packages to an `AuthorizationPolicy` Kubernetes CR. Exposed as ClusterIP service `aiac-pdp-policy-service:7072`. Python library: `aiac.pdp.policy.library`. | -| 3 | **Policy Store** | REST service that owns an in-memory `PolicyModel` cache backed by SQLite as the authoritative structured policy store. Enables the Policy Computation Engine to read current `AgentPolicyModel` state for additive merging. Deployed as a dedicated single-replica StatefulSet (`aiac-policy-store`) at `:7074`. Python library: `aiac.policy.store.library`. | -| 4 | **Policy Computation Engine** | Pure Python library module (`aiac.policy.computation`). No service, no Kubernetes deployment. Receives `list[PolicyRule]` from AIAC Agent sub-agents, queries IdP to resolve owning services, additively merges rules into `AgentPolicyModel` objects in the Policy Store, and pushes the updated `PolicyModel` to the PDP Policy Writer. Single entry point: `compute_and_apply(rules)`. | +| 3 | **Policy Model Store** | REST service that owns an in-memory `PolicyModel` cache backed by SQLite as the authoritative structured policy store. Enables the Policy Computation Engine to read current `AgentPolicyModel` state for additive merging. Deployed as a dedicated single-replica StatefulSet (`aiac-policy-model-store`) at `:7074`. Python library: `aiac.policy.model_store.library`. | +| 4 | **Policy Computation Engine** | Pure Python library module (`aiac.policy.computation`). No service, no Kubernetes deployment. Receives `list[PolicyRule]` from AIAC Agent sub-agents, queries IdP to resolve owning services, additively merges rules into `AgentPolicyModel` objects in the Policy Model Store, and pushes the updated `PolicyModel` to the PDP Policy Writer. Single entry point: `compute_and_apply(rules)`. | | 5 | **Policy and Domain Knowledge RAG** | ChromaDB vector store holding the access control policy and domain knowledge in persistent, queryable form, populated via a co-located RAG Ingest Service. | -| 6 | **Event Broker** | NATS JetStream pod that decouples event producers (Keycloak SPI listener, RAG Ingest Service) from the AIAC Agent. Provides durable, at-least-once delivery with automatic replay on Agent pod restart. Competing consumer model ensures each event is processed exactly once. | -| 7 | **AIAC Agent** | LangGraph-based AI agent triggered by Event Broker subscriptions (`aiac.apply.>` subjects) and directly by the operator (`rebuild` only). Retrieves the current policy from the RAG store, interprets it against live PDP state, and applies the required policy changes immediately. | -| 8 | **Python library** | Python API library provides typed access to IdP and policy services via `aiac.idp.configuration`, `aiac.policy.model`, `aiac.policy.store.library`, `aiac.pdp.policy.library`, and `aiac.policy.computation` modules backed by generic Pydantic models. | +| 6 | **Policy Guardrails Agent** | Verification gate co-located with ChromaDB and the RAG Ingest Service in the RAG Pod. Every document is checked before the RAG Ingest Service writes it to ChromaDB. Reachable only on the RAG Pod's loopback network — not exposed on the RAG Pod's ClusterIP Service. One service, two API families (`policy`, `domain-knowledge`); the `policy` family runs LLM-backed hygiene + corpus-contradiction checks (defined), `domain-knowledge` specced later. | +| 7 | **Event Broker** | NATS JetStream pod that decouples event producers (Keycloak SPI listener, RAG Ingest Service) from the AIAC Agent. Provides durable, at-least-once delivery with automatic replay on Agent pod restart. Competing consumer model ensures each event is processed exactly once. | +| 8 | **AIAC Agent** | LangGraph-based AI agent triggered by Event Broker subscriptions (`aiac.apply.>` subjects) and directly by the operator (`rebuild` only). Retrieves the current policy from the RAG store, interprets it against live PDP state, and applies the required policy changes immediately. | +| 9 | **Python library** | Python API library provides typed access to IdP and policy services via `aiac.idp.configuration`, `aiac.policy.model`, `aiac.policy.model_store.library`, `aiac.pdp.policy.library`, and `aiac.policy.computation` modules backed by generic Pydantic models. | ### High-level architecture @@ -125,7 +126,7 @@ Eight components across five Kubernetes Pods plus a Python library layer, all im │ | (𝘶𝘴𝘦𝘳𝘴, 𝘳𝘰𝘭𝘦𝘴, 𝘤𝘭𝘪𝘦𝘯𝘵𝘴) (𝘈𝘶𝘵𝘩𝘰𝘳𝘪𝘻𝘢𝘵𝘪𝘰𝘯𝘗𝘰𝘭𝘪𝘤𝘺 𝘊𝘙) ┌──────────────┼──────────────────────┼───────────────────┐ -│ Kagenti Interface Pod │ │ +│ Rossoctl Interface Pod │ │ │ │ │ │ │ ┌───────┴──────┐ ┌────────┴───────┐ │ │ │ IdP Config │ │ PDP Policy │ │ @@ -134,23 +135,23 @@ Eight components across five Kubernetes Pods plus a Python library layer, all im │ ▲ ▲ │ └──────────────┼──────────────────────┼───────────────────┘ │ │ - │ │ - │ │ - │ ┌──────────────────────────────────────┐ - │ │ Policy Store Pod │ - │ │ │ - │ │ ┌───────────────────────────────┐ │ - │ │ │ Policy Store Service │ │ - │ │ │ │ │ - │ │ │ (SQLite policy.db) │ │ - │ │ └───────────────────────────────┘ │ - │ │ ▲ │ - │ └──────────────────┼───────────────────┘ - │ │ -┌──────────────┼──────────────────────┼───────────────────┐ ┌────────────────────────────────┐ -│ Agent Pod └───────────────────┐ │ │ │ Event Broker Pod │ -│ │ │ │ │ │ -│ ┌──────────────────────┐ ┌────────────────┐ │ │ ┌──────────────────────────┐ │ + ┌─┼──────────────────────┘ + │ │ + │ │ ┌──────────────────────────────────────┐ + │ │ │ Policy Model Store Pod │ + │ │ │ │ + │ │ │ ┌───────────────────────────────┐ │ + │ │ │ │ Policy Model Store Service │ │ + │ │ │ │ │ │ + │ │ │ │ (SQLite policy.db) │ │ + │ │ │ └───────────────────────────────┘ │ + │ │ │ ▲ │ + │ │ └──────────────────┼───────────────────┘ + │ │ │ +┌────────────┼─┼──────────────────────┼───────────────────┐ ┌────────────────────────────────┐ +│ Agent Pod │ └───────────────────┐ │ │ │ Event Broker Pod │ +│ │ │ │ │ │ │ +│ ┌─────────┴────────────┐ ┌────────────────┐ │ │ ┌──────────────────────────┐ │ │ │ Policy Compute Engn │◄──│ AIAC Agent │◄─────────┼──┼──│ NATS JetStream │ │ │ └──────────────────────┘ └────────────────┘ (𝘯𝘰𝘵𝘪𝘧𝘺) │ │ └──────────────────────────┘ │ │ │ │ │ ▲ ▲ │ @@ -162,7 +163,12 @@ Eight components across five Kubernetes Pods plus a Python library layer, all im │ ▼ │ │ ┌─────────────────────┐ ┌─────────────────────────┐ │ │ │ RAG Ingest Service │──►│ ChromaDB (vector store) │ │ -│ └─────────────────────┘ └─────────────────────────┘ │ +│ └──────────┬──────────┘ └─────────────────────────┘ │ +│ │ (verify) ▲ (context) │ +│ ▼ │ │ +│ ┌─────────────────────────┐ │ │ +│ │ Policy Guardrails Agent │─────────────┘ │ +│ └─────────────────────────┘ │ └─────────────────────────────────────────────────────────┘ ``` @@ -190,7 +196,7 @@ All inter-pod traffic is Kubernetes ClusterIP. External access is exclusively vi │ 7. [LLM] validate policy rules against retrieved policy (second pass) │ 8. compute_and_apply(rules) ──► Policy Computation Engine │ ├── get_services_by_role / get_services_by_scope ──► IdP Configuration Service - │ ├── get_agent_policy / apply_agent_policy ──► Policy Store + │ ├── get_agent_policy / apply_agent_policy ──► Policy Model Store │ └── apply_policy ──► PDP Policy Writer ──► AuthorizationPolicy CR │ 9. ACK message ▼ @@ -214,7 +220,7 @@ All inter-pod traffic is Kubernetes ClusterIP. External access is exclusively vi │ 6. [LLM] validate policy rules against retrieved policy (second pass) │ 7. compute_and_apply(rules) ──► Policy Computation Engine │ ├── get_services_by_role / get_services_by_scope ──► IdP Configuration Service - │ ├── get_agent_policy / apply_agent_policy ──► Policy Store + │ ├── get_agent_policy / apply_agent_policy ──► Policy Model Store │ └── apply_policy ──► PDP Policy Writer ──► AuthorizationPolicy CR │ 8. ACK message ▼ @@ -228,21 +234,22 @@ All inter-pod traffic is Kubernetes ClusterIP. External access is exclusively vi │ 1. POST /ingest/policy/{text|file|url} ▼ RAG Ingest Service - │ 2. upsert documents ──► ChromaDB - │ 3. publish aiac.apply.policy.build + │ 2. verify document (pre-flight, all-or-nothing) ──► Policy Guardrails Agent + │ 3. upsert documents ──► ChromaDB + │ 4. publish aiac.apply.policy.build ▼ NATS JetStream - │ 4. deliver event + │ 5. deliver event ▼ AIAC Agent - │ 5. GET /roles, /services, /assignments ──► IdP Configuration Service ──► Keycloak Admin REST - │ 6. retrieve full policy context ──► ChromaDB - │ 7. [LLM] compute list[PolicyRule] delta against current OPA state - │ 8. compute_and_apply(rules) ──► Policy Computation Engine + │ 6. GET /roles, /services, /assignments ──► IdP Configuration Service ──► Keycloak Admin REST + │ 7. retrieve full policy context ──► ChromaDB + │ 8. [LLM] compute list[PolicyRule] delta against current OPA state + │ 9. compute_and_apply(rules) ──► Policy Computation Engine │ ├── get_services_by_role / get_services_by_scope ──► IdP Configuration Service - │ ├── get_agent_policy / apply_agent_policy ──► Policy Store + │ ├── get_agent_policy / apply_agent_policy ──► Policy Model Store │ └── apply_policy ──► PDP Policy Writer ──► AuthorizationPolicy CR - │ 9. ACK message + │ 10. ACK message ▼ NATS JetStream (message removed from pending) ``` @@ -255,13 +262,13 @@ All inter-pod traffic is Kubernetes ClusterIP. External access is exclusively vi ▼ AIAC Agent │ 2. DELETE /policy (clear all OPA policy rules) ──► PDP Policy Writer ──► AuthorizationPolicy CR - │ 3. DELETE /policy (clear Policy Store) ──► Policy Store + │ 3. DELETE /policy (clear Policy Model Store) ──► Policy Model Store │ 4. GET /roles, /services (read fresh entity state) ──► IdP Configuration Service ──► Keycloak Admin REST │ 5. retrieve full policy context ──► ChromaDB │ 6. [LLM] compute complete list[PolicyRule] from scratch │ 7. compute_and_apply(rules) ──► Policy Computation Engine │ ├── get_services_by_role / get_services_by_scope ──► IdP Configuration Service - │ ├── get_agent_policy / apply_agent_policy ──► Policy Store + │ ├── get_agent_policy / apply_agent_policy ──► Policy Model Store │ └── apply_policy ──► PDP Policy Writer ──► AuthorizationPolicy CR ▼ (synchronous HTTP response to operator) @@ -271,41 +278,45 @@ All inter-pod traffic is Kubernetes ClusterIP. External access is exclusively vi | Component | Called by | Calls | Returns | |-----------|-----------|-------|---------| -| IdP Configuration Service (in Kagenti Interface Pod) | `aiac.idp.configuration.api` | Keycloak Admin REST API | Raw Keycloak JSON (generic endpoint names) | -| PDP Policy Writer — OPA (in Kagenti Interface Pod) | `aiac.pdp.policy.library` | Kubernetes CR (`AuthorizationPolicy`) | 204 on success | -| Policy Store (StatefulSet `aiac-policy-store`) | `aiac.policy.store.library` | SQLite (`agent_policies` table, in-memory cache) | `AgentPolicyModel` / `PolicyModel` on read; 204 on write | -| Policy Computation Engine (`aiac.policy.computation`) | AIAC Agent sub-UC agents | `aiac.idp.configuration.api`, `aiac.policy.store.library`, `aiac.pdp.policy.library` | `None` on success; exceptions logged and re-raised (propagate to the caller) | +| IdP Configuration Service (in Rossoctl Interface Pod) | `aiac.idp.configuration.api` | Keycloak Admin REST API | Raw Keycloak JSON (generic endpoint names) | +| PDP Policy Writer — OPA (in Rossoctl Interface Pod) | `aiac.pdp.policy.library` | Kubernetes CR (`AuthorizationPolicy`) | 204 on success | +| Policy Model Store (StatefulSet `aiac-policy-model-store`) | `aiac.policy.model_store.library` | SQLite (`agent_policies` table, in-memory cache) | `AgentPolicyModel` / `PolicyModel` on read; 204 on write | +| Policy Computation Engine (`aiac.policy.computation`) | AIAC Agent sub-UC agents | `aiac.idp.configuration.api`, `aiac.policy.model_store.library`, `aiac.pdp.policy.library` | `None` on success; exceptions logged and re-raised (propagate to the caller) | | `aiac.idp.configuration.models` | `aiac.idp.configuration.api`, `aiac.policy.model`, AIAC Agent | — | Pydantic model definitions for IdP entities (Subject, Role, Service, Scope) | | `aiac.idp.configuration.api` | AIAC Agent, Policy Computation Engine, Python scripts | IdP Configuration Service (HTTP) | Typed Pydantic instances (reads and writes IdP configuration entities) | -| `aiac.policy.model` | `aiac.pdp.policy.library`, `aiac.policy.store.library`, `aiac.policy.computation`, AIAC Agent | — | Pydantic model definitions for policy entities (PolicyRule, AgentPolicyModel, PolicyModel) | +| `aiac.policy.model` | `aiac.pdp.policy.library`, `aiac.policy.model_store.library`, `aiac.policy.computation`, AIAC Agent | — | Pydantic model definitions for policy entities (PolicyRule, AgentPolicyModel, PolicyModel) | | `aiac.pdp.policy.library` | `aiac.policy.computation` | PDP Policy Writer — OPA (HTTP) | None (writes Rego policy rules to AuthorizationPolicy CR) | -| `aiac.policy.store.library` | `aiac.policy.computation` | Policy Store (HTTP) | `AgentPolicyModel` / `PolicyModel` on read; None on write/delete | -| ChromaDB | RAG Ingest Service (writes), AIAC Agent (reads) | — | Policy and domain knowledge vectors | -| RAG Ingest Service | Developer (via `kubectl port-forward`) | ChromaDB, Embedding API, Event Broker | — | -| Event Broker (NATS JetStream) | Keycloak SPI listener, RAG Ingest Service (publishers); NATS JetStream (DLQ routing) | — | Durable event delivery to AIAC Agent; DLQ on max retries | -| AIAC Agent | Event Broker (NATS consumer), operator (`/apply/policy/rebuild` HTTP direct) | Service Onboarding / Policy Update / Role Update orchestrators → `aiac.idp.configuration.api`, `aiac.policy.computation`, ChromaDB, LLM API, Kubernetes API | Rego policy written to AuthorizationPolicy CR; structured policy written to Policy Store (SQLite); provisioned service permissions/scopes (onboarding) | +| `aiac.policy.model_store.library` | `aiac.policy.computation` | Policy Model Store (HTTP) | `AgentPolicyModel` / `PolicyModel` on read; None on write/delete | +| ChromaDB | RAG Ingest Service (writes), Policy Guardrails Agent (reads, context), AIAC Agent (reads) | — | Policy and domain knowledge vectors | +| RAG Ingest Service | Developer (via `kubectl port-forward`) | ChromaDB, Policy Guardrails Agent, Embedding API, Event Broker | — | +| Policy Guardrails Agent (in RAG Pod) | RAG Ingest Service | ChromaDB (context reads) | Verdict per document — contract TBD | +| Event Broker (NATS JetStream) | Keycloak SPI listener, RAG Ingest Service (publishers); AIAC Agent consumer (DLQ routing) | — | Durable event delivery to AIAC Agent; DLQ on max retries | +| AIAC Agent | Event Broker (NATS consumer), operator (`/apply/policy/rebuild` HTTP direct) | Service Onboarding / Policy Update / Role Update orchestrators → `aiac.idp.configuration.api`, `aiac.policy.computation`, ChromaDB, LLM API, Kubernetes API | Rego policy written to AuthorizationPolicy CR; structured policy written to Policy Model Store (SQLite); provisioned service permissions/scopes (onboarding) | ### Key architectural decisions -- **Stateless PDP services are co-located in the Kagenti Interface Pod; the stateful Policy Store is separate.** IdP Configuration Service and PDP Policy Writer run as two containers in the Interface Pod, sharing a Kubernetes ServiceAccount. The Policy Store is a dedicated single-replica StatefulSet (`aiac-policy-store`) with its own PVC — decoupled from the Interface Pod's restart lifecycle. Three ClusterIP Services (`aiac-pdp-config-service:7071`, `aiac-pdp-policy-service:7072`, `aiac-policy-store-service:7074`) provide stable addressing. +- **Stateless PDP services are co-located in the Rossoctl Interface Pod; the stateful Policy Model Store is separate.** IdP Configuration Service and PDP Policy Writer run as two containers in the Interface Pod, sharing a Kubernetes ServiceAccount. The Policy Model Store is a dedicated single-replica StatefulSet (`aiac-policy-model-store`) with its own PVC — decoupled from the Interface Pod's restart lifecycle. Three ClusterIP Services (`aiac-pdp-config-service:7071`, `aiac-pdp-policy-service:7072`, `aiac-policy-model-store-service:7074`) provide stable addressing. - **Policy Computation Engine is a library, not a service.** `aiac.policy.computation` runs in-process within the AIAC Agent pod. It requires no Kubernetes deployment, no container image, and no ClusterIP Service. Sub-agents call `compute_and_apply(rules)` directly. -- **One CR + one SQLite store, distinct owners, distinct purposes.** The Policy Store owns a SQLite `agent_policies` table (backed by a 1 Gi RWO PVC) holding structured `AgentPolicyModel` data — the source of truth for policy state, served from an in-memory cache. The `AuthorizationPolicy` CR (one total, owned by the PDP Policy Writer) holds derived Rego packages for OPA runtime. The two services have no dependency on each other; both are driven by the PCE via their respective libraries. -- **`aiac.pdp.policy.library` has one caller: `aiac.policy.computation`.** AIAC Agent sub-agents do not call the PDP Policy Library directly; they call `compute_and_apply()` instead. This centralises all Policy Store ↔ PDP Policy Writer coordination. +- **One CR + one SQLite store, distinct owners, distinct purposes.** The Policy Model Store owns a SQLite `agent_policies` table (backed by a 1 Gi RWO PVC) holding structured `AgentPolicyModel` data — the source of truth for policy state, served from an in-memory cache. The `AuthorizationPolicy` CR (one total, owned by the PDP Policy Writer) holds derived Rego packages for OPA runtime. The two services have no dependency on each other; both are driven by the PCE via their respective libraries. +- **`aiac.pdp.policy.library` has one caller: `aiac.policy.computation`.** AIAC Agent sub-agents do not call the PDP Policy Library directly; they call `compute_and_apply()` instead. This centralises all Policy Model Store ↔ PDP Policy Writer coordination. - **Clean `idp` / `pdp` / `policy` Python namespace split.** IdP-related code (Keycloak entity management) lives under `aiac.idp.*`; PDP policy code (OPA Rego writing) lives under `aiac.pdp.*`; shared policy model and computation code lives under `aiac.policy.*`. -- **`aiac.policy.model` is dependency-free (only `pydantic` + `aiac.idp.configuration.models`).** `PolicyRule`, `AgentPolicyModel`, and `PolicyModel` live in a neutral namespace importable by any consumer — Policy Store library, PDP Policy Library, PCE — without forcing a dependency on any service namespace. +- **`aiac.policy.model` is dependency-free (only `pydantic` + `aiac.idp.configuration.models`).** `PolicyRule`, `AgentPolicyModel`, and `PolicyModel` live in a neutral namespace importable by any consumer — Policy Model Store library, PDP Policy Library, PCE — without forcing a dependency on any service namespace. - **`PolicyRule.role` and `PolicyRule.scope` are typed objects.** They hold `Role` and `Scope` instances from `aiac.idp.configuration.models`, enabling the PCE to call `Configuration.get_services_by_role` and `Configuration.get_services_by_scope` without additional type conversion. - **`AgentPolicyModel` relationship maps are keyed by string `id`.** `source_roles`, `subject_roles`, and `target_scopes` use the entity's string `id` as the dict key, so `Service`, `Role`, `Scope`, and `Subject` need no custom hash/eq and keep pydantic's default field-based equality. This also lets the maps serialize to JSON without a custom key serializer. - **PCE merge semantics are additive, with drift-GC and an authoritative offboard.** The default merge (`override=False`) is additive — new rules are appended to a service's SPM `inbound_rules` (dedup by `role.id + scope.id`); existing edges are preserved. Two mechanisms remove edges: (1) **reconcile drift-GC** prunes each *touched* SPM against the `get_services()` catalog on every write, dropping edges whose scope or agent-role no longer exists and collapsing churned/duplicate user-role generations (order-independent; skipped on a catalog miss so a transient outage never wipes an SPM); and (2) **`decommission(service_id)`** — the authoritative service **offboard** — deletes a decommissioned service's SPM, purges its outbound footprint from other SPMs, deletes its APM/Rego if it was an agent, and re-derives every affected agent (keyed by clientId, since an offboarded client is gone from `get_services()`). Fine-grained **single-rule** revocation is still TBD; `override=True` gives role-level replace. - **PDP services bind to `0.0.0.0`.** Exposed as Kubernetes ClusterIP Services so that the Agent Pod can reach them over the cluster network. - **RBAC via OPA Rego rules.** AIAC manages role → service permission mappings by writing `AgentPolicyModel` instances to the `AuthorizationPolicy` CR. Each agent pod's OPA plugin fetches its packages from the CR at startup. - **RAG Pod is a StatefulSet with persistent ChromaDB storage.** ChromaDB data is stored on a 1 Gi `ReadWriteOnce` PersistentVolumeClaim mounted at `/chroma/chroma` (ChromaDB default). On pod recreation, the StatefulSet rebinds the same PVC and ChromaDB resumes from persisted state without re-ingestion. The pod runs a single replica. -- **RAG Pod runs ChromaDB and RAG Ingest Service together.** Exposed as `aiac-rag-service` on ports 8000 (ChromaDB default) and 7073 (RAG Ingest Service). +- **RAG Pod runs ChromaDB, RAG Ingest Service, and the Policy Guardrails Agent together.** Exposed as `aiac-rag-service` on ports 8000 (ChromaDB default) and 7073 (RAG Ingest Service). +- **The Policy Guardrails Agent is not exposed on the RAG Pod's ClusterIP Service.** It is reachable only on the pod's loopback network (`localhost:7075`), making the RAG Ingest Service structurally the only caller. +- **Guardrails verification is a synchronous, per-document, pre-flight, fail-closed gate.** The RAG Ingest Service calls the Policy Guardrails Agent once per document before making any ChromaDB mutation; any rejection fails the whole request with nothing written, and an unreachable or erroring agent is treated the same as a rejection unless verification is explicitly disabled via `AIAC_GUARDRAILS_ENABLED`. +- **The Policy Guardrails Agent has no Event Broker involvement.** It neither publishes nor consumes NATS subjects; the RAG Ingest Service's existing `aiac.apply.policy.build` publish is unchanged. - **AIAC Agent is stateless.** Changes are applied immediately on trigger — no pending session or human confirmation step. - **Event Broker decouples all automated triggers from the Agent.** The Keycloak SPI listener and RAG Ingest Service publish to NATS subjects; the Agent subscribes as a durable competing consumer. This removes all direct dependencies between trigger sources and the Agent. - **`rebuild` bypasses the Event Broker.** It is an operator-only command issued directly via HTTP (`kubectl port-forward`). It is never published to NATS and has no NATS listener. - **NATS consumer is a thin adapter.** It receives events from the Event Broker and calls the same internal handler functions used by the debug HTTP endpoints. No business logic lives in the consumer. - **Agent HTTP endpoints are retained for debugging.** They are not the primary trigger path; the NATS consumer is. `kubectl port-forward` to the Agent is used only for `rebuild` and debugging. -- **Event Broker uses WorkQueuePolicy.** Messages are removed from the stream after acknowledgement. Unacknowledged messages survive Agent pod restarts and are redelivered automatically. After 5 failed deliveries, messages are routed to `aiac.apply.dlq`. +- **Event Broker uses WorkQueuePolicy.** Messages are removed from the stream after acknowledgement. Unacknowledged messages survive Agent pod restarts and are redelivered automatically. After 5 failed deliveries, the consumer republishes them to `aiac.apply.dlq`. - **AIAC init container gates Agent startup.** Before the Agent container starts, the init container waits for NATS, IdP Configuration Service, PDP Policy Writer, and RAG Ingest Service to be healthy, then creates the `aiac-events` JetStream stream idempotently. _(Deferred to Phase 2 — issue 4.21; the Phase 1 Agent pod runs without it.)_ - **All `__init__.py` files under `aiac.*` are empty.** Callers use explicit submodule paths: `from aiac.idp.configuration.models import Subject`, `from aiac.policy.model.models import PolicyModel`. - **ChromaDB hosts two collections: `aiac-policies` and `aiac-domain-knowledge`.** Collection slug to ChromaDB name mapping: `policy` → `aiac-policies`, `domain-knowledge` → `aiac-domain-knowledge`. @@ -313,11 +324,11 @@ All inter-pod traffic is Kubernetes ClusterIP. External access is exclusively vi --- -## 6. Kagenti / Keycloak / OPA Interfaces +## 6. Rossoctl / Keycloak / OPA Interfaces -**AIAC ↔ Kagenti platform** +**AIAC ↔ Rossoctl platform** The AIAC Agent reads `AgentRuntime` and `AgentCard` custom resources from the Kubernetes API to -extract service metadata during UC-1 service onboarding. The `aiac.idp.configuration` and `aiac.pdp.policy.library` Python packages are the integration surface for other Kagenti components needing typed access to the IdP and PDP respectively. +extract service metadata during UC-1 service onboarding. The `aiac.idp.configuration` and `aiac.pdp.policy.library` Python packages are the integration surface for other Rossoctl components needing typed access to the IdP and PDP respectively. **AIAC ↔ Keycloak** The IdP Configuration Service proxies Keycloak Admin REST endpoints under generic entity names (subjects, roles, services, scopes, assignments). Read endpoints include per-service role and scope enrichment. The Keycloak SPI listener publishes entity lifecycle events to NATS; it is a separate component outside the AIAC codebase. @@ -327,7 +338,7 @@ The PDP Policy Writer (`aiac-pdp-policy-opa`) writes LLM-generated Rego packages **AIAC ↔ Event Broker (NATS JetStream)** The Agent subscribes to the event stream as a durable consumer with at-least-once delivery. -Unacknowledged messages survive pod restarts; failed messages are routed to a dead-letter subject. +Unacknowledged messages survive pod restarts; after the final failed delivery, the consumer republishes the message to a dead-letter subject. See Section 7.5 (Event Broker) and Section 8 (Deployment) for subject names and handler mapping. --- @@ -336,7 +347,7 @@ See Section 7.5 (Event Broker) and Section 8 (Deployment) for subject names and ### 7.1 IdP Configuration Service -FastAPI service (`0.0.0.0:7071`) co-located with the PDP Policy Writer in the **Kagenti Interface Pod**. Manages IdP (Keycloak) entity data (subjects, roles, services, scopes) via Keycloak Admin REST API. Exposes read and write endpoints for configuration entities. Stateless. All endpoints except `/health` require a `?realm=` query parameter; returns `422` if absent. `/health` requires no realm parameter — it uses `KEYCLOAK_ADMIN_REALM` directly. `KeycloakAdmin` instances are created lazily per realm and cached in a thread-safe map; the admin always authenticates via the realm in `KEYCLOAK_ADMIN_REALM`. +FastAPI service (`0.0.0.0:7071`) co-located with the PDP Policy Writer in the **Rossoctl Interface Pod**. Manages IdP (Keycloak) entity data (subjects, roles, services, scopes) via Keycloak Admin REST API. Exposes read and write endpoints for configuration entities. Stateless. All endpoints except `/health` require a `?realm=` query parameter; returns `422` if absent. `/health` requires no realm parameter — it uses `KEYCLOAK_ADMIN_REALM` directly. `KeycloakAdmin` instances are created lazily per realm and cached in a thread-safe map; the admin always authenticates via the realm in `KEYCLOAK_ADMIN_REALM`. **Full spec:** [components/idp-configuration-service.md](components/idp-configuration-service.md) @@ -344,17 +355,17 @@ FastAPI service (`0.0.0.0:7071`) co-located with the PDP Policy Writer in the ** ### 7.2 PDP Policy Writer -FastAPI service (`0.0.0.0:7072`, `aiac-pdp-policy-opa`) co-located with the IdP Configuration Service in the **Kagenti Interface Pod**. Writes LLM-generated Rego packages to an `AuthorizationPolicy` Kubernetes CR. Each AuthBridge OPA plugin instance fetches its Rego packages from the CR at startup. +FastAPI service (`0.0.0.0:7072`, `aiac-pdp-policy-opa`) co-located with the IdP Configuration Service in the **Rossoctl Interface Pod**. Writes LLM-generated Rego packages to an `AuthorizationPolicy` Kubernetes CR. Each AuthBridge OPA plugin instance fetches its Rego packages from the CR at startup. **Full spec:** [components/pdp-policy-writer-opa.md](components/pdp-policy-writer-opa.md) --- -### 7.3 Policy Store +### 7.3 Policy Model Store -FastAPI service (`0.0.0.0:7074`, `aiac-policy-store-service`) deployed as a dedicated single-replica StatefulSet (`aiac-policy-store`) with a `volumeClaimTemplate` PVC (1 Gi, `ReadWriteOnce`) mounted at `/data`. Owns an in-memory `PolicyModel` cache backed by a SQLite database (`/data/state.db`) as the authoritative structured policy store. All GET requests are served from the in-memory cache; mutations write through to SQLite synchronously; on pod restart the cache is repopulated from SQLite. The Policy Computation Engine reads current `AgentPolicyModel` state for additive merging and writes updated state after each computation. The PDP Policy Writer has no dependency on the Policy Store; the SQLite store and `AuthorizationPolicy` CR are written by distinct services and serve distinct purposes. +FastAPI service (`0.0.0.0:7074`, `aiac-policy-model-store-service`) deployed as a dedicated single-replica StatefulSet (`aiac-policy-model-store`) with a `volumeClaimTemplate` PVC (1 Gi, `ReadWriteOnce`) mounted at `/data`. Owns an in-memory `PolicyModel` cache backed by a SQLite database (`/data/policy_model.db`) as the authoritative structured policy store. All GET requests are served from the in-memory cache; mutations write through to SQLite synchronously; on pod restart the cache is repopulated from SQLite. The Policy Computation Engine reads current `AgentPolicyModel` state for additive merging and writes updated state after each computation. The PDP Policy Writer has no dependency on the Policy Model Store; the SQLite store and `AuthorizationPolicy` CR are written by distinct services and serve distinct purposes. -**Full spec:** [components/policy-store.md](components/policy-store.md) +**Full spec:** [components/policy-model-store.md](components/policy-model-store.md) --- @@ -362,7 +373,7 @@ FastAPI service (`0.0.0.0:7074`, `aiac-policy-store-service`) deployed as a dedi Pure Python library module (`aiac.policy.computation`). No FastAPI, no Kubernetes deployment, no container image. Runs in-process within the AIAC Agent pod. AIAC Agent sub-UC agents call `compute_and_apply(rules: list[PolicyRule]) -> None` to translate partial policy rule lists into merged `AgentPolicyModel` objects and push them to OPA. -The PCE is the **single point of coordination** between the Policy Store and PDP Policy Writer: it reads current agent policy state, additively merges new rules, writes back to the Policy Store, then pushes the updated `PolicyModel` to `aiac.pdp.policy.library.apply_policy()`. All exceptions from any dependency (IdP, Policy Store, PDP) are logged and **re-raised** so the caller (the Controller / HTTP layer) surfaces the failure — e.g. as a 500 — instead of returning success while silently applying nothing. +The PCE is the **single point of coordination** between the Policy Model Store and PDP Policy Writer: it reads current agent policy state, additively merges new rules, writes back to the Policy Model Store, then pushes the updated `PolicyModel` to `aiac.pdp.policy.library.apply_policy()`. All exceptions from any dependency (IdP, Policy Model Store, PDP) are logged and **re-raised** so the caller (the Controller / HTTP layer) surfaces the failure — e.g. as a 500 — instead of returning success while silently applying nothing. **Full spec:** [components/policy-computation-engine.md](components/policy-computation-engine.md) @@ -379,20 +390,20 @@ Python package at `aiac/src/`. Clean `idp` / `pdp` / `policy` namespace split: **Policy model** (shared, dependency-light): - **`aiac.policy.model`** — canonical Pydantic models for policy entities (`PolicyRule`, `AgentPolicyModel`, `PolicyModel`) with typed `Role`/`Scope`/`Service` fields. Importable by any consumer without pulling in HTTP or service dependencies. -**Policy libraries** (OPA + Policy Store access): +**Policy libraries** (OPA + Policy Model Store access): - **`aiac.pdp.policy.library`** — HTTP client wrapping the PDP Policy Writer (OPA). Four module-level functions: `apply_policy`, `apply_agent_policy`, `delete_agent_policy`, `delete_policy`. Called exclusively by `aiac.policy.computation`. -- **`aiac.policy.store.library`** — HTTP client wrapping the Policy Store. Six module-level functions: `get_policy`, `get_agent_policy`, `apply_policy`, `apply_agent_policy`, `delete_agent_policy`, `delete_policy`. Returns `PolicyModel` and `AgentPolicyModel` directly. Called exclusively by `aiac.policy.computation`. +- **`aiac.policy.model_store.library`** — HTTP client wrapping the Policy Model Store. Six module-level functions: `get_policy`, `get_agent_policy`, `apply_policy`, `apply_agent_policy`, `delete_agent_policy`, `delete_policy`. Returns `PolicyModel` and `AgentPolicyModel` directly. Called exclusively by `aiac.policy.computation`. **Computation library** (policy rule processing): -- **`aiac.policy.computation`** — library module implementing `compute_and_apply(rules: list[PolicyRule]) -> None`. Orchestrates IdP resolution, Policy Store merge, and PDP Policy Writer push. +- **`aiac.policy.computation`** — library module implementing `compute_and_apply(rules: list[PolicyRule]) -> None`. Orchestrates IdP resolution, Policy Model Store merge, and PDP Policy Writer push. -**Full specs:** [components/library-idp.md](components/library-idp.md) · [components/library-pdp-policy.md](components/library-pdp-policy.md) · [components/library-policy-store.md](components/library-policy-store.md) · [components/policy-model.md](components/policy-model.md) · [components/policy-computation-engine.md](components/policy-computation-engine.md) +**Full specs:** [components/library-idp.md](components/library-idp.md) · [components/library-pdp-policy.md](components/library-pdp-policy.md) · [components/library-policy-model-store.md](components/library-policy-model-store.md) · [components/policy-model.md](components/policy-model.md) · [components/policy-computation-engine.md](components/policy-computation-engine.md) --- ### 7.6 Event Broker -NATS JetStream pod (`aiac-event-broker-service:4222`). Decouples event producers (Keycloak SPI listener, RAG Ingest Service) from the AIAC Agent. Provides at-least-once delivery, replay on pod restart via `WorkQueuePolicy`, and a dead-letter subject (`aiac.apply.dlq`) after 5 failed deliveries. No authentication — ClusterIP network isolation is the access control mechanism. Stream: `aiac-events`, subjects `aiac.apply.>`, consumer group `aiac-agent-consumer`. +NATS JetStream pod (`aiac-event-broker-service:4222`). Decouples event producers (Keycloak SPI listener, RAG Ingest Service) from the AIAC Agent. Provides at-least-once delivery, replay on pod restart via `WorkQueuePolicy`, and a dead-letter subject (`aiac.apply.dlq`) that the consumer republishes to after 5 failed deliveries. No authentication — ClusterIP network isolation is the access control mechanism. Stream: `aiac-events`, subjects `aiac.apply.>`, consumer group `aiac-agent-consumer`. **Full spec:** [components/event-broker.md](components/event-broker.md) @@ -400,7 +411,7 @@ NATS JetStream pod (`aiac-event-broker-service:4222`). Decouples event producers ### 7.7 AIAC Agent -FastAPI + LangGraph service (`0.0.0.0:7070`). Receives automated triggers via the **Event Broker** (NATS JetStream durable consumer, `aiac-agent-consumer` queue group) and the operator-only `rebuild` command directly via HTTP. Structured as a thin **Controller** (`controller/routes.py`) that dispatches `/apply/*` handlers to three **Orchestrators**, each owning one or more compiled `StateGraph` sub-agents. A **NATS consumer** (asyncio background task in the FastAPI `lifespan` handler) is a thin adapter that receives NATS events and calls the same internal handler functions used by the HTTP endpoints: +FastAPI + LangGraph service (`0.0.0.0:7070`). Receives automated triggers via the **Event Broker** (NATS JetStream durable consumer, `aiac-agent-consumer` queue group) and the operator-only `rebuild` command directly via HTTP. Structured as a thin **Controller** (`controller/routes.py`) that dispatches `/apply/*` handlers to three **Orchestrators**, each owning one or more compiled `StateGraph` sub-agents. A **NATS consumer** (asyncio background task in the FastAPI `lifespan` handler, implemented in `eventbus/consumer.py` with shared config in `eventbus/stream.py`) is a thin adapter that receives NATS events and calls the same internal handler functions used by the HTTP endpoints: | Orchestrator | Trigger(s) | Sub-agents | |---|---|---| @@ -408,7 +419,7 @@ FastAPI + LangGraph service (`0.0.0.0:7070`). Receives automated triggers via th | Policy Update | `aiac.apply.policy.build`, `/apply/policy/rebuild` (HTTP) | Build sub-agent or Rebuild sub-agent (alternative) | | Role Update | `aiac.apply.role.{id}` | Role sub-agent | -All sub-agent `StateGraph` instances are logically separated modules running within a single pod and process. Sub-UC agents produce `list[PolicyRule]` and call `compute_and_apply(rules)` — they do not call `aiac.policy.store.library` or `aiac.pdp.policy.library` directly. The **Policy Update** sub-agents compute a minimal rule delta between the current ChromaDB policy and live OPA state. The **Rebuild** variant additionally clears the Policy Store and all OPA policy rules before recomputing. The **Role Update** orchestrator computes rules for all services affected by the role change. The **Service Onboarding** orchestrator classifies the new service via the pod's `kagenti.io/type` label (for agents reads the `AgentCard` CR; for tools calls `tools/list` on the MCP endpoint discovered via K8s Service label lookup), then computes rules and calls `compute_and_apply`. Stateless; changes are applied immediately. Integrated retry with differentiated error codes per upstream. +All sub-agent `StateGraph` instances are logically separated modules running within a single pod and process. Sub-UC agents produce `list[PolicyRule]` and call `compute_and_apply(rules)` — they do not call `aiac.policy.model_store.library` or `aiac.pdp.policy.library` directly. The **Policy Update** sub-agents compute a minimal rule delta between the current ChromaDB policy and live OPA state. The **Rebuild** variant additionally clears the Policy Model Store and all OPA policy rules before recomputing. The **Role Update** orchestrator computes rules for all services affected by the role change. The **Service Onboarding** orchestrator classifies the new service via the pod's `rossoctl.io/type` label (for agents reads the `AgentCard` CR; for tools calls `tools/list` on the MCP endpoint discovered via K8s Service label lookup), then computes rules and calls `compute_and_apply`. Stateless; changes are applied immediately. Integrated retry with differentiated error codes per upstream. **Full spec:** [components/aiac-agent.md](components/aiac-agent.md) @@ -430,7 +441,15 @@ FastAPI service (`0.0.0.0:7073`) co-located with ChromaDB. Thirteen collection-p --- -### 7.10 Keycloak SPI Listener +### 7.10 Policy Guardrails Agent + +FastAPI service (`0.0.0.0:7075`) co-located with ChromaDB and the RAG Ingest Service in the **RAG Pod**. Verifies each document before the RAG Ingest Service writes it to ChromaDB — one verification call per document, pre-flight (before any ChromaDB mutation), all-or-nothing (any rejection fails the whole ingest request, nothing is written), fail-closed (an unreachable or erroring agent is treated as a rejection unless verification is disabled via `AIAC_GUARDRAILS_ENABLED`). Reachable only on the RAG Pod's loopback network — not exposed on `aiac-rag-service`, so the RAG Ingest Service is structurally the only caller. May read ChromaDB for evaluation context. Neither publishes nor consumes Event Broker subjects. One service exposing two independently-developed API families (`policy`, `domain-knowledge`) selected by collection slug. The `policy` family is defined — LangGraph agent running **policy hygiene** (on-topic/well-formed, actionable/translatable, internally consistent) and **contradiction against the persistent corpus** (`update` only; `replace` gets hygiene only), returning a two-level-severity verdict; the `domain-knowledge` family is specced independently later. + +**Full spec:** [components/policy-guardrails-agent.md](components/policy-guardrails-agent.md) + +--- + +### 7.11 Keycloak SPI Listener A custom Keycloak Event Listener SPI (Java) that listens to Keycloak's internal event bus and translates entity-scoped events into NATS publish calls to the Event Broker. The AIAC Agent subject schema is authoritative; the SPI PRD references it. @@ -440,7 +459,7 @@ A custom Keycloak Event Listener SPI (Java) that listens to Keycloak's internal | `CLIENT_CREATED` | `aiac.apply.service.{id}` | | Role created/updated | `aiac.apply.role.{id}` | -**Full spec:** TBD (separate PRD). +**Full spec:** [components/keycloak-spi-listener.md](components/keycloak-spi-listener.md) --- @@ -452,36 +471,36 @@ Four separate manifest files: | File | Contents | |------|----------| -| `aiac/k8s/pdp-interface-deployment.yaml` | `aiac-pdp-config` ConfigMap + Kagenti Interface Pod Deployment (IdP Configuration Service container + PDP Policy Writer container) + two ClusterIP Services (`aiac-pdp-config-service:7071`, `aiac-pdp-policy-service:7072`) | -| `aiac/k8s/policy-store-statefulset.yaml` | `aiac-policy-store` StatefulSet (Policy Store container) + `volumeClaimTemplate` (1 Gi, `ReadWriteOnce`, mounted at `/data`) + headless Service + `aiac-policy-store-service:7074` ClusterIP Service | +| `aiac/k8s/pdp-interface-deployment.yaml` | `aiac-pdp-config` ConfigMap + Rossoctl Interface Pod Deployment (IdP Configuration Service container + PDP Policy Writer container) + two ClusterIP Services (`aiac-pdp-config-service:7071`, `aiac-pdp-policy-service:7072`) | +| `aiac/k8s/policy-model-store-statefulset.yaml` | `aiac-policy-model-store` StatefulSet (Policy Model Store container) + `volumeClaimTemplate` (1 Gi, `ReadWriteOnce`, mounted at `/data`) + headless Service + `aiac-policy-model-store-service:7074` ClusterIP Service | | `aiac/k8s/agent-deployment.yaml` | Agent Pod Deployment (AIAC Agent container) + ClusterIP Service _(Phase 1; `aiac-init` init container added in Phase 2, issue 4.21)_ | | `aiac/k8s/event-broker-deployment.yaml` _(pending)_ | Event Broker Pod Deployment (NATS JetStream) + ClusterIP Service | -| `aiac/k8s/rag-statefulset.yaml` _(pending)_ | RAG StatefulSet (ChromaDB + RAG Ingest Service containers) + 1 Gi PVC template + ClusterIP Service | +| `aiac/k8s/rag-statefulset.yaml` _(pending)_ | RAG StatefulSet (ChromaDB + RAG Ingest Service + Policy Guardrails Agent containers) + 1 Gi PVC template + ClusterIP Service (ChromaDB + RAG Ingest Service ports only — the Policy Guardrails Agent is pod-local, not on the ClusterIP Service) | -Both Interface Pod containers mount `aiac-pdp-config` (KEYCLOAK_URL, KEYCLOAK_REALM, KEYCLOAK_ADMIN_REALM) as env vars; only the IdP Configuration Service container also mounts `keycloak-admin-secret` (KEYCLOAK_ADMIN_USERNAME, KEYCLOAK_ADMIN_PASSWORD) and uses `KEYCLOAK_ADMIN_REALM` (ignoring `KEYCLOAK_REALM`). The PDP Policy Writer (`aiac-pdp-policy-opa`, the Phase 1 rego-file mock) needs no Keycloak credentials — it writes `.rego` files to `REGO_OUTPUT_DIR` (default `/rego`, an `emptyDir` volume). The Policy Store container mounts `aiac-policy-store-config` for `AGENTPOLICY_DB_PATH` (default `/data/state.db`) — no Kubernetes API access or RBAC required. +Both Interface Pod containers mount `aiac-pdp-config` (KEYCLOAK_URL, KEYCLOAK_REALM, KEYCLOAK_ADMIN_REALM) as env vars; only the IdP Configuration Service container also mounts `keycloak-admin-secret` (KEYCLOAK_ADMIN_USERNAME, KEYCLOAK_ADMIN_PASSWORD) and uses `KEYCLOAK_ADMIN_REALM` (ignoring `KEYCLOAK_REALM`). The PDP Policy Writer (`aiac-pdp-policy-opa`, the Phase 1 rego-file mock) needs no Keycloak credentials — it writes `.rego` files to `REGO_OUTPUT_DIR` (default `/rego`, an `emptyDir` volume). The Policy Model Store container mounts `aiac-policy-model-store-config` for `SERVICEPOLICY_DB_PATH` (default `/data/policy_model.db`) — no Kubernetes API access or RBAC required. ### Docker images Built independently. No entry in the repo's `build.yaml` CI matrix. ```bash -# Build IdP Configuration Service (Kagenti Interface Pod container 1) +# Build IdP Configuration Service (Rossoctl Interface Pod container 1) docker build -f aiac/src/aiac/idp/service/configuration/keycloak/Dockerfile -t aiac-pdp-config:latest aiac/src/ -# Build PDP Policy Writer — Phase 1 OPA rego-file mock (Kagenti Interface Pod container 2; writes .rego to filesystem) +# Build PDP Policy Writer — Phase 1 OPA rego-file mock (Rossoctl Interface Pod container 2; writes .rego to filesystem) docker build -f aiac/src/aiac/pdp/service/policy/opa/Dockerfile -t aiac-pdp-policy-opa:latest aiac/src/ -# Legacy: PDP Policy Writer — Keycloak composite-role implementation, superseded, not deployed by any current manifest -docker build -f aiac/src/aiac/pdp/service/policy/keycloak/Dockerfile -t aiac-pdp-policy-keycloak:latest aiac/src/ - -# Build Policy Store (deployed as StatefulSet aiac-policy-store) -docker build -f aiac/src/aiac/policy/store/service/Dockerfile -t aiac-policy-store:latest aiac/src/ +# Build Policy Model Store (deployed as StatefulSet aiac-policy-model-store) +docker build -f aiac/src/aiac/policy/model_store/service/Dockerfile -t aiac-policy-model-store:latest aiac/src/ # Build Agent (aiac-init init container deferred to Phase 2, issue 4.21) docker build -f aiac/src/aiac/agent/controller/Dockerfile -t aiac-agent:latest aiac/src/ # Build RAG Ingest Service docker build -t aiac-rag-ingest:latest aiac/rag-ingest/ + +# Build Policy Guardrails Agent +docker build -t aiac-policy-guardrails:latest aiac/policy-guardrails/ ``` The Event Broker uses the official `nats` Docker image with JetStream enabled (`-js` flag). No custom build required. @@ -495,11 +514,11 @@ metadata: name: aiac-pdp-config data: KEYCLOAK_URL: "http://keycloak-service.keycloak.svc:8080" - KEYCLOAK_REALM: "kagenti" + KEYCLOAK_REALM: "rossoctl" KEYCLOAK_ADMIN_REALM: "master" AIAC_PDP_CONFIG_URL: "http://aiac-pdp-config-service:7071" AIAC_PDP_POLICY_URL: "http://aiac-pdp-policy-service:7072" - AIAC_POLICY_STORE_URL: "http://aiac-policy-store-service:7074" + AIAC_POLICY_MODEL_STORE_URL: "http://aiac-policy-model-store-service:7074" # Added in Phase 2 by issue 4.19 (Event Broker): NATS_URL: "nats://aiac-event-broker-service:4222" # Added in Phase 3 by issue 4.20 (RAG Pod): @@ -507,17 +526,17 @@ data: AIAC_CHROMADB_URL: "http://aiac-rag-service:8000" ``` -`AGENTPOLICY_DB_PATH` is absent — it belongs to `aiac-policy-store-config` (defined in `policy-store-statefulset.yaml`), not to the shared ConfigMap. +`SERVICEPOLICY_DB_PATH` is absent — it belongs to `aiac-policy-model-store-config` (defined in `policy-model-store-statefulset.yaml`), not to the shared ConfigMap. Likewise, `AIAC_GUARDRAILS_URL` and `AIAC_GUARDRAILS_ENABLED` (RAG Ingest Service → Policy Guardrails Agent, both pod-local) belong to the RAG Pod's own ConfigMap, not the shared `aiac-pdp-config` — no component outside the RAG Pod calls the Policy Guardrails Agent. -### `aiac-policy-store-config` ConfigMap template +### `aiac-policy-model-store-config` ConfigMap template ```yaml apiVersion: v1 kind: ConfigMap metadata: - name: aiac-policy-store-config + name: aiac-policy-model-store-config data: - AGENTPOLICY_DB_PATH: "/data/state.db" + SERVICEPOLICY_DB_PATH: "/data/policy_model.db" ``` Update `KEYCLOAK_URL` and `KEYCLOAK_REALM` for the target environment before applying. @@ -534,15 +553,16 @@ Tests live in `aiac/test/`. |--------|-------------|----------------| | IdP Configuration Service endpoints | `KeycloakAdmin` methods (return fixture dicts) | Correct JSON response, 502 on Keycloak error | | PDP Policy Writer (OPA) endpoints | Kubernetes CR write (`AuthorizationPolicy`) | 204 on success, 502 on CR write error | -| Policy Store endpoints | SQLite `:memory:` database | Correct read/write/delete; 404 on missing agent; 502 on SQLite write error; 503 on SQLite open/query failure at `/health` | -| `aiac.policy.store.library` functions | Policy Store HTTP endpoints | Correct method + path per function; returns typed model on read; `RuntimeError` on non-2xx; default URL fallback | +| Policy Model Store endpoints | SQLite `:memory:` database | Correct read/write/delete; 404 on missing agent; 502 on SQLite write error; 503 on SQLite open/query failure at `/health` | +| `aiac.policy.model_store.library` functions | Policy Model Store HTTP endpoints | Correct method + path per function; returns typed model on read; `RuntimeError` on non-2xx; default URL fallback | | `aiac.policy.model` | No mock needed | `extra='ignore'` drops unknown fields; relationship maps keyed by string `id` round-trip through `model_dump(mode="json")` / `model_validate` with typed `Role` / `Scope` values preserved | | `aiac.idp.configuration.api` functions | IdP Configuration Service HTTP endpoints | Returns correct Pydantic model instances; `RuntimeError` on non-2xx; default URL fallback; `get_services_by_role` and `get_services_by_scope` issue correct query params | | `aiac.pdp.policy.library` functions | PDP Policy Writer HTTP endpoints | Correct serialisation; `RuntimeError` on non-2xx; default URL fallback | -| `aiac.policy.computation` | `aiac.idp.configuration.api`, `aiac.policy.store.library`, `aiac.pdp.policy.library` (import-boundary mocks) | Correct `apply_agent_policy` calls per resolved service; additive merge preserves existing rules; no duplicate rule insertion; `apply_policy` called once after all writes; exceptions logged and re-raised (propagate to the caller) | +| `aiac.policy.computation` | `aiac.idp.configuration.api`, `aiac.policy.model_store.library`, `aiac.pdp.policy.library` (import-boundary mocks) | Correct `apply_agent_policy` calls per resolved service; additive merge preserves existing rules; no duplicate rule insertion; `apply_policy` called once after all writes; exceptions logged and re-raised (propagate to the caller) | | Event Broker NATS consumer | NATS message delivery (mock `nats-py` subscription) | Correct handler dispatched per subject; ack issued on success; no ack on handler exception | -| Event Broker DLQ | NATS max redelivery exceeded | Message routed to `aiac.apply.dlq` after 5 failures | +| Event Broker DLQ | consumer detects num_delivered == max_deliver | Message republished by the consumer to `aiac.apply.dlq` after 5 failures | | Init container health-check | HTTP 4xx then 200 sequence; NATS TCP refused then connected | Exits 0 only after all four dependencies healthy; `add_stream` called with correct config | +| Policy Guardrails Agent endpoints | ChromaDB (context reads) | TBD — pending the verification endpoint and verdict contract | | AIAC Agent | TBD | TBD | ### Integration tests @@ -585,8 +605,8 @@ Tracking issues: the live-Keycloak pytest integration tests in `testing/5.1-inte - Base Docker image: `python:3.12-slim` - Linting: ruff (line length 120, target py312 per root `pyproject.toml`) - Commits: DCO sign-off required (`git commit -s`); use `Assisted-By` not `Co-Authored-By` -- No auth on IdP Configuration Service, PDP Policy Writer, RAG Ingest Service, or Event Broker — network isolation (ClusterIP + `kubectl port-forward`) is the access control mechanism -- IdP Configuration Service, PDP Policy Writer, Agent, RAG Ingest Service, and Event Broker are not registered in the repo's `build.yaml` CI matrix; they have independent build processes +- No auth on IdP Configuration Service, PDP Policy Writer, RAG Ingest Service, Policy Guardrails Agent, or Event Broker — network isolation (ClusterIP + `kubectl port-forward`; the Policy Guardrails Agent additionally has no ClusterIP exposure at all) is the access control mechanism +- IdP Configuration Service, PDP Policy Writer, Agent, RAG Ingest Service, Policy Guardrails Agent, and Event Broker are not registered in the repo's `build.yaml` CI matrix; they have independent build processes - `aiac/__init__.py` exists and is empty — `aiac` is a regular package, not a namespace package - NATS consumer must **await** handler completion before issuing ack — fire-and-forget (`asyncio.create_task`) is prohibited; premature ack breaks at-least-once delivery guarantees - AIAC provisioning marker: every role and client scope AIAC provisions carries the Keycloak attribute `aiac.managed` = `true`, distinguishing AIAC-provisioned entities from Keycloak's built-ins (default client scopes, `default-roles-`). Realm-role attribute values are lists (`["true"]`), client-scope values are plain strings (`"true"`). The IdP Configuration Service stamps it on create and returns full role representations so it survives reads; the Policy Computation Engine filters on it (`Role.aiac_managed` / `Scope.aiac_managed`) when embedding each agent's own roles/scopes (P2) diff --git a/aiac/docs/specs/components/aiac-agent.md b/aiac/docs/specs/components/aiac-agent.md index 69f5d8547..4778a53b9 100644 --- a/aiac/docs/specs/components/aiac-agent.md +++ b/aiac/docs/specs/components/aiac-agent.md @@ -21,7 +21,7 @@ The service is structured as a **Controller** (FastAPI routes) that dispatches t | Policy Update (UC2) | Controller → sub-agent directly | Build or Rebuild (TBD) | `list[PolicyRule]` | | Role Update (UC3) | Controller → sub-agent directly | Role sub-agent | `list[PolicyRule]` | -Each producing sub-agent calls the **shared Policy Rules Builder** (`agent/policy_rules_builder/`) for each applicable (roles, scope) or (role, scopes) pair, merges the results, and returns a single `list[PolicyRule]` to the Controller. The Controller calls `compute_and_apply(merged_rules)` from `aiac.policy.computation` (PCE) once — no shared apply node exists. The PCE owns all Policy Store ↔ PDP Policy Writer coordination. Neither sub-agents nor the Policy Rules Builder call `aiac.pdp.policy.library` or `aiac.policy.store.library` directly. +Each producing sub-agent calls the **shared Policy Rules Builder** (`agent/policy_rules_builder/`) for each applicable (roles, scope) or (role, scopes) pair, merges the results, and returns a single `list[PolicyRule]` to the Controller. The Controller calls `compute_and_apply(merged_rules)` from `aiac.policy.computation` (PCE) once — no shared apply node exists. The PCE owns all Policy Model Store ↔ PDP Policy Writer coordination. Neither sub-agents nor the Policy Rules Builder call `aiac.pdp.policy.library` or `aiac.policy.model_store.library` directly. All components are **logically separated modules within a single pod and process** — no inter-service network calls between orchestrators and sub-agents. @@ -78,7 +78,7 @@ flowchart TD ## NATS Consumer -A thin adapter started as an **asyncio background task** in the FastAPI `lifespan` handler. It subscribes to the `aiac.apply.>` wildcard on the `aiac-events` NATS JetStream stream using the `aiac-agent-consumer` durable queue group. +A thin adapter started as an **asyncio background task** in the FastAPI `lifespan` handler. It subscribes to the `aiac.apply.>` wildcard on the `aiac-events` NATS JetStream stream using the `aiac-agent-consumer` durable queue group. Implemented in `eventbus/consumer.py` (dispatch + lifespan), with shared stream/consumer config in `eventbus/stream.py`. ### Dispatch table @@ -92,7 +92,7 @@ A thin adapter started as an **asyncio background task** in the FastAPI `lifespa ### Ack contract -The consumer **awaits** the internal handler before issuing the NATS acknowledgement. On handler success → ack. On handler exception → do not ack; NATS redelivers after `AckWait`. After 5 unacknowledged redeliveries, NATS routes the message to `aiac.apply.dlq`. +The consumer **awaits** the internal handler before issuing the NATS acknowledgement. On handler success → ack. On handler exception → do not ack; NATS redelivers after `AckWait`. After the 5th unacknowledged delivery, the consumer republishes the message to `aiac.apply.dlq` and terminates it. Fire-and-forget (`asyncio.create_task`) is explicitly prohibited — acking before handler completion would break at-least-once guarantees. @@ -145,15 +145,18 @@ Every sub-agent (UC1 Provision + Service Policy Builder, UC2 Build + Rebuild, UC | Method | Path | Orchestrator | Sub-agent | |---|---|---|---| +| GET | `/health` | — | — (liveness/readiness) | | POST | `/apply/policy/build` | Policy Update | Build | | POST | `/apply/policy/rebuild` | Policy Update | Rebuild | | POST | `/apply/role/{role_id}` | Role Update | Role | | POST | `/apply/service/{service_id}` | Service Onboarding | Provision | | POST | `/apply/offboard/{service_id}` | Service Offboarding | Offboard (calls PCE `decommission` directly) | +`GET /health` is a bare liveness/readiness probe: the Controller is stateless (no local state, no connection held at rest), so it answers `200 {"status": "ok"}` whenever the process is serving, dispatching to no handler and touching no upstream. Upstream reachability (IdP, PCE, NATS) is validated per-request by the handlers. The k8s Deployment wires both the readiness and liveness probes to it. + The `/apply/offboard/{service_id}` path uses the `{service_id:path}` converter (slash-bearing SPIFFE-URI clientIds) and is keyed on the **clientId (SPM key)**, not the Keycloak UUID that `/apply/service/{service_id}` carries — an offboarded client is gone from `get_services()`, so UUID→clientId resolution is impossible. -All endpoints return bare HTTP status codes: `200 OK` on success (no response body), and the status codes from the Error Handling table on upstream failure. Success responses carry no body; upstream failures are raised as FastAPI `HTTPException`s, so error responses carry FastAPI's default JSON error body (`{"detail": ...}`) alongside the status code. Summary, applied-rule details, and debug information are written to the service log. Validation failures surface as an error status and log entry; detailed reporting is specified in [policy-rules-builder.md](aiac-agent/policy-rules-builder.md). +The `/apply/*` endpoints return bare HTTP status codes: `200 OK` on success (no response body), and the status codes from the Error Handling table on upstream failure. Success responses carry no body; upstream failures are raised as FastAPI `HTTPException`s, so error responses carry FastAPI's default JSON error body (`{"detail": ...}`) alongside the status code. Summary, applied-rule details, and debug information are written to the service log. Validation failures surface as an error status and log entry; detailed reporting is specified in [policy-rules-builder.md](aiac-agent/policy-rules-builder.md). --- @@ -164,7 +167,7 @@ All endpoints return bare HTTP status codes: `200 OK` on success (no response bo | `NATS_URL` | `nats://aiac-event-broker-service:4222` | ConfigMap (`aiac-pdp-config`) | | `AIAC_PDP_CONFIG_URL` | `http://aiac-pdp-config-service:7071` | ConfigMap (`aiac-pdp-config`) — used by `aiac.idp.configuration.api` (in-process via PCE) | | `AIAC_PDP_POLICY_URL` | `http://aiac-pdp-policy-service:7072` | ConfigMap (`aiac-pdp-config`) — used by `aiac.pdp.policy.library` (in-process via PCE) | -| `AIAC_POLICY_STORE_URL` | `http://aiac-policy-store-service:7074` | ConfigMap (`aiac-pdp-config`) — used by `aiac.policy.store.library` (in-process via PCE) | +| `AIAC_POLICY_MODEL_STORE_URL` | `http://aiac-policy-model-store-service:7074` | ConfigMap (`aiac-pdp-config`) — used by `aiac.policy.model_store.library` (in-process via PCE) | | `AIAC_CHROMADB_URL` | `http://aiac-rag-service:8000` | ConfigMap (`aiac-pdp-config`) | | `KEYCLOAK_REALM` | — | ConfigMap (`aiac-pdp-config`) | | `LLM_BASE_URL` | — | ConfigMap | diff --git a/aiac/docs/specs/components/aiac-agent/policy-rules-builder.md b/aiac/docs/specs/components/aiac-agent/policy-rules-builder.md index 523c92a95..32e86c371 100644 --- a/aiac/docs/specs/components/aiac-agent/policy-rules-builder.md +++ b/aiac/docs/specs/components/aiac-agent/policy-rules-builder.md @@ -7,7 +7,7 @@ exposes two module-level functions that producing sub-agents call directly. Each internally runs a LangGraph `StateGraph`; callers are decoupled from LangGraph mechanics. The PRB fetches its own policy context (see **Policy source** below), reasons over it with an LLM, and emits `list[PolicyRule]` scoped to the input. It does **not** call -`aiac.pdp.policy.library` or `aiac.policy.store.library` directly; only the PCE does. +`aiac.pdp.policy.library` or `aiac.policy.model_store.library` directly; only the PCE does. --- diff --git a/aiac/docs/specs/components/aiac-agent/uc1-service-onboarding.md b/aiac/docs/specs/components/aiac-agent/uc1-service-onboarding.md index 0f1cfd903..9c0eda1ab 100644 --- a/aiac/docs/specs/components/aiac-agent/uc1-service-onboarding.md +++ b/aiac/docs/specs/components/aiac-agent/uc1-service-onboarding.md @@ -81,11 +81,11 @@ START → classify_service → [analyze_agent | analyze_tool] → provision_serv ### Nodes -- **`classify_service`**: resolves identity + determines service type from the operator's authoritative `kagenti.io/type` label (values `agent`/`tool`) — **not** from the `entity_id` format. +- **`classify_service`**: resolves identity + determines service type from the operator's authoritative `rossoctl.io/type` label (values `agent`/`tool`) — **not** from the `entity_id` format. 1. Store `service_id = trigger.entity_id` (the Keycloak **internal client UUID** — `Service.id` — **not** the `clientId`/`serviceId`). The `/apply/service/{service_id}` route and every downstream lookup (`get_service` → `admin.get_client`, and the builder's focus resolution) are keyed on this UUID because a `clientId` can be a slash-bearing SPIFFE URI that a single path segment cannot carry. - 2. Resolve identity: call `get_service(service_id)` from `aiac.idp.configuration.api` → `client.name`, which the kagenti-operator sets to `"{namespace}/{workload_name}"` for every workload (agents and tools, SPIRE-enabled or not). Split on the first `/` → store `namespace` and `workload_name`. `502` if `client.name` has no `/` (namespace unrecoverable). + 2. Resolve identity: call `get_service(service_id)` from `aiac.idp.configuration.api` → `client.name`, which the rossoctl-operator sets to `"{namespace}/{workload_name}"` for every workload (agents and tools, SPIRE-enabled or not). Split on the first `/` → store `namespace` and `workload_name`. `502` if `client.name` has no `/` (namespace unrecoverable). 3. LIST pods in `namespace`; select the pod owned by `workload_name` via `ownerReferences` (Deployment → ReplicaSet name prefix, or `StatefulSet`/`Sandbox` name match). `502` on Kubernetes API failure or no matching pod. - 4. Read the `kagenti.io/type` label on that pod and normalize it to a `ServiceType` + 4. Read the `rossoctl.io/type` label on that pod and normalize it to a `ServiceType` member via `ServiceType(label.capitalize())` — the label is lowercase (`agent`/`tool`); `ServiceType` values are capitalized (`Agent`/`Tool`): - `agent` → `ServiceType.AGENT`; route to `analyze_agent`. @@ -93,10 +93,10 @@ START → classify_service → [analyze_agent | analyze_tool] → provision_serv - Absent or any other value (normalization raises `ValueError`) → `502` (inconsistent deployment). > K8s access: `list` on `pods` in the target namespace (both paths). - > `kagenti.io/type` is authoritative — applied by the kagenti-operator (via the AgentRuntime CR) and propagated to pod labels; it is the operator's own agent/tool discriminator (`SkipReason`, kagenti-operator `internal/clientreg/names.go`). The operator only registers a Keycloak client for a workload that already carries this label, so it is effectively guaranteed for operator-registered clients; a missing/invalid value still fails loud (`502`, naming the workload + label). The service's `clientId` format (SPIFFE vs plain) reflects whether SPIRE is enabled, **not** the service type, so it is not used for classification (and is why `service_id`/`entity_id` is the slash-free internal UUID, not the clientId). + > `rossoctl.io/type` is authoritative — applied by the rossoctl-operator (via the AgentRuntime CR) and propagated to pod labels; it is the operator's own agent/tool discriminator (`SkipReason`, rossoctl-operator `internal/clientreg/names.go`). The operator only registers a Keycloak client for a workload that already carries this label, so it is effectively guaranteed for operator-registered clients; a missing/invalid value still fails loud (`502`, naming the workload + label). The service's `clientId` format (SPIFFE vs plain) reflects whether SPIRE is enabled, **not** the service type, so it is not used for classification (and is why `service_id`/`entity_id` is the slash-free internal UUID, not the clientId). - **`analyze_agent`**: non-LLM node; reads AgentCard CR. - 1. LIST `AgentCard` CRs (`agent.kagenti.dev/v1alpha1`) in `namespace`; find the one whose + 1. LIST `AgentCard` CRs (`agent.rossoctl.dev/v1alpha1`) in `namespace`; find the one whose `spec.targetRef.name` is `workload_name` (the operator names the CR after the Deployment, e.g. `{workload}-deployment-card`, **not** after the workload — so match by `targetRef`, falling back to `metadata.name == workload_name` for hand-authored cards). @@ -113,12 +113,12 @@ START → classify_service → [analyze_agent | analyze_tool] → provision_serv - `reasoning`: `"partial: no AgentCard found, default scope assigned"` (no CR) or `"partial: AgentCard has no synced skills, default scope assigned"` (CR present, unsynced). - > K8s access: `list` on `agentcards.agent.kagenti.dev` in the target namespace. + > K8s access: `list` on `agentcards.agent.rossoctl.dev` in the target namespace. - **`analyze_tool`**: non-LLM node; discovers MCP tools. `namespace` + `workload_name` are already resolved by `classify_service` (from the `client.name` split). MCP endpoint lookup uses the **hybrid Keycloak→K8s strategy** decided in issue [`6.2`](../../../issues/agent/service-onboarding/6.2-analyze-tool-lookup-strategy.md): the Keycloak client name supplied the key `{namespace, workload_name}`; K8s supplies the reachable endpoint. 1. Locate MCP endpoint: a. GET the K8s `Service` named `workload_name` in `namespace` (operator convention: Service name == workload name). - b. Require the `protocol.kagenti.io/mcp` label present on that Service; `502` (actionable) if absent — the label is applied at deploy time, not stamped by the operator. + b. Require the `protocol.rossoctl.io/mcp` label present on that Service; `502` (actionable) if absent — the label is applied at deploy time, not stamped by the operator. c. Build `http://{workload_name}.{namespace}.svc.cluster.local:{port}/mcp`, where `port` is the Service's first port (not hardcoded). 2. Mint a discovery token via `Configuration.mint_discovery_token(service_id)` — the tool's MCP endpoint sits behind its AuthBridge sidecar, whose inbound `jwt-validation` plugin enforces an @@ -134,12 +134,12 @@ START → classify_service → [analyze_agent | analyze_tool] → provision_serv 5. Returns `502` on Service/label lookup failure, discovery-token minting failure, or MCP call failure. > K8s access: `get` on `services` in the workload namespace (tool path). Identity is resolved by `classify_service` (config API). - > MCP path convention: all MCP tool services must serve at `/mcp` and carry the `protocol.kagenti.io/mcp` label. This label is a **deploy-time prerequisite** — the kagenti-operator does not stamp it today; automatic stamping is requested upstream (`docs/issues/kagenti-operator-mcp-label-stamping.md`). Until then it must be applied at deploy time; `analyze_tool` fails loud (`502`, naming the workload + missing label) if it is absent. + > MCP path convention: all MCP tool services must serve at `/mcp` and carry the `protocol.rossoctl.io/mcp` label. This label is a **deploy-time prerequisite** — the rossoctl-operator does not stamp it today; automatic stamping is requested upstream (`docs/issues/rossoctl-operator-mcp-label-stamping.md`). Until then it must be applied at deploy time; `analyze_tool` fails loud (`502`, naming the workload + missing label) if it is absent. > Discovery auth: the tool's inbound `jwt-validation` plugin stays fully enforcing — there is no > path bypass for `/mcp`. `analyze_tool` authenticates instead of relaxing the sidecar's auth. - **`provision_service`**: non-LLM node; calls `create_service_role` and `create_service_scope` from `aiac.idp.configuration.api` for each entry in `ServiceProvision`. Reads `service_id` from state. Writes are **idempotent** (create-or-get). - - Also persists the discovered `service_type` onto the Keycloak client via `Configuration.set_service_type(service, service_type)`, which stores it as the **`client.type`** attribute. This is the **authoritative origin** of the attribute that the IdP library's `Service._resolve_keycloak_fields` reads back (see the IdP library spec's type-resolution precedence). No case mapping is needed here: `service_type` is a `ServiceType` (values `Agent`/`Tool`), already matching `client.type` and `Service.type`. Case normalization happens once, upstream, when `classify_service` reads the lowercase `kagenti.io/type` label. + - Also persists the discovered `service_type` onto the Keycloak client via `Configuration.set_service_type(service, service_type)`, which stores it as the **`client.type`** attribute. This is the **authoritative origin** of the attribute that the IdP library's `Service._resolve_keycloak_fields` reads back (see the IdP library spec's type-resolution precedence). No case mapping is needed here: `service_type` is a `ServiceType` (values `Agent`/`Tool`), already matching `client.type` and `Service.type`. Case normalization happens once, upstream, when `classify_service` reads the lowercase `rossoctl.io/type` label. ### State: `OnboardingProvisionState` diff --git a/aiac/docs/specs/components/event-broker.md b/aiac/docs/specs/components/event-broker.md index 0f190e4d3..7f74df612 100644 --- a/aiac/docs/specs/components/event-broker.md +++ b/aiac/docs/specs/components/event-broker.md @@ -19,7 +19,7 @@ The Event Broker is a single-node NATS JetStream instance. It owns no business l | Consumer type | Durable push consumer with queue group (competing consumers) | | Authentication | None — ClusterIP network isolation is the access control mechanism | | Dead-letter subject | `aiac.apply.dlq` | -| Max delivery attempts | 5 — message routed to DLQ after 5 unacknowledged redeliveries | +| Max delivery attempts | 5 (max_deliver); the consumer republishes to the DLQ subject on the final failed delivery | --- @@ -30,7 +30,7 @@ The Event Broker is a single-node NATS JetStream instance. It owns no business l | `aiac.apply.service.{id}` | Keycloak SPI listener | AIAC Agent | Keycloak `CLIENT_CREATED` event | | `aiac.apply.role.{id}` | Keycloak SPI listener | AIAC Agent | Keycloak role created/updated | | `aiac.apply.policy.build` | RAG Ingest Service | AIAC Agent | Post-ingest completion (any collection) | -| `aiac.apply.dlq` | NATS JetStream (automatic) | Operator (manual inspection) | Max delivery attempts exceeded | +| `aiac.apply.dlq` | AIAC Agent consumer (republish on max-deliver) | Operator (manual inspection) | Max delivery attempts exceeded | **`rebuild` is not routed through the Event Broker.** It is an operator-only command issued directly via `POST /apply/policy/rebuild` on the AIAC Agent using `kubectl port-forward`. @@ -53,7 +53,7 @@ For `aiac.apply.policy.build`, the payload is empty (`{}`). The AIAC Agent pulls - **At-least-once delivery** — NATS redelivers any message not acknowledged within the `AckWait` window. - **Exactly-one processing** — the Agent subscribes via a queue group (`aiac-agent-consumer`). Only one Agent pod receives each message; other pods in the group are not notified. - **Replay on restart** — `WorkQueuePolicy` retains all unacknowledged messages. A restarted Agent pod automatically receives pending messages on reconnection. -- **DLQ on repeated failure** — after 5 unacknowledged redeliveries, NATS routes the message to `aiac.apply.dlq` for operator inspection. No message is silently dropped. +- **DLQ on repeated failure** — after the 5th unacknowledged delivery, the consumer republishes the message to `aiac.apply.dlq` and terminates it. No message is silently dropped. --- @@ -119,4 +119,4 @@ httpx | Init container health-check loop | HTTP 4xx then 200 sequence | Exits 0 only after all four dependencies respond healthy | | Init container stream creation | NATS JetStream `add_stream` call | Called with correct stream name, subjects, and retention policy; idempotent on second call | | Agent NATS consumer dispatch | NATS message delivery | Correct `/apply/*` handler invoked for each subject pattern; message acked on success; message not acked on handler exception | -| DLQ routing | NATS max redelivery exceeded | Message appears on `aiac.apply.dlq` after 5 failures | +| DLQ routing | consumer detects num_delivered == max_deliver | Message appears on `aiac.apply.dlq` after 5 failures | diff --git a/aiac/docs/specs/components/idp-configuration-service.md b/aiac/docs/specs/components/idp-configuration-service.md index 49bed69ee..604978bd2 100644 --- a/aiac/docs/specs/components/idp-configuration-service.md +++ b/aiac/docs/specs/components/idp-configuration-service.md @@ -175,7 +175,7 @@ Environment variables (injected via Kubernetes Deployment manifest): - Bind: `0.0.0.0:7071` - Base image: `python:3.12-slim` - Kubernetes ClusterIP Service: `aiac-pdp-config-service:7071` -- Deployment: co-located with PDP Policy Writer as a container in the **Kagenti Interface Pod** (`pdp-interface-deployment.yaml`) +- Deployment: co-located with PDP Policy Writer as a container in the **Rossoctl Interface Pod** (`pdp-interface-deployment.yaml`) - Python library: `aiac.idp.configuration` ## Dependencies (`requirements.txt`) diff --git a/aiac/docs/specs/components/keycloak-service.md b/aiac/docs/specs/components/keycloak-service.md index b6d4a1d56..409b9f2cb 100644 --- a/aiac/docs/specs/components/keycloak-service.md +++ b/aiac/docs/specs/components/keycloak-service.md @@ -1,8 +1,6 @@ # ~~Component PRD: Keycloak Configuration Service~~ -> **Superseded.** This component has been replaced by two separate services: -> - **IdP Configuration Service** (read endpoints) — see [idp-configuration-service.md](idp-configuration-service.md) -> - **PDP Policy Writer — Keycloak Implementation** (write endpoints) — see [pdp-policy-keycloak-service.md](pdp-policy-keycloak-service.md) +> **Superseded.** This component's read endpoints have been replaced by the **IdP Configuration Service** — see [idp-configuration-service.md](idp-configuration-service.md). > > The content below is retained for reference only. > @@ -41,7 +39,7 @@ Environment variables (injected via Kubernetes Deployment manifest): | Variable | Required | Description | |----------|----------|-------------| | `KEYCLOAK_URL` | Yes | Keycloak base URL, e.g. `http://keycloak-service.keycloak.svc:8080` | -| `KEYCLOAK_REALM` | Yes | Realm name, e.g. `kagenti` | +| `KEYCLOAK_REALM` | Yes | Realm name, e.g. `rossoctl` | | `KEYCLOAK_ADMIN_USERNAME` | Yes | Admin username (from `keycloak-admin-secret`) | | `KEYCLOAK_ADMIN_PASSWORD` | Yes | Admin password (from `keycloak-admin-secret`) | diff --git a/aiac/docs/specs/components/keycloak-spi-listener.md b/aiac/docs/specs/components/keycloak-spi-listener.md new file mode 100644 index 000000000..ecf49a0ea --- /dev/null +++ b/aiac/docs/specs/components/keycloak-spi-listener.md @@ -0,0 +1,134 @@ +# Component PRD: Keycloak SPI Listener + +## Description + +A custom Keycloak **Event Listener SPI** (Java) that listens to Keycloak's internal admin-event bus and translates entity lifecycle events into NATS publish calls on the [Event Broker](event-broker.md). It is a pure publisher — no business logic, the same "thin adapter" philosophy as the AIAC Agent's own NATS consumer (`aiac/agent/eventbus/`). + +The SPI is the trigger source for two of the Agent's three automated use cases: new service onboarding (`CLIENT_CREATED`) and role change (role create/update). It carries no policy knowledge and no state — it emits a minimal `{ "id": "" }` trigger and lets the Agent pull all required state from the IdP Configuration Service at processing time. + +Source lives at `aiac/keycloak-spi/`. It is a Java/Maven module, not part of the Python `aiac` package, and is built and released independently (see [Build & Deploy](#build--deploy)). + +This PRD documents the **as-built** behavior of the implemented SPI and resolves the open questions previously tracked in `keycloak-spi/README.md`. + +--- + +## Event → Subject Mapping + +The SPI subscribes to Keycloak **admin events** (`onEvent(AdminEvent, boolean)`). The legacy user-facing event stream (`onEvent(Event)`) is a **no-op by design** — `CLIENT_CREATED` and role create/update are admin events in modern Keycloak (`ResourceType` + `OperationType`), not entries in the user-facing `EventType` enum. User events (`REGISTER`, `UPDATE_PROFILE`, …) are dropped, because OPA rules are role-scoped and resolve entitlements from the caller's role automatically. + +| Keycloak admin event (`ResourceType` + `OperationType`) | Event Broker subject | Entity ID source | +|---|---|---| +| `CLIENT` + `CREATE` | `aiac.apply.service.{internal-uuid}` | segment after `clients/` in `resourcePath` (`clients/{uuid}`) — the Keycloak **internal client UUID**, not the human-readable `clientId` | +| `REALM_ROLE` + `CREATE` \| `UPDATE` | `aiac.apply.role.{role-name}` | trailing segment of `resourcePath` (`roles/{name}`) — the role **name** | +| `CLIENT_ROLE` + `CREATE` \| `UPDATE` | `aiac.apply.role.{role-name}` | trailing segment of `resourcePath` (`clients/{uuid}/roles/{name}`) — the role **name** | +| everything else (other resource kinds, `DELETE`, user events) | — (dropped) | — | + +`CLIENT_ROLE` is a deliberate, harmless **superset** of the AIAC PRD's realm-role-only mention: a client role and a realm role both map to `aiac.apply.role.{name}` using the trailing path segment, so no additional handling is needed on the Agent side. + +The subject mapping is isolated in a pure `SubjectMapper` class with **no Keycloak imports**, so it is unit-testable without a running server (see [Testing](#testing)). Malformed or `null` resource paths are dropped, never thrown. + +The AIAC Agent subject schema (documented in [event-broker.md](event-broker.md) and PRD §7.6) is **authoritative**; this SPI is one publisher against it. The `aiac.apply.service.{id}` / `aiac.apply.role.{id}` subjects here are the same ones the Agent's consumer dispatches on (`onboard_service` / `update_role`). + +--- + +## Message Payload + +Every publish carries the minimal JSON payload: + +```json +{ "id": "" } +``` + +The event is a **trigger, not a data carrier** — consistent with [event-broker.md](event-broker.md). The entity ID is the trailing segment of the subject (subjects are always `aiac.apply..` and IDs never contain `.`), so the payload ID always equals the ID encoded in the subject. The Agent uses it to pull full entity state from the IdP Configuration Service. + +--- + +## Publish Semantics & Retry Policy + +- **Core NATS publish.** The SPI publishes to the subject with a plain NATS publish. The Event Broker's `aiac-events` stream (subjects `aiac.apply.>`, `WorkQueuePolicy`) captures the message; **durability, replay, and DLQ handling all live on the consumer/broker side** (see [event-broker.md](event-broker.md)), not in the SPI. +- **No application-level retry or buffering.** The SPI does not retry individual publishes and does not queue events locally. End-to-end at-least-once delivery begins once a message lands in the JetStream stream. +- **Transport-level reconnect.** The shared NATS connection uses jnats' built-in auto-reconnect, so a broker that drops after a connection is established is transparently reconnected. +- **Fail-open at startup.** The NATS connection is opened once by the provider **factory** (`postInit`) and shared across every per-request provider instance; it is closed by the factory (`close`). If the broker is unreachable at Keycloak startup, the connection is left null, Keycloak startup is **never failed**, and matching events are dropped with a `WARN` log until the deployment is fixed. This is a deliberate availability tradeoff: the SPI must never take Keycloak down. + +The publish boundary is therefore **best-effort (at-most-once)**; the platform's durability guarantee starts at the stream. Missed triggers (broker down at emit time) are recovered by the operator-only `rebuild` command, which reconciles full state independently of the event stream. + +--- + +## Configuration + +| Variable | Default | Source | +|---|---|---| +| `NATS_URL` | `nats://aiac-event-broker-service:4222` | ConfigMap (`aiac-pdp-config`) — the same ConfigMap used by the Agent and RAG Ingest Service | + +`NATS_URL` is resolved in `AiacEventListenerProviderFactory.init()` in this order: + +1. Keycloak SPI config value `natsUrl` (see the override key below); +2. the plain `NATS_URL` environment variable; +3. the cluster default `nats://aiac-event-broker-service:4222`. + +**The deployment uses the plain `NATS_URL` env var** injected from `aiac-pdp-config` — the deterministic, ambiguity-free path, matching every other AIAC component. The Keycloak SPI-scoped override key is available if ever needed (see open question 3 below). + +No authentication credentials are required. Consistent with PRD §10, the Event Broker has **no auth** — ClusterIP network isolation is the access control mechanism. + +--- + +## Build & Deploy + +- **Language / build:** Java 17, Maven. Artifact: `target/aiac-event-listener-0.1.0.jar`, **shaded** so the compile-scope `jnats` dependency (not on Keycloak's classpath) is bundled onto the ServiceLoader runtime classpath. All `keycloak-*` dependencies are `provided`. +- **Provider discovery:** registered via `META-INF/services/org.keycloak.events.EventListenerProviderFactory`; provider id `aiac-event-listener`. +- **Custom image:** 3-stage Dockerfile — (1) build the shaded JAR, (2) drop it into `/opt/keycloak/providers/` and run `kc.sh build` so the augmented server is baked into the image (no per-pod build at startup), (3) copy the built distribution into the final runtime image. Default base `quay.io/keycloak/keycloak:26.5.2`; image ref `ghcr.io/rossoctl/cortex/aiac-keycloak-event-listener:0.1.0-kc26.5.2` (`make image` / `make push`). +- **JAR-only install:** copy the JAR into an existing Keycloak's `providers/`, run `kc.sh build`, restart. +- **CI:** like the other AIAC components (PRD §10), this SPI is **not** registered in the repo's `build.yaml` matrix; it has its own Makefile-driven build. +- **Not self-wiring:** this module builds and packages the SPI only. Deploying the image into a running Keycloak (Helm values / Operator CR image override) and enabling the listener are **manual steps owned by the separate Keycloak deployment**, not this repo. + +### Enabling in a realm + +The listener is discovered automatically once on the classpath, but must still be added to the realm's admin-events listener list — via **Realm Settings → Events → Event Listeners** (add `aiac-event-listener`) or the Admin REST API: + +```sh +curl -X PUT "$KEYCLOAK_URL/admin/realms/$REALM/events/config" \ + -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \ + -d '{"adminEventsEnabled": true, "eventsListeners": ["jboss-logging", "aiac-event-listener"]}' +``` + +(`eventsListeners` replaces the whole list — keep `jboss-logging` unless you intend to remove it.) + +--- + +## Open Questions (Resolved) + +These were tracked as "Known gaps / open questions" in `keycloak-spi/README.md`. Resolutions: + +1. **Role-ID format — resolved: role *name*, provisional pending UC3.** + The **service** path is fully confirmed: the Agent's `onboard_service` expects the Keycloak **internal client UUID** (`Service.id == Trigger.entity_id`, *not* `clientId`), which is exactly the segment the SPI extracts from `clients/{uuid}`. The **role** path targets `update_role`, which is currently a UC3 stub performing a role-keyed replace (`override=True`). Keying the subject by role **name** is consistent with that role-keyed replace semantics and is the accepted contract for now. This must be re-verified when UC3 (issue 3.11) finalizes its ID contract; if UC3 requires a UUID, the `REALM_ROLE` / `CLIENT_ROLE` extraction in `SubjectMapper` is the single point to change. + +2. **jnats / shade plugin versions — resolved: pin jnats 2.x, shade 3.6.0.** + As-built pins `jnats 2.20.6` and `maven-shade-plugin 3.6.0` against `keycloak 26.5.2` (compiler-plugin 3.13.0, surefire 3.5.2, JUnit 5.11.3). `maven-shade-plugin 3.6.0` is current. `jnats` current stable is `2.25.1` (Jan 2026); bumping is low-risk and recommended at the next maintenance pass, but 2.20.6 is a valid, tested pin. The pin policy: track jnats 2.x, re-check Maven Central at each Keycloak version bump. + +3. **SPI env-var naming — resolved: `NATS_URL` is authoritative; SPI key confirmed as an override.** + The deployment injects the plain **`NATS_URL`** env var (from `aiac-pdp-config`), which is deterministic and sidesteps camelCase normalization. Keycloak's provider-config namespace uses a **hyphenated** SPI id `events-listener`, so the CLI/override forms are: + - CLI: `--spi-events-listener-aiac-event-listener-nats-url=` + - Env: `KC_SPI_EVENTS_LISTENER_AIAC_EVENT_LISTENER_NATS_URL=` + + (pattern: `KC_SPI___`, uppercased, dashes → underscores). These are available as overrides, but plain `NATS_URL` is the path the platform uses. + +--- + +## Testing + +Good tests here assert **external behavior** — the subject and payload produced for a given Keycloak event — not internal wiring. The `EventListenerProvider` / `EventListenerProviderFactory` classes are thin wrappers around the pure mapping logic and around jnats; unit-testing them without a running Keycloak server is impractical, so they are covered by manual/integration verification instead. + +| Target | What to test | Prior art | +|---|---|---| +| `SubjectMapper` (existing seam) | Every event → subject/payload rule in isolation, plus drop-not-throw for malformed / null / unhandled events. Pure JUnit, no Keycloak server, no mocked `KeycloakSession`/`AdminEvent`. | `SubjectMapperTest` (implemented — client-created, client-update-dropped, realm/client role create+update, other-kind-dropped, malformed/null paths, payload shape) | +| Provider / Factory | Not unit-tested. Verified manually: build the image, enable the listener in a realm, `nats sub 'aiac.apply.>'`, then create a client/role and confirm a message on the expected subject with the expected id. | Event Broker consumer integration tests (require a live NATS JetStream instance) — see [event-broker.md](event-broker.md) | + +The `SubjectMapper`-as-seam decision mirrors the wider AIAC testing convention: push logic into a pure, import-free unit and test it there; treat the Keycloak/NATS boundary as integration-only. + +--- + +## Cross-References + +- [event-broker.md](event-broker.md) — the NATS JetStream broker this SPI publishes to; authoritative subject schema, payload contract, delivery guarantees, and DLQ. +- PRD §7.11 (Keycloak SPI Listener) — component summary; links here as its full spec. +- `aiac/keycloak-spi/README.md` — build/deploy/config runbook. diff --git a/aiac/docs/specs/components/library-idp.md b/aiac/docs/specs/components/library-idp.md index 1dabaeba6..e953251a9 100644 --- a/aiac/docs/specs/components/library-idp.md +++ b/aiac/docs/specs/components/library-idp.md @@ -101,9 +101,9 @@ Represents a service (Keycloak: `client`). 2. Otherwise the Keycloak client attribute **`client.type`** ∈ {`Agent`, `Tool`} — a **plain string**. Client attribute values are plain strings; a **list** value (e.g. `["Agent"]`, the shape realm-role attributes use) fails the check and resolves to `None`. Capitalization matches the `ServiceType` values. 3. Otherwise `None`. -The attribute is set via `Configuration.set_service_type` (below); its authoritative origin is UC1 Service Onboarding — `classify_service` **discovers** the type from the operator's `kagenti.io/type` label and `provision_service` **persists** it onto the client via `set_service_type` (see the aiac-agent UC1 spec). There is **no** `spiffe://` clientId fallback and **no** description-keyword inference — typing is `client.type`-attribute-only. (The former `spiffe:// ⇒ Agent` fallback was **removed**: a `spiffe://` clientId indicates a SPIRE-enabled workload, **not** necessarily an agent — it could mis-type a SPIRE-enabled tool — so clients without a `client.type` attribute now resolve to `None`.) +The attribute is set via `Configuration.set_service_type` (below); its authoritative origin is UC1 Service Onboarding — `classify_service` **discovers** the type from the operator's `rossoctl.io/type` label and `provision_service` **persists** it onto the client via `set_service_type` (see the aiac-agent UC1 spec). There is **no** `spiffe://` clientId fallback and **no** description-keyword inference — typing is `client.type`-attribute-only. (The former `spiffe:// ⇒ Agent` fallback was **removed**: a `spiffe://` clientId indicates a SPIRE-enabled workload, **not** necessarily an agent — it could mis-type a SPIRE-enabled tool — so clients without a `client.type` attribute now resolve to `None`.) -> **`ServiceType`** (`aiac.idp.configuration.models`) is a `str` enum — `AGENT = "Agent"`, `TOOL = "Tool"` — shared by `Service.type`, `set_service_type`, and the aiac-agent sub-agents (one vocabulary, no duplication). Values are capitalized to match the `client.type` attribute; because it subclasses `str`, `ServiceType.AGENT == "Agent"`, so it is a drop-in for the former `Literal["Agent", "Tool"]`. The operator's lowercase `kagenti.io/type` pod label is normalized to a member via `ServiceType(label.capitalize())` in UC1 `classify_service`. +> **`ServiceType`** (`aiac.idp.configuration.models`) is a `str` enum — `AGENT = "Agent"`, `TOOL = "Tool"` — shared by `Service.type`, `set_service_type`, and the aiac-agent sub-agents (one vocabulary, no duplication). Values are capitalized to match the `client.type` attribute; because it subclasses `str`, `ServiceType.AGENT == "Agent"`, so it is a drop-in for the former `Literal["Agent", "Tool"]`. The operator's lowercase `rossoctl.io/type` pod label is normalized to a member via `ServiceType(label.capitalize())` in UC1 `classify_service`. #### `Scope` @@ -282,7 +282,7 @@ class Configuration: > introduced by these methods; `get_services_by_role` is **retained as a method** for API completeness > and ad-hoc callers, but the current SPM-based PCE no longer calls it: its **only** runtime IdP read is > `Configuration.get_services()` (see `aiac.policy.computation`), and owner/role lookups against stored -> policy go through the Policy Store's `get_service_policies_by_role`, not this IdP filter. +> policy go through the Policy Model Store's `get_service_policies_by_role`, not this IdP filter. `get_subjects_by_role(role: Role) -> list[Subject]`: 1. `GET {AIAC_PDP_CONFIG_URL}/subjects?role_id={role.id}&realm=` @@ -352,7 +352,7 @@ Read from a `.env` file co-located with `api.py` (`aiac/src/aiac/idp/configurati ```python from aiac.idp.configuration.api import Configuration -cfg = Configuration.for_realm("kagenti") +cfg = Configuration.for_realm("rossoctl") subjects = cfg.get_subjects() for s in subjects: print(s.username, s.email) diff --git a/aiac/docs/specs/components/library-pdp-policy.md b/aiac/docs/specs/components/library-pdp-policy.md index e6a2ba773..1f4f8d336 100644 --- a/aiac/docs/specs/components/library-pdp-policy.md +++ b/aiac/docs/specs/components/library-pdp-policy.md @@ -102,7 +102,7 @@ Key behaviors to assert: - **Keycloak interaction:** this library never calls Keycloak directly. All IdP operations go through `aiac.idp.configuration`. - **Policy computation:** translating `list[PolicyRule]` into `AgentPolicyModel` objects is the responsibility of `aiac.policy.computation`, not this library. -- **Policy persistence:** the Policy Store (`aiac.policy.store`) owns structured `AgentPolicyModel` durability. This library targets the OPA runtime only. +- **Policy persistence:** the Policy Model Store (`aiac.policy.model_store`) owns structured `AgentPolicyModel` durability. This library targets the OPA runtime only. --- diff --git a/aiac/docs/specs/components/library-policy-store.md b/aiac/docs/specs/components/library-policy-model-store.md similarity index 79% rename from aiac/docs/specs/components/library-policy-store.md rename to aiac/docs/specs/components/library-policy-model-store.md index 9e18b592f..e2c2d7991 100644 --- a/aiac/docs/specs/components/library-policy-store.md +++ b/aiac/docs/specs/components/library-policy-model-store.md @@ -1,6 +1,6 @@ -# Component PRD: Policy Store Library (`aiac.policy.store.library`) +# Component PRD: Policy Model Store Library (`aiac.policy.model_store.library`) -Companion library for the [AIAC Policy Store](policy-store.md). Follows the same pattern as `aiac.pdp.policy.library` — module-level functions, URL from env var via `python-dotenv`, `RuntimeError` on non-2xx. +Companion library for the [AIAC Policy Model Store](policy-model-store.md). Follows the same pattern as `aiac.pdp.policy.library` — module-level functions, URL from env var via `python-dotenv`, `RuntimeError` on non-2xx. ## Location `aiac/src/aiac/policy/store/library/` @@ -17,7 +17,7 @@ aiac/src/aiac/policy/store/ All `__init__.py` files are empty. Callers use explicit submodule paths: ```python -from aiac.policy.store.library.api import ( +from aiac.policy.model_store.library.api import ( get_service_policy, get_service_policy_by_scope, get_service_policies_by_role, @@ -37,10 +37,10 @@ exposes any per-agent read/write functions. The library surface is entirely SPM- --- -## Submodule: `aiac.policy.store.library.api` +## Submodule: `aiac.policy.model_store.library.api` ### Description -HTTP client module wrapping the [AIAC Policy Store](policy-store.md) REST API. Exposes five module-level functions returning `ServicePolicyModel` objects directly — no Kubernetes client boilerplate. Service URL is read from the `AIAC_POLICY_STORE_URL` environment variable (default: `http://127.0.0.1:7074`). All functions raise `RuntimeError` on an unexpected non-2xx response (a `404` on the by-id read is handled, not raised — see below). +HTTP client module wrapping the [AIAC Policy Model Store](policy-model-store.md) REST API. Exposes five module-level functions returning `ServicePolicyModel` objects directly — no Kubernetes client boilerplate. Service URL is read from the `AIAC_POLICY_MODEL_STORE_URL` environment variable (default: `http://127.0.0.1:7074`). All functions raise `RuntimeError` on an unexpected non-2xx response (a `404` on the by-id read is handled, not raised — see below). ### Dependencies ``` @@ -84,7 +84,7 @@ def delete_service_policy(service_id: str) -> None `service_id` is a plain string everywhere in this API (slashes and all) — callers never encode anything. Internally, the three functions above base64url-encode `service_id` into the URL path -segment via `aiac.policy.store.keying.encode_service_id` before issuing the request (the clientId is +segment via `aiac.policy.model_store.keying.encode_service_id` before issuing the request (the clientId is slash-bearing and can't be a single path segment); the service decodes it back. `service_id` in every returned `ServicePolicyModel` is always the original, decoded form. @@ -104,16 +104,16 @@ the same signature without changing callers. ### Configuration -Read from `AIAC_POLICY_STORE_URL` environment variable (or `.env` file co-located with `api.py`). Falls back to the default if absent. +Read from `AIAC_POLICY_MODEL_STORE_URL` environment variable (or `.env` file co-located with `api.py`). Falls back to the default if absent. | Variable | Default | |----------|---------| -| `AIAC_POLICY_STORE_URL` | `http://127.0.0.1:7074` | +| `AIAC_POLICY_MODEL_STORE_URL` | `http://127.0.0.1:7074` | ### Usage ```python -from aiac.policy.store.library.api import ( +from aiac.policy.model_store.library.api import ( get_service_policy, get_service_policy_by_scope, get_service_policies_by_role, @@ -139,7 +139,7 @@ apply_service_policy("weather-service", updated_spm) ## Testing Decisions -**Seam:** HTTP boundary — mock responses from `AIAC_POLICY_STORE_URL`. +**Seam:** HTTP boundary — mock responses from `AIAC_POLICY_MODEL_STORE_URL`. **Prior art:** `3.14-unit-tests-write-api.md` (mock PDP Policy Writer HTTP; cover module-level functions). @@ -151,4 +151,4 @@ Key behaviors to assert: - `apply_service_policy(id, spm)` issues `POST /policy/services/{id}` with serialized `ServicePolicyModel`; upsert round-trip (write then read back the same SPM). - `delete_service_policy(id)` issues `DELETE /policy/services/{id}`; returns `None` on success. - Any unexpected non-2xx response raises `RuntimeError`. -- `AIAC_POLICY_STORE_URL` is read from env; falls back to `http://127.0.0.1:7074`. +- `AIAC_POLICY_MODEL_STORE_URL` is read from env; falls back to `http://127.0.0.1:7074`. diff --git a/aiac/docs/specs/components/pdp-policy-keycloak-service.md b/aiac/docs/specs/components/pdp-policy-keycloak-service.md deleted file mode 100644 index 2073c3d6b..000000000 --- a/aiac/docs/specs/components/pdp-policy-keycloak-service.md +++ /dev/null @@ -1,80 +0,0 @@ -# Component PRD: PDP Policy Writer — Keycloak Implementation - -## Location -`aiac/src/aiac/pdp/service/policy/keycloak/` - -## Description -A FastAPI web service that applies RBAC policy changes to Keycloak by managing composite role mappings. Roles are made composites of service (client) permissions (roles), so that any subject (user) assigned a role automatically inherits the associated service permissions. Stateless — no caching. - -**Superseded — not deployed by any current manifest.** This was an earlier implementation of the PDP Policy Writer, managing Keycloak composite role mappings directly. The Kubernetes Interface Pod (`aiac/k8s/pdp-interface-deployment.yaml`) deploys the **OPA rego-file mock** (`aiac-pdp-policy-opa`, see [pdp-policy-writer-opa.md](pdp-policy-writer-opa.md)) as its Phase 1 PDP Policy Writer container instead, behind the same `aiac-pdp-policy-service:7072` ClusterIP. This service's source remains in-tree for reference but is not built or deployed by the current K8s manifests. - -## Endpoints - -| Method | Path | Keycloak Admin API call | Description | -|--------|------|------------------------|-------------| -| POST | `/roles/{role_name}/composites` | `POST /admin/realms/{realm}/roles/{role-name}/composites` | Add service permissions (client roles) to a role composite | -| DELETE | `/roles/{role_name}/composites` | `DELETE /admin/realms/{realm}/roles/{role-name}/composites` | Remove service permissions (client roles) from a role composite | -| DELETE | `/composites` | loop: `GET /admin/realms/{realm}/roles` → per role `GET .../composites` → `DELETE .../composites` | Revoke all composite mappings from all roles (rebuild) | -| POST | `/services/{service_id}/permissions` | `POST /admin/realms/{realm}/clients/{service_id}/roles` | Create a new permission (client role) for a specific service | -| POST | `/services/{service_id}/scopes` | `POST /admin/realms/{realm}/client-scopes` then `PUT .../clients/{service_id}/default-client-scopes/{scope_id}` | Create a realm-level scope and assign it to a service as a default scope (atomic) | - -Every endpoint accepts an optional `realm` query parameter, same as the IdP Configuration Service. - -`POST /roles/{role_name}/composites` and `DELETE /roles/{role_name}/composites` accept a JSON array of role representation objects `[{"id": "...", "name": "..."}]` and return `204 No Content` on success. - -`DELETE /composites` returns `204 No Content` on success. - -`POST /services/{service_id}/permissions` and `POST /services/{service_id}/scopes` accept a JSON object `{"name": "...", "description": "..."}` and return `201 Created` with the created resource as JSON. - -All endpoints return `502 Bad Gateway` with a JSON error body if the Keycloak Admin API call fails. - -## Configuration - -| Variable | Required | Description | -|----------|----------|-------------| -| `KEYCLOAK_URL` | Yes | Keycloak base URL, e.g. `http://keycloak-service.keycloak.svc:8080` | -| `KEYCLOAK_REALM` | Yes | Realm name, e.g. `kagenti` | -| `KEYCLOAK_ADMIN_USERNAME` | Yes | Admin username (from `keycloak-admin-secret`) | -| `KEYCLOAK_ADMIN_PASSWORD` | Yes | Admin password (from `keycloak-admin-secret`) | - -## Runtime - -- Framework: FastAPI -- Server: uvicorn -- Bind: `0.0.0.0:7072` -- Base image: `python:3.12-slim` -- Kubernetes ClusterIP Service: `aiac-pdp-policy-service:7072` -- Deployment: co-located with IdP Configuration Service as a container in the **Kagenti Interface Pod** (`pdp-interface-deployment.yaml`) - -## Dependencies (`requirements.txt`) - -``` -fastapi -uvicorn[standard] -python-keycloak -``` - -## File structure - -``` -aiac/src/aiac/pdp/service/ -├── __init__.py -└── policy/ - ├── __init__.py - └── keycloak/ - ├── __init__.py - ├── Dockerfile - ├── requirements.txt - └── main.py -``` - -## `main.py` behaviour notes - -- Instantiate the default `KeycloakAdmin` once at startup using env vars. -- `get_admin` is a FastAPI dependency accepting `realm: str | None = Query(None)`. -- `POST /roles/{role_name}/composites`: call `admin.add_composite_realm_roles_to_role(role_name, roles)`; return `Response(status_code=204)`. -- `DELETE /roles/{role_name}/composites`: call `admin.remove_composite_realm_roles_to_role(role_name, roles)`; return `Response(status_code=204)`. -- `DELETE /composites`: fetch all roles via `admin.get_realm_roles()`; for each role call `admin.get_composite_realm_roles_of_role(role_name)`; if composites are non-empty call `admin.remove_composite_realm_roles_to_role(role_name, composites)`; return `Response(status_code=204)`. -- `POST /services/{service_id}/permissions`: call `admin.create_client_role(service_id, {"name": ..., "description": ...})`; return `JSONResponse(status_code=201)` with the created permission representation. -- `POST /services/{service_id}/scopes`: call `admin.create_client_scope({"name": ..., "description": ..., "protocol": "openid-connect"})` to create the realm-level scope, then call `admin.add_default_default_client_scope(service_id, scope_id)` to assign it to the service; return `JSONResponse(status_code=201)` with the created scope representation. -- On `KeycloakError`, return HTTP 502 with `{"error": str(e)}`. diff --git a/aiac/docs/specs/components/pdp-policy-writer-opa.md b/aiac/docs/specs/components/pdp-policy-writer-opa.md index f7c65c4e2..95bbd05b4 100644 --- a/aiac/docs/specs/components/pdp-policy-writer-opa.md +++ b/aiac/docs/specs/components/pdp-policy-writer-opa.md @@ -4,11 +4,11 @@ `aiac/src/aiac/pdp/service/policy/opa/` ## Description -A FastAPI web service that translates a **Policy Model** into OPA Rego packages and writes them to an `AuthorizationPolicy` Kubernetes Custom Resource. The OPA plugin embedded in each AuthBridge instance fetches the Rego packages relevant to its pod from this CR at startup. +A FastAPI web service that translates a **Policy Model** into OPA Rego packages and, for each agent, **server-side-applies** the two generated packages into a per-agent `AuthorizationPolicy` Kubernetes Custom Resource (`agent.rossoctl.dev/v1alpha1`, `scope: client` — one CR per agent). The `bundle-service` (operator repo) composes those per-agent CRs into per-pod OPA bundles; the OPA plugin embedded in each AuthBridge instance polls the bundle relevant to its pod and evaluates it. -The service is deployed as a container in the **Kagenti Interface Pod** alongside the IdP Configuration Service, behind the `aiac-pdp-policy-service:7072` ClusterIP. +The service is deployed as a container in the **Rossoctl Interface Pod** alongside the IdP Configuration Service, behind the `aiac-pdp-policy-service:7072` ClusterIP. -The service has no dependency on Keycloak. All Keycloak operations (entity reads) are handled by the **IdP Configuration Service** and its library (`aiac.idp.configuration`). +The service has no dependency on Keycloak. All Keycloak operations (entity reads) are handled by the **IdP Configuration Service** and its library (`aiac.idp.configuration`). The legacy Keycloak composite / authorization-services policy writer has been **removed** (handoff 04); this OPA CR writer is the sole policy-writer surface. --- @@ -38,9 +38,9 @@ Complete policy definition for a single agent (service). Contains two sets of `P | `agent_id` | `str` | Service ID from the AIAC trigger event (`aiac.apply.service.{id}`) | | `agent_roles` | `list[Role]` | Realm roles assigned to this agent | | `agent_scopes` | `list[Scope]` | Scopes this agent exposes | -| `source_roles` | `dict[str, list[Role]]` | Inbound: source (calling service) **id** → roles held. **Optional** gate input — an absent source passes. | -| `subject_roles` | `dict[str, list[Role]]` | Inbound: subject (end-user) **id** → roles held. **Mandatory** gate input. | -| `target_scopes` | `dict[str, list[Scope]]` | Outbound: target service **id** → scopes this agent may request on it | +| `source_roles` | `dict[str, list[Role]]` | Inbound: source (calling service) **id** → roles held. Keyed by the inbound `input.identity.client_id`. **Optional** gate input — an absent `client_id`, or a platform bypass client, passes. | +| `subject_roles` | `dict[str, list[Role]]` | Inbound + outbound: subject (end-user) **id** → roles held. Keyed by `input.identity.subject`. Inbound gate: **mandatory**. | +| `target_scopes` | `dict[str, list[Scope]]` | Outbound: target service **id** → scopes this agent may request on it. Keys stay the **full** target service id (matching `input.identity.service_id`, a full SPIFFE ID); the scope **values** are de-prefixed to the bare MCP tool names carried in `input.mcp.params.name` (Q9). | | `inbound_rules` | `list[PolicyRule]` | Who may call this agent: `(subject_role, agent_scope)` tuples | | `outbound_rules` | `list[PolicyRule]` | What this agent may call: `(this_agent_role, target_scope)` tuples | | `outbound_subject_rules` | `list[PolicyRule]` | Which users may reach the agent's targets: `(user_role, tool_scope)` tuples. Defaults to `[]`. | @@ -51,9 +51,9 @@ Complete policy definition for a single agent (service). Contains two sets of `P **Outbound rule semantics:** this agent acting as realm role `role` is permitted to request the target scope `scope`. Grouped by role, these rules become the `agent_role_scopes` map (agent role → target scopes) that the outbound package evaluates. -**Outbound subject rule semantics:** a subject holding realm role `role` (a **user** role) is permitted to reach a **tool** exposing scope `scope`. Grouped by role, these rules become the `subject_role_scopes` map (user role → tool scopes) that the **outbound** package's subject gate evaluates against `target_scopes[input.target]`. This is distinct from `inbound_rules` (user → *agent* scope): the outbound subject gate answers "may this user reach the tool?", not "may this user call the agent?". +**Outbound subject rule semantics:** a subject holding realm role `role` (a **user** role) is permitted to reach a **tool** exposing scope `scope`. Grouped by role, these rules become the `subject_role_scopes` map (user role → tool scopes) that the **outbound** package's subject gate evaluates as `input.mcp.params.name in subject_role_scopes[role]`; its scope **values** are **de-prefixed** to the bare MCP tool name (Q9). This is distinct from `inbound_rules` (user → *agent* scope): the outbound subject gate answers "may this user reach the tool?", not "may this user call the agent?". -**Note on `target_scopes` direction:** the map is keyed by **target service id → allowed scopes** (the inverse of the former `scope_targets`, which was `scope → targets`). The outbound Rego generator emits this map **verbatim** and evaluates `target_scopes[input.target]` directly — there is no inversion (see below). +**Note on `target_scopes` direction:** the map is keyed by **target service id → allowed scopes** (the inverse of the former `scope_targets`, which was `scope → targets`). The outbound Rego generator emits the **full** target service id as the map key and evaluates `target_scopes[input.identity.service_id]` directly — there is no inversion (see below). Only the scope **values** are de-prefixed to bare MCP tool names; the **keys** stay the full target service id (Q9). ### `PolicyModel` @@ -87,57 +87,97 @@ No `?realm=` parameter — the service operates on a Kubernetes CR, not a Keyclo | Endpoint | Success | Error | |----------|---------|-------| -| `POST /policy` | `204 No Content` | `502 Bad Gateway` with `{"error": "..."}` if CR write fails | -| `POST /policy/agents/{agent_id}` | `204 No Content` | `502 Bad Gateway` if CR write fails | -| `DELETE /policy/agents/{agent_id}` | `204 No Content` | `502 Bad Gateway` if CR write fails | -| `DELETE /policy` | `204 No Content` | `502 Bad Gateway` if CR write fails | -| `GET /health` | `200 OK` `{"status": "ok"}` | `503 Service Unavailable` if CR is unreachable | +| `POST /policy` | `204 No Content` | **400** `{"error": …}` for a malformed / namespace-less `agent_id` (batch aborts, naming the bad agent; agents already applied stay written — no rollback); **502** `{"error": …}` for a Kubernetes API failure (or the additive dump's `OSError`) | +| `POST /policy/agents/{agent_id}` | `204 No Content` | **400** for a malformed `agent_id`; **502** for a Kubernetes API / dump failure | +| `DELETE /policy/agents/{agent_id}` | `204 No Content` | **400** for a malformed `agent_id`; **502** for a Kubernetes API failure. Deleting a **missing** agent is a no-op **204** (k8s 404 treated as success — idempotent) | +| `DELETE /policy` | `204 No Content` | **502** for a Kubernetes API failure | +| `GET /health` | `200 OK` `{"status": "ok"}` | `503 Service Unavailable` `{"status": "unavailable", "error": …}` if the bounded CR list fails | + +`GET /health` performs a bounded (`limit=1`) cluster-wide list of the CRD: a successful list — **including an empty one** — is `200`; any failure (unreachable API, RBAC-forbidden, CRD not served) is `503`. + +**400 vs 502 (Q11).** `400` is reserved strictly for a malformed / namespace-less `agent_id` — the `identity_ref` `ValueError`, whose message names the bad id. `502` is strictly for Kubernetes API failures and the additive rego dump's `OSError`. The two are never conflated. --- ## Rego package structure -For each `AgentPolicyModel`, the service generates **two Rego packages**: one for the inbound pipeline and one for the outbound pipeline. The `agent_id` is slugified for use in the package name (and filename). `agent_id` is the Keycloak clientId — a SPIFFE URI under SPIRE (`spiffe://host/ns/{ns}/sa/{name}`), or the plain `{ns}/{name}` clientId without it — so slugifying is not a simple hyphen→underscore substitution: the trust domain/host is dropped, the `{ns}/{name}` portion is extracted, and every remaining non-alphanumeric run collapses to a single underscore, lowercased (`aiac.pdp.service.policy.opa.rego.slugify`). This keeps the slug short and identical regardless of whether SPIRE is enabled — e.g. both `spiffe://localtest.me/ns/team1/sa/github-agent` and `team1/github-agent` slugify to `team1_github_agent`. +For each `AgentPolicyModel`, the service generates **two Rego packages** — one for the inbound pipeline and one for the outbound pipeline — and server-side-applies them as the two `policies[]` entries of the agent's `AuthorizationPolicy` CR. + +**Fixed package names — no slug (Q2).** Both packages use **fixed** names, regardless of agent: + +| Tier | Package | CR `policies[].path` | +|------|---------|----------------------| +| inbound | `authbridge.client.inbound.request` | `inbound/request.rego` | +| outbound | `authbridge.client.outbound.request` | `outbound/request.rego` | + +Each package begins with `import rego.v1`. The names never contain a slug: the `bundle-service` combiner requires the **exact** path `data.authbridge.client.`, so a per-agent package name would break the composition. Per-agent isolation is achieved at the **CR / bundle level** — bundle-service looks a CR up by namespace + name — not in the package name. + +**`identity_ref` drives the CR metadata, not a package name (Q3).** `identity_ref(agent_id) -> (namespace, name)` accepts a SPIFFE URI (`spiffe:///ns//sa/`) or a plain `/` clientId, validates both segments as DNS-1123 labels (`^[a-z0-9]([-a-z0-9]*[a-z0-9])?$`, ≤63 chars), and returns the `(namespace, name)` used for the CR's `metadata`. There is **no** fallback — a bare `github-agent` (no derivable namespace) or an invalid label raises `ValueError` (→ 400). This function replaces the former per-package slug: it feeds `metadata`, never a package name. + +> **Two identifiers, two layers (no contradiction).** UC-1 onboarding and the Trigger use the internal Keycloak **client UUID** (`service.id` / `Trigger.entity_id`) purely to *look up* a service in the IdP — that UUID **never reaches this writer**. What flows down the policy pipeline into `PolicyRule.scope.serviceId` / `Role.actorIds` and lands as `AgentPolicyModel.agent_id` is the **clientId** (the `/` / SPIFFE form), which `identity_ref` maps to the CR's `(namespace, name)`. The UUID→clientId resolution happens once, in the IdP Configuration Service, before the AgentPolicyModel is ever built. + +### Live plugin input shape (Q4) -> **Two identifiers, two layers (no contradiction).** UC-1 onboarding and the Trigger use the internal Keycloak **client UUID** (`service.id` / `Trigger.entity_id`) purely to *look up* a service in the IdP — that UUID **never reaches this writer**. What flows down the policy pipeline into `PolicyRule.scope.serviceId` / `Role.actorIds` and lands as `AgentPolicyModel.agent_id` is the **clientId** (the `{ns}/{name}` / SPIFFE form), which is what this writer slugifies for the Rego package name. The UUID→clientId resolution happens once, in the IdP Configuration Service, before the AgentPolicyModel is ever built. +The Rego packages evaluate the `input` document the live AuthBridge OPA plugin populates — never IDs-plus-roles supplied per request. The fields the packages read: -**Input is identifiers only.** Both packages receive an input document of **IDs**, never roles or scopes: inbound input is `{subject, source}`, outbound input is `{subject, target}` (`subject` is the end-user id, `source` the calling service id, `target` the called service id). Every role/scope mapping is therefore **embedded in the package itself**, and the `allow` logic resolves IDs → roles → scopes internally. Because no per-request scope is supplied, the decision is **coarse**: a principal passes when it has access to **at least one** relevant scope. +| Input field | Meaning | Tier | +|-------------|---------|------| +| `input.identity.subject` | The delegated end-user id (JWT `sub`) | inbound + outbound | +| `input.identity.client_id` | The calling client — the inbound source | inbound | +| `input.identity.service_id` | The downstream target audience the exchanged token was minted for — a **full SPIFFE id** | outbound | +| `input.mcp.params.name` | The **bare** invoked MCP tool name (e.g. `source-read`) | outbound | + +On the outbound leg there is no validated JWT; the plugin synthesizes `input.identity` from the token-exchange delegation hop. A **missing** `input.mcp.params.name` (e.g. a `tools/list` discovery request, which carries no tool name) or an **absent** `input.identity.service_id` matches nothing in the maps and is therefore **denied**. The generator embeds these symbols, derived from the `AgentPolicyModel`: -| Rego symbol | Source | Shape | -|-------------|--------|-------| -| `subject_roles` | `model.subject_roles` | subject id → `[role.name, …]` | -| `source_roles` | `model.source_roles` | source id → `[role.name, …]` | -| `agent_scopes` | `model.agent_scopes` | `[scope.name, …]` — **inbound package only** (the audience gate; outbound decisions do not consider the agent's own scopes) | -| `agent_roles` | `model.agent_roles` | `[role.name, …]` | -| `role_scopes` | grouped `model.inbound_rules` | role.name → `[scope.name, …]` (agent scopes granted per subject role) — **inbound package only** | -| `subject_role_scopes` | grouped `model.outbound_subject_rules` | role.name → `[scope.name, …]` (tool scopes granted per user role) — **outbound package only** | -| `agent_role_scopes` | grouped `model.outbound_rules` | role.name → `[scope.name, …]` (tool scopes per agent role) | -| `target_scopes` | `model.target_scopes` | target id → `[scope.name, …]` | +| Rego symbol | Source | Shape | De-prefixed? | +|-------------|--------|-------|--------------| +| `agent_scopes` | `model.agent_scopes` | `[scope.name, …]` — **inbound only** (the audience gate) | no — full scope names | +| `subject_roles` | `model.subject_roles` | subject id → `[role.name, …]` | n/a (roles) | +| `source_roles` | `model.source_roles` | source client id → `[role.name, …]` — **inbound only** | n/a (roles) | +| `role_scopes` | grouped `model.inbound_rules` | role → `[agent scope name, …]` — **inbound only** | no — full scope names | +| `agent_roles` | `model.agent_roles` | `[role.name, …]` — **outbound only** (informational) | n/a (roles) | +| `subject_role_scopes` | grouped `model.outbound_subject_rules` | user role → `[tool name, …]` — **outbound only** | **yes** — bare tool names | +| `agent_role_scopes` | grouped `model.outbound_rules` | agent role → `[tool name, …]` — **outbound only** (informational) | **yes** — bare tool names | +| `target_scopes` | `model.target_scopes` | full target service id → `[tool name, …]` — **outbound only** | **values yes, keys no** | + +De-prefixing (Q9) is **outbound-only**: provisioned scope names are prefixed with their owning workload (`github-tool.source-read`), but the value that arrives in `input.mcp.params.name` at runtime is the bare tool name (`source-read`), so the outbound map **values** are stripped of a leading `"."` (where `owner = identity_ref(scope.serviceId).name`). The **keys** of `target_scopes` stay the full target service id (they match `input.identity.service_id`). Inbound `agent_scopes` / `role_scopes` keep their **full** names — the inbound gate compares scopes internally, never against `input.mcp.params.name`. -### Inbound package: `authz.{agent_slug}.inbound` +### Inbound package: `authbridge.client.inbound.request` -Evaluated by the AuthBridge OPA plugin in the **inbound pipeline** — "who may call this agent". Input document: `{subject, source}` (IDs). **`subject` is mandatory; `source` is optional** (an absent source passes). A principal passes when it holds a role that grants at least one of the agent's own scopes (`agent_scopes`). +Evaluated by the AuthBridge OPA plugin in the **inbound pipeline** — "who may call this agent". `allow` requires `subject_ok` **and** `source_ok`. `subject_ok` passes when the subject (`input.identity.subject`) holds a role granting at least one of the agent's own `agent_scopes`. `source_ok` passes when (a) there is no calling `input.identity.client_id` (pure end-user traffic), (b) the `client_id` is one of the **platform bypass clients** — `rossoctl` by default, from `PLATFORM_SOURCE_CLIENTS` (Q5); this bypass is **mandatory**, since end-user traffic carries the platform client and would otherwise be denied — or (c) that client holds a role granting an agent scope. + +The block below is reproduced **verbatim** from `docs/examples/opa-team1-policy.yaml` (`inbound/request.rego`): ```rego -package authz.{agent_slug}.inbound +package authbridge.client.inbound.request +import rego.v1 + +agent_scopes := ["github-agent.issue_operations", "github-agent.source_operations"] -agent_scopes := ["{scope.name}", ...] # from agent_scopes +subject_roles := { + "dev-user": ["developer"], + "test-user": ["tester"], +} -subject_roles := { "{subject_id}": ["{role.name}", ...], ... } -source_roles := { "{source_id}": ["{role.name}", ...], ... } +source_roles := {} -role_scopes := { "{role.name}": ["{scope.name}", ...], ... } # from inbound_rules +role_scopes := { + "developer": ["github-agent.issue_operations", "github-agent.source_operations"], + "tester": ["github-agent.issue_operations"], +} subject_ok if { - some role in subject_roles[input.subject] + some role in subject_roles[input.identity.subject] some scope in role_scopes[role] scope in agent_scopes } -source_ok if { not input.source } # optional: absent source passes + +source_ok if { not input.identity.client_id } +source_ok if { input.identity.client_id == "rossoctl"} source_ok if { - some role in source_roles[input.source] + some role in source_roles[input.identity.client_id] some scope in role_scopes[role] scope in agent_scopes } @@ -146,36 +186,84 @@ default allow := false allow if { subject_ok; source_ok } ``` -### Outbound package: `authz.{agent_slug}.outbound` +### Outbound package: `authbridge.client.outbound.request` -Evaluated by the AuthBridge OPA plugin in the **outbound pipeline** — "what this agent may call". Input document: `{subject, target, function_name}` (IDs; `function_name` is the requested target scope). `allow` is a **per-scope AND** keyed on `input.function_name`, requiring **both** gates to pass on that same scope. The outbound **subject** gate is user→**tool** (distinct from the inbound user→agent gate): it passes iff the subject holds a role granted the **requested** `function_name` (via `subject_role_scopes`, grouped from `outbound_subject_rules`). The **capability** gate (`target_ok`) passes iff the requested `function_name` is one of the scopes the `target` accepts — `target_scopes[input.target]`, consumed **directly** (target id → scopes, not inverted). Because both gates test the *same* `function_name`, `allow` is a genuine per-scope intersection: a mismatch (user reaches scope A, agent reaches scope B) denies both. `agent_roles` / `agent_role_scopes` are still emitted (informational / debugging) but `allow` does **not** reference them — `target_scopes[input.target]` already *is* the per-scope capability gate. The inbound `role_scopes`/`agent_scopes` subject gate is **not** used here, and the outbound package does not emit `agent_scopes` at all: outbound decisions never consider the agent's own audience scopes. +Evaluated by the AuthBridge OPA plugin in the **outbound pipeline** — "what this agent may call", **per invoked tool**. `allow` is an AND on the **same** `input.mcp.params.name`: `subject_ok` (the delegated user's role admits the tool — `input.mcp.params.name in subject_role_scopes[role]`, de-prefixed values) AND `target_ok` (the target service, keyed by the full `input.identity.service_id`, admits the tool — `input.mcp.params.name in target_scopes[input.identity.service_id]`). `agent_roles` / `agent_role_scopes` are emitted for debugging but are **not** referenced by `allow` — `target_scopes[input.identity.service_id]` already *is* the per-scope capability gate. This package emits neither `agent_scopes` nor the inbound `role_scopes` gate: outbound decisions never consider the agent's own audience scopes. -```rego -package authz.{agent_slug}.outbound +The block below is reproduced **verbatim** from `docs/examples/opa-team1-policy.yaml` (`outbound/request.rego`): -agent_roles := ["{role.name}", ...] # from agent_roles - -subject_roles := { "{subject_id}": ["{role.name}", ...], ... } - -subject_role_scopes := { "{role.name}": ["{scope.name}", ...], ... } # from outbound_subject_rules (user role → tool scopes) -agent_role_scopes := { "{role.name}": ["{scope.name}", ...], ... } # from outbound_rules (agent role → tool scopes) -target_scopes := { "{target_id}": ["{scope.name}", ...], ... } # from target_scopes +```rego +package authbridge.client.outbound.request +import rego.v1 -# user may reach the tool: holds a role granted the REQUESTED scope (input.function_name) +agent_roles := ["github-agent.issue_operations", "github-agent.source_operations"] +subject_roles := { + "dev-user": ["developer"], + "test-user": ["tester"] +} +# The deployed github-tool (aiac/demo/assets/tools/github_tool) exposes +# exactly four MCP tools — source-read, source-write, issues-read, +# issues-write — one per skill. These names ARE the values that arrive in +# input.mcp.params.name when a specific tool is invoked, so the maps +# below key on them. +subject_role_scopes := { + "developer": ["issues-read", "source-write", "source-read"], + "tester": ["issues-read", "issues-write"], +} +agent_role_scopes := { + "github-agent.issue_operations": ["issues-read", "issues-write"], + "github-agent.source_operations": ["source-write", "source-read"], +} +target_scopes := { + "spiffe://localtest.me/ns/team1/sa/github-tool": ["source-read", "source-write", "issues-read", "issues-write"], +} subject_ok if { - some role in subject_roles[input.subject] - input.function_name in subject_role_scopes[role] + some role in subject_roles[input.identity.subject] + input.mcp.params.name in subject_role_scopes[role] } -# agent may reach the tool: the requested scope is one the target accepts (direct, per-scope) target_ok if { - input.function_name in target_scopes[input.target] + input.mcp.params.name in target_scopes[input.identity.service_id] } - default allow := false allow if { subject_ok; target_ok } ``` -A worked example (agent `github-agent`, users `developer`/`tester`, tool `github-tool`) is maintained alongside the tests. +A worked example (agent `github-agent`, users `developer`/`tester`, tool `github-tool`) is maintained alongside the tests, and mirrored in `docs/examples/opa-team1-policy.yaml`. + +### `AuthorizationPolicy` Custom Resource (Q6) + +The two packages become the `policies[]` of a per-agent CR. The writer builds the body in `_build_cr`, keyed on `identity_ref(agent_id)`: + +```yaml +apiVersion: agent.rossoctl.dev/v1alpha1 +kind: AuthorizationPolicy +metadata: + name: github-agent # identity_ref(agent_id).name + namespace: team1 # identity_ref(agent_id).namespace + labels: + app.kubernetes.io/managed-by: aiac-pdp-policy-writer +spec: + scope: client + clientID: "github-agent" # display / print-column only + policies: + - path: "inbound/request.rego" + content: | + # ... generate_inbound_rego(model) ... + - path: "outbound/request.rego" + content: | + # ... generate_outbound_rego(model) ... +``` + +- **Written via server-side apply.** `_upsert_agent` calls `patch_namespaced_custom_object` with `_content_type="application/apply-patch+yaml"`, `field_manager="aiac-pdp-policy-writer"`, `force=True` — create-or-update in one idempotent call. +- **`metadata.labels["app.kubernetes.io/managed-by"] = "aiac-pdp-policy-writer"`** marks every CR the writer owns; `_delete_all` selects on it. +- **`spec.clientID` is display-only** — bundle-service looks the CR up by `metadata.name` + `metadata.namespace` (matched against the SPIFFE ServiceAccount segment), **never** by `clientID`. It must nonetheless be a valid DNS label (no `spiffe://`, no `/`); the writer sets it to the `name`. +- **Delete-by-label vs single-agent delete.** `_delete_all` (`DELETE /policy`) lists every CR carrying the managed-by label **cluster-wide** and deletes each; `_delete_agent` (`DELETE /policy/agents/{agent_id}`) deletes the single `(name, namespace)` from `identity_ref` (a k8s 404 is treated as success — idempotent). + +### Both tiers always emitted; delete = off-boarding (Q7) + +Every upsert writes **both** tiers, each with `default allow := false`. The shipped global combiner allows a tier only when `ns_ok AND client_ok`, where `client_ok` comes from this package's `allow` — **except** that a tier with **no** CR falls back to allow via the combiner's `client_ok if not data.authbridge.client.` rule. + +Consequently **deleting a CR is off-boarding, not lockdown**: removing an agent's CR returns that agent to the combiner's **allow-fallback**, it does *not* deny the agent. To actually block an agent while keeping it in the system, **upsert** a CR with empty maps (so `allow` stays `false`) rather than deleting it. --- @@ -220,16 +308,19 @@ apply_policy(full_model) ## Configuration -| Variable | Required | Description | -|----------|----------|-------------| -| `AUTHORIZATION_POLICY_NAME` | TBD | Name of the `AuthorizationPolicy` CR to patch | -| `AUTHORIZATION_POLICY_NAMESPACE` | TBD | Namespace of the `AuthorizationPolicy` CR | +| Variable | Required | Default | Description | +|----------|----------|---------|-------------| +| `PLATFORM_SOURCE_CLIENTS` | No | `rossoctl` | Comma-separated platform bypass clients, sourced from the `aiac-pdp-config` ConfigMap. Drives the inbound package's `source_ok if { input.identity.client_id == "" }` bypass rules (Q5). Blanks are dropped; an unset or all-blank value falls back to `rossoctl` (dropping the bypass would deny end-user traffic, which carries the platform client). | +| `POLICY_WRITER_DUMP_REGO` | No | off | When truthy (`1`/`true`/`yes`/`on`) enables the **additive** local rego dump (see below). Never gates the CR write. | +| `REGO_OUTPUT_DIR` | No | `/rego` | Destination for the additive dump — only consulted when `POLICY_WRITER_DUMP_REGO` is on. | + +There are **no** CR-name or CR-namespace env vars — CR coordinates are derived per-agent from `identity_ref(agent_id)`, and the group/version/plural/field-manager/label are code constants (Q8). -Authentication to the Kubernetes API: in-cluster service account (auto-detected by the `kubernetes` Python client). The pod's `ServiceAccount` must be bound to a `ClusterRole` granting `get`/`patch`/`update` on `AuthorizationPolicy` resources. The `ServiceAccount`, `ClusterRole`, and `ClusterRoleBinding` are declared in `pdp-interface-deployment.yaml`. +**Auth model.** The writer authenticates to the Kubernetes API as an **in-cluster ServiceAccount** (`aiac-pdp-policy-writer`), bound cluster-wide to a `ClusterRole` granting `get`, `list`, `create`, `update`, `patch`, `delete` on `authorizationpolicies.agent.rossoctl.dev` — **no `watch`** (the writer only creates/patches; bundle-service polls). The `ServiceAccount`, `ClusterRole`, and `ClusterRoleBinding` are declared in `k8s/pdp-interface-deployment.yaml`. The binding is cluster-scoped (not a namespaced `RoleBinding`) because the writer creates CRs in arbitrary workload namespaces (`team1`, …), derived from each agent's `identity_ref` namespace. For local development, the `kubernetes` client falls back to `~/.kube/config` automatically. -For local development, the `kubernetes` client falls back to `~/.kube/config` automatically. +## Always-on CR write + additive debug dump -> **Note:** `AuthorizationPolicy` CR schema and ConfigMap source for env vars are TBD. +The **CR server-side-apply is always active** — it is never gated by an env var. The former filesystem-stub behaviour survives **only** as an additive debug/test aid, toggled by `POLICY_WRITER_DUMP_REGO` (default off). When on, `_upsert_agent` **also** writes the same rego to `///inbound/request.rego` and `///outbound/request.rego`, mirroring the CR `policies[].path` so the on-disk output equals the CR content; `_delete_agent` / `_delete_all` clear the corresponding dumped tree. The dump is **never** a substitute for, or a switch away from, the CR write — production runs with it off (`k8s/pdp-interface-deployment.yaml` sets no `POLICY_WRITER_DUMP_REGO`). A dump `OSError` maps to 502, so a broken debug mount surfaces rather than silently dropping files. --- @@ -240,7 +331,7 @@ For local development, the `kubernetes` client falls back to `~/.kube/config` au - Bind: `0.0.0.0:7072` - Base image: `python:3.12-slim` - Kubernetes ClusterIP Service: `aiac-pdp-policy-service:7072` -- Deployment: co-located with IdP Configuration Service as a container in the **Kagenti Interface Pod** (`pdp-interface-deployment.yaml`) +- Deployment: co-located with IdP Configuration Service as a container in the **Rossoctl Interface Pod** (`pdp-interface-deployment.yaml`) --- @@ -266,7 +357,8 @@ aiac/src/aiac/pdp/service/ ├── __init__.py ├── Dockerfile ├── requirements.txt - └── main.py + ├── rego.py # identity_ref + generate_inbound_rego + generate_outbound_rego + └── main.py # the always-on CR writer (with optional additive dump) aiac/src/aiac/pdp/policy/ ├── __init__.py @@ -276,6 +368,8 @@ aiac/src/aiac/pdp/policy/ # (models now imported from aiac.policy.model.models) ``` +There is **no** `stub.py` and no separate filesystem-writer module: `main.py` is the single, always-on CR writer (rego rendering lives in `rego.py`; the optional dump is a branch inside `main.py`, not a distinct mode). + Build command: ```bash docker build -f aiac/src/aiac/pdp/service/policy/opa/Dockerfile \ @@ -286,17 +380,17 @@ docker build -f aiac/src/aiac/pdp/service/policy/opa/Dockerfile \ ## `main.py` behaviour notes -- Load Kubernetes in-cluster config at startup via `kubernetes.config.load_incluster_config()`; fall back to `kubernetes.config.load_kube_config()` for local development. -- Instantiate a `kubernetes.client.CustomObjectsApi` for all CR operations. -- `_slugify(agent_id: str) -> str`: extract `{namespace}/{name}` from a SPIFFE URI (or use the plain `{ns}/{name}` clientId as-is), then collapse every non-alphanumeric run to `_` and lowercase — produces a valid Rego package name segment, short and SPIRE-independent. -- `_generate_inbound_rego(model: AgentPolicyModel) -> str`: render the inbound Rego package string. Embeds `agent_scopes`, `subject_roles`, `source_roles`, and a `role_scopes` map (grouping `inbound_rules` by role → agent scope names); emits `subject_ok` (mandatory) and `source_ok` (optional — an absent `input.source` passes); `allow if { subject_ok; source_ok }`. -- `_generate_outbound_rego(model: AgentPolicyModel) -> str`: render the outbound Rego package string. Embeds `agent_roles`, `subject_roles`, `subject_role_scopes` (from `outbound_subject_rules`), `agent_role_scopes` (from `outbound_rules`), and `target_scopes` (consumed directly, target id → scopes — **no inversion**); emits a user→tool `subject_ok` (matching `input.function_name in subject_role_scopes[role]`, **not** the inbound `role_scopes`/`agent_scopes`) and a capability `target_ok` (matching `input.function_name in target_scopes[input.target]`); `allow if { subject_ok; target_ok }` — a per-scope AND on the same `input.function_name`. Neither the inbound `role_scopes` map nor `agent_scopes` is embedded in the outbound package — outbound decisions never consider the agent's own audience scopes. -- `_upsert_agent(agent_id: str, inbound_rego: str, outbound_rego: str)`: patch the `AuthorizationPolicy` CR to upsert the two packages for `agent_id`. Schema TBD. -- `_delete_agent(agent_id: str)`: patch the CR to remove all packages for `agent_id`. -- `_delete_all()`: patch the CR to remove all packages. -- `POST /policy`: iterate `model.agents`; for each call `_generate_inbound_rego` + `_generate_outbound_rego` + `_upsert_agent`; return `Response(status_code=204)`. -- `POST /policy/agents/{agent_id}`: call `_generate_inbound_rego` + `_generate_outbound_rego` + `_upsert_agent`; return `Response(status_code=204)`. -- `DELETE /policy/agents/{agent_id}`: call `_delete_agent(agent_id)`; return `Response(status_code=204)`. -- `DELETE /policy`: call `_delete_all()`; return `Response(status_code=204)`. -- On Kubernetes API error, return HTTP 502 with `{"error": str(e)}`. -- `GET /health`: attempt to `get` the `AuthorizationPolicy` CR; return `200 {"status": "ok"}` on success, `503` on failure. +- **Kube config at import:** `_load_kube_config()` tries `config.load_incluster_config()`, falling back to `config.load_kube_config()` (local dev). Both failing is non-fatal — the module stays importable and API calls surface as 502/503 until real config exists. A module-level `client.CustomObjectsApi` handles all CR operations. +- **Code constants (never env vars):** `_GROUP = "agent.rossoctl.dev"`, `_VERSION = "v1alpha1"`, `_PLURAL = "authorizationpolicies"`, `_MANAGED_BY_LABEL = {"app.kubernetes.io/managed-by": "aiac-pdp-policy-writer"}`, `_FIELD_MANAGER = "aiac-pdp-policy-writer"` (Q8). +- **`identity_ref(agent_id) -> (namespace, name)`** (in `rego.py`): SPIFFE or `/` → DNS-1123-validated `(namespace, name)`; raises `ValueError` (→ 400) when no namespace is derivable or a segment is an invalid label — no fallback. +- **`generate_inbound_rego(model, platform_clients)` / `generate_outbound_rego(model)`** (in `rego.py`): render the two fixed-package strings. The inbound generator emits one `source_ok` bypass rule per `platform_clients` entry (plus the no-`client_id` and role-based rules); the outbound generator de-prefixes its map values. +- **`_build_cr(model)`:** assemble the CR body — `metadata.name`/`.namespace` from `identity_ref`, the managed-by label, `spec.scope: client`, `spec.clientID` = the display name, and `policies[]` = the two rendered packages. Raises `ValueError` (via `identity_ref`) on a malformed `agent_id`. +- **`_upsert_agent(model)`:** server-side apply via `patch_namespaced_custom_object` (`_content_type="application/apply-patch+yaml"`, `field_manager=_FIELD_MANAGER`, `force=True`); then, if the dump is enabled, `_dump_cr`. +- **`_delete_agent(agent_id)`:** `delete_namespaced_custom_object` for the single `(name, namespace)`; a k8s **404 is swallowed** (idempotent → 204); then dump-clear the agent's tree if enabled. +- **`_delete_all()`:** `list_cluster_custom_object` filtered by the managed-by label selector, then delete each item (per-item 404 tolerated for concurrent-delete races); then clear the dumped tree if enabled. +- **`_run_write(op)`** maps outcomes to responses: success → **204**; `ValueError` → **400** `{"error": …}`; `ApiException` → **502** `{"error": …}`; `OSError` (the additive dump) → **502**. +- **`POST /policy`:** iterate `policy.agents`, `_upsert_agent` each; a malformed `agent_id` aborts the batch with a 400 naming it (agents already applied before that point stay written — no rollback). +- **`POST /policy/agents/{agent_id}`:** `_upsert_agent(model)`. +- **`DELETE /policy/agents/{agent_id}`:** `_delete_agent(agent_id)`. +- **`DELETE /policy`:** `_delete_all()`. +- **`GET /health`:** a bounded `list_cluster_custom_object(..., limit=1)` — a successful list (even empty) → `200 {"status": "ok"}`; any failure (unreachable API, RBAC-forbidden, CRD not served) → `503 {"status": "unavailable", "error": …}`. The dump dir is not part of this signal. diff --git a/aiac/docs/specs/components/policy-computation-engine.md b/aiac/docs/specs/components/policy-computation-engine.md index 87a4924dd..42800f6c6 100644 --- a/aiac/docs/specs/components/policy-computation-engine.md +++ b/aiac/docs/specs/components/policy-computation-engine.md @@ -81,7 +81,7 @@ def compute_and_apply(rules: list[PolicyRule], override: bool = False) -> None def decommission(service_id: str) -> None # service_id = clientId (SPM key), not the Keycloak UUID ``` -- **No return value; failures propagate:** on success the caller receives no return value. Both functions log exceptions and **re-raise** them — a failure in IdP resolution, Policy Store I/O, or PDP Policy Writer push surfaces to the caller (the Controller returns HTTP 500; a NATS consumer nacks → at-least-once redelivery) rather than being silently swallowed while nothing is applied. +- **No return value; failures propagate:** on success the caller receives no return value. Both functions log exceptions and **re-raise** them — a failure in IdP resolution, Policy Model Store I/O, or PDP Policy Writer push surfaces to the caller (the Controller returns HTTP 500; a NATS consumer nacks → at-least-once redelivery) rather than being silently swallowed while nothing is applied. - **`override`:** selects the merge mode (see [Merge Semantics](#merge-semantics)). `False` (default) appends additively at the SPM layer; `True` authoritatively replaces every input role's mappings **across all SPMs** (role-level revocation). Set by the caller (the Controller) from the producing UC's choice — UC1 = `False`, UC3 = `True`, UC2 Rebuild = `True`, UC2 Build = TBD. - **`decommission`:** the authoritative service **offboard** — tears down a decommissioned service's entire policy footprint (see [Decommission (service offboard)](#decommission-service-offboard)). Keyed by the **clientId (SPM key)**, since an offboarded client is gone from `get_services()` and its UUID can no longer be resolved. - Import path: `from aiac.policy.computation.engine import compute_and_apply, decommission` @@ -198,7 +198,7 @@ Steps: |--------|---------| | `aiac.policy.model` | `PolicyRule`, `ServicePolicyModel`, `AgentPolicyModel`, `PolicyModel` | | `aiac.idp.configuration` | `Configuration.get_services` — the **only** runtime IdP read (catalog: `service_type` + own roles/scopes for the P2 seed) | -| `aiac.policy.store.library` | `get_service_policy` / `get_service_policy_by_scope` (fetch SPM), `get_service_policies_by_role` (SPMs containing a role — override purge + outbound derivation), `apply_service_policy` (persist SPM), `delete_service_policy` (offboard) | +| `aiac.policy.model_store.library` | `get_service_policy` / `get_service_policy_by_scope` (fetch SPM), `get_service_policies_by_role` (SPMs containing a role — override purge + outbound derivation), `apply_service_policy` (persist SPM), `delete_service_policy` (offboard) | | `aiac.pdp.policy.library` | `apply_policy` — partial-upsert derived APMs to OPA; `delete_agent_policy` — remove an offboarded agent's APM/Rego | Note: the PCE no longer calls `get_services_by_role` / `get_services_by_scope` / `get_subjects_by_role` at routing or classification time — those facts arrive on the rules (input contract) and derivation reads SPMs. The single IdP read is `get_services()` for the identity seed. @@ -206,7 +206,7 @@ Note: the PCE no longer calls `get_services_by_role` / `get_services_by_scope` / ### Not Called By - PDP Policy Writer — the downstream consumer, not a caller. -- Policy Store — pure CRUD, no computation. +- Policy Model Store — pure CRUD, no computation. - IdP Configuration Service — no awareness of this module. ### Not Responsible For @@ -219,12 +219,12 @@ Note: the PCE no longer calls `get_services_by_role` / `get_services_by_scope` / ## Testing Decisions -Good tests assert external behavior — what the engine writes to the Policy Store (SPMs) and pushes to the PDP (derived APMs) — not internal merge logic directly. +Good tests assert external behavior — what the engine writes to the Policy Model Store (SPMs) and pushes to the PDP (derived APMs) — not internal merge logic directly. **Seam:** mock all downstream dependencies at their module-level import boundary: - `aiac.idp.configuration` — mock `Configuration.get_services` (the catalog: `service_type` + each service's own roles/scopes for the P2 seed). -- `aiac.policy.store.library` — mock `get_service_policy` / `get_service_policy_by_scope`, `get_service_policies_by_role`, `apply_service_policy`, `delete_service_policy`. +- `aiac.policy.model_store.library` — mock `get_service_policy` / `get_service_policy_by_scope`, `get_service_policies_by_role`, `apply_service_policy`, `delete_service_policy`. - `aiac.pdp.policy.library` — mock `apply_policy`, `delete_agent_policy`. **Un-freeze `test/policy/computation/`.** These tests were excluded (frozen imports caused collection errors). With the SPM redesign landed, un-freeze the directory so the suite runs under `pytest test/ -m "not integration"`. diff --git a/aiac/docs/specs/components/policy-guardrails-agent-policy-hygiene.md b/aiac/docs/specs/components/policy-guardrails-agent-policy-hygiene.md new file mode 100644 index 000000000..69f87a7df --- /dev/null +++ b/aiac/docs/specs/components/policy-guardrails-agent-policy-hygiene.md @@ -0,0 +1,130 @@ +# Component Sub-PRD: Policy Hygiene + +> **Depends on:** [policy-guardrails-agent.md](policy-guardrails-agent.md) — module placement, the verification contract, the LangGraph design, the shared findings/verdict model, and Configuration. This sub-PRD specifies **Check 1 (Policy hygiene)** of the `policy` API family only; it does not restate the parent's architecture. + +## Scope + +Policy hygiene is the **intrinsic, single-document** check the Policy Guardrails Agent runs against one incoming `aiac-policies` document. It judges whether the document is a well-formed, actionable, internally consistent access-control statement — independent of any other document. + +It runs **identically for both `replace` and `update`** operations (hygiene is operation-independent; only Check 2, contradiction, depends on operation). It is the first node in the parent's `StateGraph`; a blocking hygiene finding short-circuits straight to `verdict`, so contradiction is never evaluated on malformed text. + +### Intrinsic-only boundary (fixed constraint) + +The agent's configuration exposes only `CHROMA_URL` and the LLM trio — **no IdP access**. Hygiene therefore judges **intrinsically, from the document text alone**: + +- It does **not** read ChromaDB (that is Check 2's `corpus_fetch`). +- It does **not** resolve subjects/roles/services/scopes against Keycloak. "Actionable / translatable" means the text *names a plausible* subject/role → service/scope/action, **not** that those identifiers actually exist in the IdP. Whether the referenced entities resolve is the downstream AIAC Agent's concern, not hygiene's. + +### Deferred (out of scope this increment) + +- **Prompt-injection / adversarial-content screening** — always a separate concern from hygiene (see parent). +- **Corpus contradiction and intra-batch contradiction** — Check 2 and a documented parent deferral, respectively. + +## The three facets + +Hygiene evaluates three facets of a document's intrinsic quality. Each facet maps to one or more finding codes (next section): + +1. **On-topic & well-formed** — the document must actually express access-control intent (some subject/role → service/scope/action). Empty, gibberish, or off-topic text fails. +2. **Actionable / translatable** — the statement must be specific enough for the downstream AIAC Agent LLM to emit Rego: it references resolvable-*looking* roles/services/scopes/actions and avoids hopeless vagueness. +3. **Internally consistent & clear** — the document does not contradict *itself* and is not so ambiguous it would yield nondeterministic Rego. (Self-contradiction here is distinct from corpus contradiction, which is Check 2.) + +## Finding taxonomy + +Hygiene emits findings from a **closed vocabulary of codes**. Each code has a **fixed severity** — the degree distinction (e.g. hopeless vs. minor vagueness) is encoded by *which code* is chosen, not by a per-finding severity. This makes the verdict deterministic given the set of codes. + +| Code | Facet | Severity | Definition | Example | +|------|-------|----------|------------|---------| +| `NOT_A_POLICY` | on-topic / well-formed | **blocking** | Empty, whitespace-only, gibberish, or text that expresses no access-control intent (off-topic). | *"The cafeteria menu changes every Friday."* (off-topic; no subject → action) | +| `UNTRANSLATABLE` | actionable | **blocking** | Expresses intent but is too vague to yield Rego — no resolvable subject/role or service/scope/action. | *"Systems should be secure and do the right thing."* | +| `SELF_CONTRADICTION` | consistent | **blocking** | The document contradicts *itself* (two statements in the same document cannot both hold). | *"Admins may delete any record. Admins may never delete records."* | +| `AMBIGUOUS` | consistent | **blocking** | Admits multiple irreconcilable readings, such that translation to Rego would be nondeterministic. | *"Users in group A or B with the admin role may access billing."* (does "admin role" qualify both groups, or only B?) | +| `TOO_LARGE` | operational | **blocking** | Document exceeds `GUARDRAILS_MAX_DOC_CHARS`. An operational bound, not a semantic judgement — the text may well be valid policy that is simply too large to reason over reliably. | — (any document longer than the char ceiling; no snippet applies) | +| `UNDERSPECIFIED` | actionable | advisory | Minor vagueness; still translatable but could be tightened (e.g. an unqualified action verb). | *"Support staff can update tickets."* (translatable, but the "update" scope is loose) | +| `REDUNDANCY` | consistent | advisory | The document repeats the same access-control statement within itself. | *"Editors may publish articles. Editors are allowed to publish articles."* | +| `STYLE` | cross-cutting | advisory | Wording / formatting nits that do not affect meaning. | *"editors CAN publsih articles!!!"* (typo / formatting; meaning intact) | + +**LLM-emittable vs. guard-only.** The LLM's structured output is constrained to the **seven semantic codes** (all except `TOO_LARGE`). `TOO_LARGE` is emitted only by the pre-LLM guard and never appears in LLM output. `NOT_A_POLICY` is emitted by *both* paths — the guard for empty/whitespace input, the LLM for gibberish/off-topic. + +> **Note.** The `Example` column gives one illustrative snippet per code to sharpen its definition (`TOO_LARGE` is a size bound, so no snippet applies). These are definitional anchors, **not** a calibration corpus — a larger good/bad example set for tuning the blocking/advisory threshold remains a separate rubric/eval artifact (see [Open decisions](#open-decisions-tbd)). + +## Severity and verdict + +Severity is **derived by the agent from the code**, not emitted by the LLM. The LLM emits only `{code, message}`; the agent looks up severity from the fixed table above. This prevents the LLM from contradicting the taxonomy (e.g. marking a `STYLE` finding blocking). + +- **Verdict = reject** iff at least one finding has a derived severity of **blocking**; otherwise **accept**. (This is the parent's rule; hygiene simply feeds blocking findings into it.) +- **Advisory findings never block.** They are returned alongside the verdict so the operator sees non-fatal concerns, but they do not fail the request — important given the parent's all-or-nothing semantics, where one false blocking finding fails the operator's whole ingest request. +- **Emit-all.** In its single call the LLM returns **all** applicable findings, including advisories, even when a blocking finding is also present. It does not stop at the first problem. + +## Processing flow + +Hygiene is the parent graph's `hygiene` node. Internally it is two stages: + +1. **Pre-LLM guard (deterministic, no LLM call):** + - Empty or whitespace-only text → single `NOT_A_POLICY` finding; return immediately. + - Text length > `GUARDRAILS_MAX_DOC_CHARS` → single `TOO_LARGE` finding; return immediately. + - These paths are reproducible and cost nothing, and (being blocking) trigger the parent's short-circuit to `verdict`. +2. **LLM call (one call, all three facets):** the document text is judged against the three facets in a single structured-output call. The LLM returns zero or more `{code, message}` findings drawn from the seven semantic codes. The agent then derives severity per finding and hands the findings to the parent's verdict aggregation. + +The hygiene node never populates `related_doc_id` — that field is reserved for Check 2 (contradiction), which names a conflicting corpus document. Hygiene is intrinsic and has no peer document to reference. + +Determinism: the hygiene LLM call is issued at a **low (pinned) temperature** to keep verdicts as reproducible as an LLM boundary allows. + +## Structured-output contract + +The LLM call uses the LangChain structured-output surface (parent: `with_structured_output`) so findings are validated at the call boundary. The model the LLM must satisfy: + +**`HygieneFinding` (LLM-emitted):** + +| Field | Type | Notes | +|-------|------|-------| +| `code` | enum | One of the **seven** semantic codes (`TOO_LARGE` excluded — guard-only). | +| `message` | str | Specific, actionable, one-sentence explanation. Should point at the offending aspect so an operator can fix the document. | + +**`HygieneResult` (LLM-emitted):** + +| Field | Type | Notes | +|-------|------|-------| +| `findings` | `list[HygieneFinding]` | All applicable findings; empty list ⇒ the document is clean. | + +The agent maps each `HygieneFinding` into the parent's shared finding shape by adding the fixed fields: `check = "hygiene"`, `severity` (derived from `code`), and the LLM's `message`. `related_doc_id` is omitted. + +## Prompt responsibilities + +(Literal prompt text is an implementation artifact, out of scope for this spec — see the parent's no-prompt-text convention.) The hygiene system prompt must convey: + +- The agent's role: judge one candidate `aiac-policies` document for intrinsic quality only. +- The three facets and the seven semantic codes with their definitions and fixed severities. +- The instruction to emit **all** applicable findings (blocking and advisory together), or an empty list when clean. +- The structured-output shape (`HygieneResult`). +- The intrinsic-only boundary: judge from the text alone; do not assume access to a corpus or to the IdP; do not require that named roles/scopes actually exist — only that they are named plausibly. + +Few-shot examples and the exact blocking/advisory boundary phrasing are calibration concerns (see [Open decisions](#open-decisions-tbd)). + +## Configuration + +Adds one variable to the Policy Guardrails Agent's configuration (parent Configuration table), alongside the existing `GUARDRAILS_MAX_CORPUS_DOCS`: + +| Variable | Default | Source | Purpose | +|----------|---------|--------|---------| +| `GUARDRAILS_MAX_DOC_CHARS` | `20000` | ConfigMap | Upper bound on a single document's character length. Exceeding it yields a deterministic `TOO_LARGE` rejection with no LLM call. | + +`LLM_BASE_URL` / `LLM_MODEL` / `LLM_API_KEY` are reused from the parent (the same trio the AIAC Agent uses); hygiene requires no additional LLM configuration. + +## Testing decisions + +**Seams:** the deterministic guard and the severity-derivation/verdict aggregation need **no LLM** and are unit-testable directly; the LLM call is tested with a mocked structured-output response. + +Key behaviors to assert: + +- **Guard:** empty and whitespace-only text yield exactly one `NOT_A_POLICY` finding with **no LLM call**; text over `GUARDRAILS_MAX_DOC_CHARS` yields exactly one `TOO_LARGE` finding with no LLM call. +- **Severity derivation:** each code maps to its fixed severity; the LLM's output carries no severity and cannot alter it. A `STYLE`/`REDUNDANCY`/`UNDERSPECIFIED` finding never produces a reject; any of the five blocking codes does. +- **Verdict aggregation:** verdict = reject iff ≥1 blocking finding; advisories alone ⇒ accept; empty findings ⇒ accept. +- **Emit-all:** a mocked response mixing blocking and advisory findings preserves all of them in the result (advisories are not dropped when a blocking finding is present). +- **Contract:** LLM output constrained to the seven semantic codes; `TOO_LARGE` never appears in LLM output; hygiene findings never carry `related_doc_id`. +- **Operation independence:** hygiene produces identical findings for `replace` and `update` given the same text. + +## Open decisions (TBD) + +1. **Strictness calibration** — how conservative the prompt is about emitting *blocking* vs. *advisory* findings (parent open-decision #2). This sub-PRD is the home for that decision but does not yet resolve it; the code→severity table fixes the *mechanics*, while the LLM's threshold for choosing, e.g., `UNTRANSLATABLE` (blocking) over `UNDERSPECIFIED` (advisory) is deferred to prompt-tuning against real examples. +2. **Calibration corpus** — a larger good/bad example set (beyond the one-per-code snippets in the taxonomy table) for tuning the blocking/advisory threshold. Deferred; this is a separate rubric/eval artifact, not this design spec. +3. **`GUARDRAILS_MAX_DOC_CHARS` default** — `20000` is a starting recommendation; the right ceiling depends on the chosen model's context budget and realistic policy-document sizes. diff --git a/aiac/docs/specs/components/policy-guardrails-agent.md b/aiac/docs/specs/components/policy-guardrails-agent.md new file mode 100644 index 000000000..d98f169bf --- /dev/null +++ b/aiac/docs/specs/components/policy-guardrails-agent.md @@ -0,0 +1,138 @@ +# Component PRD: Policy Guardrails Agent + +## Description + +A FastAPI verification service co-located with ChromaDB and the RAG Ingest Service in the RAG Pod. It sits between the RAG Ingest Service and ChromaDB: before any document from an ingest request is written, the RAG Ingest Service calls the Policy Guardrails Agent to obtain a verdict on that document. It is reachable only on the RAG Pod's loopback network (`localhost:7075`) — it is not exposed on the `aiac-rag-service` ClusterIP Service, so the RAG Ingest Service is structurally the only caller. + +The agent evaluates one document per call. It may query the co-located ChromaDB instance for evaluation context, including the current (pre-update) version of the same `doc_id` when one exists. + +**One service, two API families.** The module is a single container / image (`aiac-policy-guardrails`) on a single port (`:7075`), reached through a single `AIAC_GUARDRAILS_URL`. It exposes two independently-developed verification API families — one for the `policy` collection (`aiac-policies`) and one for the `domain-knowledge` collection (`aiac-domain-knowledge`). The RAG Ingest Service selects the family by collection slug. **This spec defines the `policy` API only**; the `domain-knowledge` API is specced independently later and mirrors the same wiring and verdict conventions. + +For the `policy` collection the agent performs two checks — **policy hygiene** and **contradiction against the existing corpus** — detailed under [Responsibilities and check set](#responsibilities-and-check-set). Some aspects of the surface remain unresolved — see [Open decisions (TBD)](#open-decisions-tbd). This spec fixes the agent's place in the architecture, its `policy`-family responsibilities and internal design, and its contract obligations with the RAG Ingest Service. + +## Endpoints + +**Route paths and wire-schema TBD** — see [Open decisions (TBD)](#open-decisions-tbd). The verification route path and the exact JSON field names of the request/verdict payloads are unresolved. What the request **must convey** is fixed, however (see the contradiction decision below): **operation** (`replace` | `update`), **collection**, **doc_id**, and the document **text**. A `/health` endpoint is expected, following the convention used by every other AIAC service. + +## Verification contract + +These behaviors are fixed regardless of what the endpoint surface ends up looking like: + +- Invoked by the RAG Ingest Service on all 12 write endpoints (`replace` and `update`, each across `text`/`file`/`url`) for every collection slug in `AIAC_RAG_COLLECTIONS`. `DELETE /ingest/{collection}/{doc_id}` is exempt — removal introduces no new text to verify. +- Called once per document. A multi-document request (a multi-doc `replace` body, or a multipart `/file` upload with several files) results in one call per document. +- **Pre-flight**: every document in a request is verified before the RAG Ingest Service makes any ChromaDB mutation for that request. +- **All-or-nothing**: if any document in a request is rejected, the whole ingest request fails and nothing is written — the collection is left exactly as it was. This preserves the RAG Ingest Service's existing collection-level atomicity guarantee for `replace`. +- **Fail-closed**: if the agent is unreachable, times out, or errors, the RAG Ingest Service treats this as a failure and writes nothing. `AIAC_GUARDRAILS_ENABLED` (on the RAG Ingest Service) is the explicit operator off-switch — with it set to `false` the RAG Ingest Service skips verification entirely. This keeps "guardrails disabled" distinguishable from "guardrails enabled but broken." +- No Event Broker interaction. The agent neither publishes nor consumes NATS subjects. The RAG Ingest Service's existing `aiac.apply.policy.build` publish behavior is unchanged: a fail-closed rejection means the ingest request never succeeds, so the build event is simply never published for that request. + +## Responsibilities and check set + +The `policy` API runs two checks against a single incoming `aiac-policies` document. Both are LLM-backed (hygiene and contradiction over natural-language policy are inherently semantic). + +### Check 1 — Policy hygiene (intrinsic; single document, no corpus) + +> **Detailed design:** [policy-guardrails-agent-policy-hygiene.md](policy-guardrails-agent-policy-hygiene.md) — the finding-code taxonomy, severity derivation, pre-LLM guards, and structured-output contract for this check. + +Three facets of the document's intrinsic quality: + +- **On-topic & well-formed** — the document must actually express access-control intent (some subject/role → service/scope/action). Empty, gibberish, or off-topic text is rejected. +- **Actionable / translatable** — the statement must be specific enough for the downstream AIAC Agent LLM to emit Rego: it references resolvable roles/services/scopes/actions and avoids hopeless vagueness ("be secure", "do the right thing"). +- **Internally consistent & clear** — the document does not contradict *itself* and is not so ambiguous it would yield nondeterministic Rego. (Self-contradiction; distinct from corpus contradiction below.) + +Prompt-injection / adversarial-content screening is **out of scope for this increment** (deferred; it was always a separate concern from hygiene). + +### Check 2 — Contradiction against the existing corpus (relational) + +Whether the incoming document conflicts with policy already in the collection. The baseline is **the persistent corpus only** — what will still exist after the write: + +- **`update`** → the incoming document is compared against the persistent ChromaDB corpus (all documents that survive the upsert), **excluding this `doc_id`'s own prior version**. Replacing a document with a reversed policy is a legitimate change, not a contradiction, so the prior version is never treated as a conflicting peer. +- **`replace`** → **no** corpus-contradiction check. `replace` drops and recreates the collection, redefining it atomically; the pre-write corpus is about to be deleted, so flagging a conflict against soon-to-be-removed documents would be wrong. `replace` documents receive hygiene (including internal self-consistency) only. +- **Intra-batch contradiction** (one document in a request conflicting with a *sibling* document in the same request) is **not** detected in this increment — a documented deferral for both operations. Because verification is one-call-per-document and pre-flight, sibling documents are not yet in ChromaDB and are not passed in the call. + +This is why the verify request must convey the **operation** and **collection**: the agent cannot otherwise interpret ChromaDB contents correctly (a `replace` reading the pre-write corpus would produce false contradictions). + +## Agent design + +Built on **LangGraph** to match the AIAC Agent stack. A `StateGraph` runs the checks staged, with a short-circuit so a failed hygiene check never triggers an unnecessary corpus scan or contradiction LLM call. + +**Graph state** (per verification call): the input document (`text`, `doc_id`), `operation`, `collection`, the fetched `corpus` (populated only for `update`), the accumulated `findings`, and the final `verdict`. + +**Nodes and edges:** + +``` + ┌──────────┐ + in ──► │ hygiene │ (1 LLM call; no corpus) + └────┬─────┘ + │ any blocking hygiene finding + ├───────────────────────────────► verdict (short-circuit) + │ hygiene clean + ▼ + operation == update ? ──── no (replace) ──► verdict + │ yes + ▼ + ┌──────────────┐ ┌───────────────┐ + │ corpus_fetch │ ───► │ contradiction │ ──► verdict + └──────────────┘ └───────────────┘ (1 LLM call) +``` + +- **`hygiene`** — one LLM call judging the three hygiene facets; emits findings. +- Conditional edge — if hygiene produced any *blocking* finding, jump straight to `verdict` (judging contradiction on malformed text is noise and wastes a corpus scan). Otherwise, if `operation == update`, proceed to `corpus_fetch`; if `operation == replace`, jump to `verdict`. +- **`corpus_fetch`** — **full-corpus scan**: `.get()` all documents in the collection from ChromaDB (no similarity search, so the agent needs no embedding-API dependency), regroup chunks by `doc_id`, and drop this document's own prior version. A full scan catches logically-opposed-but-dissimilar contradictions ("admins may delete anything" vs "production is immutable") that top-K retrieval would miss. Guarded by `GUARDRAILS_MAX_CORPUS_DOCS` — see [Corpus-scan guard](#corpus-scan-guard). +- **`contradiction`** — one LLM call judging the incoming document against the fetched corpus; emits findings naming the conflicting `related_doc_id`. +- **`verdict`** — aggregates findings into the final verdict (see below). + +### Findings and verdict model + +Each check emits structured findings: `{check, severity, message, related_doc_id?}` where `severity ∈ {blocking, advisory}`. + +- **Verdict = reject** iff at least one **blocking** finding is present; otherwise **accept**. +- **Advisory** findings are returned but never block — a non-fatal channel so the LLM can flag concerns without failing the operator's whole ingest request (recall all-or-nothing). +- The LLM is instructed which conditions are blocking (not-a-policy, untranslatable, self-contradiction, corpus-contradiction) vs advisory (style, redundancy, minor vagueness). + +LLM structured output is obtained via the LangChain structured-output surface (e.g. `with_structured_output`) so findings are validated at the call boundary. + +### Corpus-scan guard + +`GUARDRAILS_MAX_CORPUS_DOCS` bounds the full-corpus scan. If the collection exceeds the limit, the `corpus_fetch` / `contradiction` path **fails-closed** (the agent returns an error, which the RAG Ingest Service treats as a rejection), consistent with the module's fail-closed posture. This keeps a large corpus from silently degrading to partial or truncated contradiction coverage. + +## Configuration + +| Variable | Default | Source | +|----------|---------|--------| +| `CHROMA_URL` | `http://localhost:8000` | ConfigMap | +| `LLM_BASE_URL` | — | ConfigMap | +| `LLM_MODEL` | — | ConfigMap | +| `LLM_API_KEY` | — | Kubernetes Secret | +| `GUARDRAILS_MAX_CORPUS_DOCS` | `500` | ConfigMap | + +`LLM_BASE_URL` / `LLM_MODEL` / `LLM_API_KEY` reuse the existing AIAC LLM convention (same trio the AIAC Agent and integration tests use). + +Corresponding variables on the **RAG Ingest Service** side (documented in [rag-ingest-service.md](rag-ingest-service.md)): `AIAC_GUARDRAILS_URL`, `AIAC_GUARDRAILS_ENABLED`, `AIAC_GUARDRAILS_TIMEOUT_SECONDS`. + +## Runtime + +- Framework: FastAPI with uvicorn +- Bind: `0.0.0.0:7075` +- Base image: `python:3.12-slim` +- Runs as non-root UID 10001 per the AIAC container pattern. + +## Dependencies (`requirements.txt`) + +``` +fastapi +uvicorn[standard] +httpx +chromadb +langgraph +langchain-openai +``` + +(`langchain-openai` is the OpenAI-compatible LLM client for the LangGraph nodes; swap for the equivalent client if the chosen `LLM_BASE_URL` provider differs.) + +## Open decisions (TBD) + +1. **Endpoint surface and verdict wire-schema** — the verification route path(s) and the exact JSON field names of the request/verdict payloads. The *information* the request must carry (operation, collection, doc_id, text) and the verdict/findings *model* (accept/reject + two-level-severity findings) are fixed above; only the wire representation is open. +2. **Strictness calibration** — how conservative the hygiene/contradiction prompts are about emitting *blocking* (vs advisory) findings, given that one false blocking finding fails the operator's whole request. +3. **Findings persistence** — whether rejection findings are only returned synchronously in the response (plus structured logging), or also persisted somewhere for audit. Deferred this increment: synchronous response + stdout logging only. +4. **Operator override** — whether an operator can force-accept a rejected document, and through what path. Deferred this increment: the RAG Ingest Service's `AIAC_GUARDRAILS_ENABLED` global off-switch is the only override. +5. **`domain-knowledge` API family** — the second verification API (factual-coherence / contradiction for `aiac-domain-knowledge`) is specced independently later. diff --git a/aiac/docs/specs/components/policy-store.md b/aiac/docs/specs/components/policy-model-store.md similarity index 79% rename from aiac/docs/specs/components/policy-store.md rename to aiac/docs/specs/components/policy-model-store.md index 129ee1f84..794293bf7 100644 --- a/aiac/docs/specs/components/policy-store.md +++ b/aiac/docs/specs/components/policy-model-store.md @@ -1,4 +1,4 @@ -# Component PRD: AIAC Policy Store +# Component PRD: AIAC Policy Model Store ## Problem Statement @@ -12,13 +12,13 @@ The `AgentPolicyModel` (APM) is **derived and never persisted** — it is comput ## Solution -A dedicated **AIAC Policy Store** owns an in-memory cache of `ServicePolicyModel` rows (keyed by `serviceId`) backed by a SQLite database for durability. A companion library [`aiac.policy.store.library`](library-policy-store.md) exposes module-level typed functions matching the `aiac.pdp.policy.library` pattern, used by the Policy Computation Engine to read and write SPM state without any storage-layer boilerplate. +A dedicated **AIAC Policy Model Store** owns an in-memory cache of `ServicePolicyModel` rows (keyed by `serviceId`) backed by a SQLite database for durability. A companion library [`aiac.policy.model_store.library`](library-policy-model-store.md) exposes module-level typed functions matching the `aiac.pdp.policy.library` pattern, used by the Policy Computation Engine to read and write SPM state without any storage-layer boilerplate. -The SPM is the **source of truth**. The PDP Policy Writer retains sole ownership of the `AuthorizationPolicy` CR (Rego packages) and has no dependency on the Policy Store. The two persistence artifacts serve distinct purposes and are owned by distinct services: +The SPM is the **source of truth**. The PDP Policy Writer retains sole ownership of the `AuthorizationPolicy` CR (Rego packages) and has no dependency on the Policy Model Store. The two persistence artifacts serve distinct purposes and are owned by distinct services: | Artifact | Owner | Contents | |---|---|---| -| SQLite `service_policies` table | Policy Store | Structured `ServicePolicyModel`, keyed by `serviceId` — source of truth (cache-first, write-through) | +| SQLite `service_policies` table | Policy Model Store | Structured `ServicePolicyModel`, keyed by `serviceId` — source of truth (cache-first, write-through) | | `AuthorizationPolicy` CR (one total) | PDP Policy Writer | Derived Rego packages — OPA runtime artifact | --- @@ -29,22 +29,22 @@ The SPM is the **source of truth**. The PDP Policy Writer retains sole ownership 2. As the Policy Computation Engine, I want to resolve the single SPM that owns a given scope, so that I can attach outbound/inbound rules to the correct service. 3. As the Policy Computation Engine, I want to list every SPM whose inbound rules reference a given role — including stale mappings the IdP no longer reflects — so that override-purge can remove access that should no longer exist. 4. As the Policy Computation Engine, I want to upsert a `ServicePolicyModel`, so that the current policy state survives pod restarts. -5. As a consumer of the Policy Store library, I want a typed Python library that returns `ServicePolicyModel` objects directly, so that I can work with structured policy data without writing storage client code. -6. As an operator, I want the Policy Store deployed as its own single-replica StatefulSet with a dedicated PVC, so that its storage and restart lifecycle is decoupled from the stateless policy services. +5. As a consumer of the Policy Model Store library, I want a typed Python library that returns `ServicePolicyModel` objects directly, so that I can work with structured policy data without writing storage client code. +6. As an operator, I want the Policy Model Store deployed as its own single-replica StatefulSet with a dedicated PVC, so that its storage and restart lifecycle is decoupled from the stateless policy services. --- ## Implementation Decisions -### Policy Store Service +### Policy Model Store Service **Location:** `aiac/src/aiac/policy/store/service/` **Port:** `0.0.0.0:7074` -**ClusterIP Service:** `aiac-policy-store-service:7074` +**ClusterIP Service:** `aiac-policy-model-store-service:7074` -**Deployment:** dedicated single-replica `StatefulSet` `aiac-policy-store`, with a `volumeClaimTemplate` PVC (1 Gi, `ReadWriteOnce`, cluster-default StorageClass) mounted at `/data`. Fronted by a headless Service for stable pod DNS plus the `aiac-policy-store-service:7074` ClusterIP for clients. Not co-located with IdP Configuration / PDP Policy Writer. +**Deployment:** dedicated single-replica `StatefulSet` `aiac-policy-model-store`, with a `volumeClaimTemplate` PVC (1 Gi, `ReadWriteOnce`, cluster-default StorageClass) mounted at `/data`. Fronted by a headless Service for stable pod DNS plus the `aiac-policy-model-store-service:7074` ClusterIP for clients. Not co-located with IdP Configuration / PDP Policy Writer. **Framework:** FastAPI + uvicorn. **Base image:** `python:3.12-slim`. @@ -94,7 +94,7 @@ The by-scope lookup has **no dedicated route** — it collapses to the by-id rea `service_id` is the Keycloak clientId, which is slash-bearing (`{ns}/{workload}`, or a SPIFFE URI under SPIRE) and cannot be a single URL path segment as-is. The `{service_id}` path segment on the three -per-id routes above is base64url-encoded on the wire (`aiac.policy.store.keying.encode_service_id` / +per-id routes above is base64url-encoded on the wire (`aiac.policy.model_store.keying.encode_service_id` / `decode_service_id`); the service decodes it immediately on entry, and the cache/DB stays keyed by the decoded real id — every `service_id` in a request/response *body* (including the by-role list) is always the decoded, real form. The by-role query's `role={role_id}` param is unaffected (not a path @@ -120,7 +120,7 @@ segment). | Variable | Source | Default | |---|---|---| -| `SERVICEPOLICY_DB_PATH` | ConfigMap (`aiac-policy-store-config`) | `/data/policy_model.db` | +| `SERVICEPOLICY_DB_PATH` | ConfigMap (`aiac-policy-model-store-config`) | `/data/policy_model.db` | **Dependencies:** `fastapi`, `uvicorn[standard]`, `pydantic`. `sqlite3` is stdlib (no new dependency). @@ -138,8 +138,8 @@ aiac/src/aiac/policy/store/service/ Build command (run from repo root): ```bash -docker build -f aiac/src/aiac/policy/store/service/Dockerfile \ - -t aiac-policy-store:latest aiac/src/ +docker build -f aiac/src/aiac/policy/model_store/service/Dockerfile \ + -t aiac-policy-model-store:latest aiac/src/ ``` --- @@ -148,7 +148,7 @@ docker build -f aiac/src/aiac/policy/store/service/Dockerfile \ Good tests assert external behavior at the system boundary — not internal implementation details such as private helpers or field serialization choices. -### Policy Store Service +### Policy Model Store Service **Seam:** SQLite `:memory:` database — pass an in-memory connection to the service on startup instead of opening `SERVICEPOLICY_DB_PATH`. All behavioral assertions remain valid; only the storage seam changes. @@ -160,23 +160,23 @@ Key behaviors to assert: - SQLite write error on the write or delete endpoint → `502`. - SQLite file cannot be opened/queried on `GET /health` → `503`. -See [library-policy-store.md](library-policy-store.md) for the companion library testing decisions. +See [library-policy-model-store.md](library-policy-model-store.md) for the companion library testing decisions. --- ## Out of Scope - **APM persistence:** APMs are derived on demand and never stored; the store has no per-agent surface. -- **Triggering Rego generation:** the Policy Store writes structured data only. Triggering Rego generation in the PDP Policy Writer is the responsibility of `aiac.pdp.policy.library` (called by `aiac.policy.computation`). +- **Triggering Rego generation:** the Policy Model Store writes structured data only. Triggering Rego generation in the PDP Policy Writer is the responsibility of `aiac.pdp.policy.library` (called by `aiac.policy.computation`). - **Pagination:** the by-role query returns all matching SPMs without pagination. At target scale (hundreds of services), the full result fits within one query and one HTTP response. -- **In-cluster mTLS between Policy Computation Engine and Policy Store:** secured by Kubernetes network policy; no application-layer auth. +- **In-cluster mTLS between Policy Computation Engine and Policy Model Store:** secured by Kubernetes network policy; no application-layer auth. - **Multi-writer / replica scale-out:** the current design is single-writer (single-replica StatefulSet, RWO PVC, SQLite). Future migration to a shared DB (e.g. PostgreSQL) is a backend swap; the HTTP contract is unchanged. --- ## Further Notes -- The K8s manifests issue must create the `aiac-policy-store` StatefulSet, its `volumeClaimTemplate` PVC (1 Gi, `ReadWriteOnce`), and a headless Service. No CRD or RBAC is needed — the service does not touch the Kubernetes API. +- The K8s manifests issue must create the `aiac-policy-model-store` StatefulSet, its `volumeClaimTemplate` PVC (1 Gi, `ReadWriteOnce`), and a headless Service. No CRD or RBAC is needed — the service does not touch the Kubernetes API. - `spec` fields use snake_case (matching Pydantic's `model_dump()`) — consistent with the `AuthorizationPolicy` CR convention. The JSON column avoids a translation layer. - `service_id` is the SQLite `PRIMARY KEY`. The `aiac.apply.service.{id}` naming convention (lowercase alphanumeric + hyphens) should be maintained for consistency with trigger events. -- K8s resource names: StatefulSet `aiac-policy-store`, ClusterIP Service `aiac-policy-store-service:7074`, env var `AIAC_POLICY_STORE_URL`. +- K8s resource names: StatefulSet `aiac-policy-model-store`, ClusterIP Service `aiac-policy-model-store-service:7074`, env var `AIAC_POLICY_MODEL_STORE_URL`. diff --git a/aiac/docs/specs/components/policy-model.md b/aiac/docs/specs/components/policy-model.md index eae6a0432..74fc1dc60 100644 --- a/aiac/docs/specs/components/policy-model.md +++ b/aiac/docs/specs/components/policy-model.md @@ -5,10 +5,10 @@ `PolicyRule`, `AgentPolicyModel`, and `PolicyModel` were previously defined in `aiac.pdp.library.models`. Three independent consumers now need these types: - `aiac.pdp.policy.library` — translates `PolicyModel` into HTTP calls to the PDP Policy Writer -- `aiac.policy.store.library` — reads/writes `AgentPolicyModel` from/to the Policy Store +- `aiac.policy.model_store.library` — reads/writes `AgentPolicyModel` from/to the Policy Model Store - `aiac.policy.computation` — builds and merges `AgentPolicyModel` objects -Keeping the canonical model definitions inside a PDP-namespaced module (`aiac.pdp.library.models`) forces both the Policy Store library and the Policy Computation Engine to take a dependency on the PDP package — a wrong-layer coupling. Any of the three consumers importing from `aiac.pdp.library.models` would create a transitive dependency on an unrelated service namespace. +Keeping the canonical model definitions inside a PDP-namespaced module (`aiac.pdp.library.models`) forces both the Policy Model Store library and the Policy Computation Engine to take a dependency on the PDP package — a wrong-layer coupling. Any of the three consumers importing from `aiac.pdp.library.models` would create a transitive dependency on an unrelated service namespace. Additionally, the old `PolicyRule` used plain `str` for `role` and `scope`. The current SPM/APM PCE requires typed `Role` and `Scope` objects (from `aiac.idp.configuration.models`): it routes each rule to the owning service's SPM by `scope.serviceId`, classifies edges by `role.kind`, and reads `role.actorIds` when deriving each `AgentPolicyModel` — all fields that a plain `str` cannot carry. (These typed fields replaced the earlier reliance on `Configuration.get_services_by_role` / `get_services_by_scope`, which the SPM-based engine no longer calls.) @@ -29,7 +29,7 @@ The fix is a **two-layer model**: a per-service persistent source of truth (`Ser A canonical, dependency-free model module at `aiac.policy.model` defines `ServicePolicyModel`, `PolicyRule`, `AgentPolicyModel`, and `PolicyModel` with typed fields. No HTTP client, no service code — importable by any consumer without side effects. `PolicyRule.role` and `PolicyRule.scope` are typed `Role` and `Scope` objects from `aiac.idp.configuration.models`. -**Two-layer model.** `ServicePolicyModel` (SPM) is the **persistent source of truth**, one per **service** (agent *and* tool). It holds the service's **inbound** rules plus its own identity (owned roles and scopes). `AgentPolicyModel` (APM) becomes a **pure derived projection** built from the relevant SPMs by the PCE — it is **no longer persisted**. Its shape is unchanged so existing consumers (PDP Policy Library, Policy Store readers) keep working. +**Two-layer model.** `ServicePolicyModel` (SPM) is the **persistent source of truth**, one per **service** (agent *and* tool). It holds the service's **inbound** rules plus its own identity (owned roles and scopes). `AgentPolicyModel` (APM) becomes a **pure derived projection** built from the relevant SPMs by the PCE — it is **no longer persisted**. Its shape is unchanged so existing consumers (PDP Policy Library, Policy Model Store readers) keep working. **Canonical form.** *Every rule is an inbound edge on the SPM of the service that owns the rule's scope.* An agent's outbound edge is the target's inbound edge — `AR→TS` is stored on `SPM(T)`, not on `A`. The routing key is `Scope.serviceId`: a rule `(role, scope)` routes to `SPM(scope.serviceId)`. @@ -41,7 +41,7 @@ The relationship maps (`source_roles`, `subject_roles`, `target_scopes`) are key 1. As the Policy Computation Engine, I want to import `PolicyRule`, `AgentPolicyModel`, and `PolicyModel` from a shared, neutral namespace, so that I do not take an unwanted dependency on the PDP package. 2. As the PDP Policy Library, I want to import `PolicyModel` and `AgentPolicyModel` from `aiac.policy.model`, so that my HTTP serialization logic does not duplicate model definitions. -3. As the Policy Store Library, I want to import `AgentPolicyModel` and `PolicyModel` from `aiac.policy.model`, so that response deserialization uses the same canonical types as every other consumer. +3. As the Policy Model Store Library, I want to import `AgentPolicyModel` and `PolicyModel` from `aiac.policy.model`, so that response deserialization uses the same canonical types as every other consumer. 4. As an AIAC Agent sub-UC agent, I want to construct a `PolicyRule` with typed `Role` and `Scope` objects, so that the PCE can use them for IdP queries without additional type conversion. 5. As the Policy Computation Engine, I want `source_roles`, `subject_roles`, and `target_scopes` keyed by string entity IDs, so that I build them with `entity.id` and they serialize to JSON without custom key handling. 6. As a developer, I want all models to silently ignore unknown fields from API responses, so that IdP API additions do not break deserialization. @@ -120,7 +120,7 @@ A single access rule pairing a typed role with a typed scope. Used in both inbou Complete policy definition for a single agent (service). Inbound and outbound rule sets are typed collections. -> **Derived, not persisted.** `AgentPolicyModel` is now a **pure derived projection** built by the PCE from the relevant `ServicePolicyModel`s. It is **no longer a persisted entity** — the durable source of truth is `ServicePolicyModel`. Its shape is **unchanged** so existing consumers (PDP Policy Library, Policy Store readers) keep working; the docstring on the model states this explicitly. +> **Derived, not persisted.** `AgentPolicyModel` is now a **pure derived projection** built by the PCE from the relevant `ServicePolicyModel`s. It is **no longer a persisted entity** — the durable source of truth is `ServicePolicyModel`. Its shape is **unchanged** so existing consumers (PDP Policy Library, Policy Model Store readers) keep working; the docstring on the model states this explicitly. | Field | Type | Description | |-------|------|-------------| @@ -142,7 +142,7 @@ Complete policy definition for a single agent (service). Inbound and outbound ru #### `PolicyModel` -A partial or full system policy model. When sent to `POST /policy` on the Policy Store, it may contain only the agents whose policies have changed. +A partial or full system policy model. When sent to `POST /policy` on the Policy Model Store, it may contain only the agents whose policies have changed. | Field | Type | |-------|------| @@ -215,7 +215,7 @@ Key behaviors to assert: ## Out of Scope -- HTTP serialization logic — handled by `aiac.policy.store.library`, `aiac.policy.store.service`, and `aiac.pdp.policy.library`. +- HTTP serialization logic — handled by `aiac.policy.model_store.library`, `aiac.policy.model_store.service`, and `aiac.pdp.policy.library`. - IdP API integration — `Service`, `Role`, `Scope` shapes are owned by `aiac.idp.configuration.models`. - Rule revocation semantics — TBD; no model changes required until the design is finalised. diff --git a/aiac/docs/specs/components/rag-ingest-service.md b/aiac/docs/specs/components/rag-ingest-service.md index 5c8692692..b8dee827c 100644 --- a/aiac/docs/specs/components/rag-ingest-service.md +++ b/aiac/docs/specs/components/rag-ingest-service.md @@ -1,7 +1,9 @@ # Component PRD: RAG Ingest Service ## Description -A FastAPI REST service co-located with ChromaDB in the RAG Pod. Accepts knowledge documents for any configured collection, chunks and embeds them, and writes the resulting vectors into the ChromaDB instance in the same Pod. Supports both access control policies (`aiac-policies`) and org/business domain context (`aiac-domain-knowledge`) through a single collection-parameterized API surface. Developer-driven ingestion is performed via `kubectl port-forward`. +A FastAPI REST service co-located with ChromaDB and the Policy Guardrails Agent in the RAG Pod. Accepts knowledge documents for any configured collection, chunks and embeds them, and writes the resulting vectors into the ChromaDB instance in the same Pod. Supports both access control policies (`aiac-policies`) and org/business domain context (`aiac-domain-knowledge`) through a single collection-parameterized API surface. Developer-driven ingestion is performed via `kubectl port-forward`. + +Before any document is written to ChromaDB, the service calls the co-located **Policy Guardrails Agent** to verify it. See [Guardrails pre-flight verification](#guardrails-pre-flight-verification) below. After every successful ingest operation the service publishes a trigger event to the **Event Broker** (NATS JetStream) on the `aiac.apply.policy.build` subject. This causes the AIAC Agent to recompute and apply the updated policy against the live PDP state. All three ingest semantics (replace, update, delete) publish `build`; `rebuild` is an explicit operator-only command issued directly to the Agent and is never triggered by the ingest service. @@ -37,6 +39,16 @@ The `{collection}` path segment must be a slug from `AIAC_RAG_COLLECTIONS` (defa **Delete** is the only path that removes content from a collection. `/update/*` endpoints never delete as a side effect. +## Guardrails pre-flight verification + +Before writing any document to ChromaDB, the service sends it to the co-located **Policy Guardrails Agent** (`localhost:7075`) for verification. Full contract: [policy-guardrails-agent.md](policy-guardrails-agent.md). + +- Applies to all 12 write endpoints above (`replace` and `update`, each across `text`/`file`/`url`), for every collection slug. `DELETE` is exempt. +- One verification call per document. All documents in a request are verified before any ChromaDB mutation for that request occurs. +- **All-or-nothing:** if any document is rejected, the whole request fails and nothing is written to ChromaDB — the collection is left exactly as it was before the request. +- **Fail-closed:** if the Policy Guardrails Agent is unreachable, times out, or errors, the request fails and nothing is written. Set `AIAC_GUARDRAILS_ENABLED=false` to disable verification entirely (e.g. for local development or initial corpus bootstrap) — this is a deliberate operator choice, distinct from an unavailable agent. +- This gate does not change the `aiac.apply.policy.build` publish behavior below: it only happens on a successful ingest, so a fail-closed rejection means the event is simply never published for that request. + ## Post-ingest Event Broker notification After every successful ingest operation (replace, update, or delete), the service publishes `{"id": ""}` to `aiac.apply.policy.build` on the Event Broker (`NATS_URL`). The publish is non-blocking: ingest success is reported to the caller before the NATS publish completes. Publish failures are logged but do not cause the ingest endpoint to return an error. This preserves ingest availability even when the Event Broker is temporarily unavailable. @@ -67,6 +79,9 @@ The AIAC Agent's durable consumer receives the event and acknowledges it after s | `EMBEDDING_BASE_URL` | — | ConfigMap | | `EMBEDDING_MODEL` | — | ConfigMap | | `EMBEDDING_API_KEY` | — | Kubernetes Secret | +| `AIAC_GUARDRAILS_URL` | `http://localhost:7075` | ConfigMap | +| `AIAC_GUARDRAILS_ENABLED` | `true` | ConfigMap | +| `AIAC_GUARDRAILS_TIMEOUT_SECONDS` | — | ConfigMap | Adding a third collection is a configuration-only change: add a new slug to `AIAC_RAG_COLLECTIONS` and a corresponding entry in the slug→name map. No code modification required. diff --git a/aiac/docs/specs/components/rag-knowledge-base.md b/aiac/docs/specs/components/rag-knowledge-base.md index 1b4efe807..953ae9bad 100644 --- a/aiac/docs/specs/components/rag-knowledge-base.md +++ b/aiac/docs/specs/components/rag-knowledge-base.md @@ -1,7 +1,7 @@ # Component PRD: RAG Knowledge Base ## Description -A ChromaDB vector store that holds two named collections in a single instance: AIAC access control policies (`aiac-policies`) and org/business domain context (`aiac-domain-knowledge`). Deployed in a dedicated Kubernetes Pod alongside the RAG Ingest Service. The AIAC Agent retrieves relevant chunks from both collections at runtime via similarity search. +A ChromaDB vector store that holds two named collections in a single instance: AIAC access control policies (`aiac-policies`) and org/business domain context (`aiac-domain-knowledge`). Deployed in a dedicated Kubernetes Pod alongside the RAG Ingest Service and the Policy Guardrails Agent. The AIAC Agent retrieves relevant chunks from both collections at runtime via similarity search. The Policy Guardrails Agent also reads from ChromaDB, for evaluation context, as part of its pre-flight verification of documents the RAG Ingest Service is about to write (see [policy-guardrails-agent.md](policy-guardrails-agent.md)). ## Technology ChromaDB @@ -16,7 +16,7 @@ ChromaDB The legal collection set is an open extension point governed by `AIAC_RAG_COLLECTIONS` on the RAG Ingest Service. Adding a new collection is a configuration-only change (new slug + ChromaDB name in the slug→name map) with no code modification required. ## Deployment -Kubernetes **StatefulSet** in the RAG Pod, co-located with the RAG Ingest Service container. Exposed via the `aiac-rag-service` ClusterIP Service on port 8000 (ChromaDB default). Manifest: `rag-statefulset.yaml`. +Kubernetes **StatefulSet** in the RAG Pod, co-located with the RAG Ingest Service and Policy Guardrails Agent containers. Exposed via the `aiac-rag-service` ClusterIP Service on port 8000 (ChromaDB default). Manifest: `rag-statefulset.yaml`. ChromaDB runs with `IS_PERSISTENT=TRUE` and `PERSIST_DIRECTORY=/chroma/chroma`. Data is stored on a 1 Gi `ReadWriteOnce` PersistentVolumeClaim mounted at `/chroma/chroma`. On pod recreation the StatefulSet rebinds the same PVC; ChromaDB resumes from persisted state without re-ingestion. The RAG Pod runs as a single replica. @@ -25,6 +25,7 @@ ChromaDB runs with `IS_PERSISTENT=TRUE` and `PERSIST_DIRECTORY=/chroma/chroma`. | Consumer | Operation | Collection | |----------|-----------|------------| | RAG Ingest Service | Write (replace / upsert / delete) | Either collection, selected by `{collection}` slug in the request URL | +| Policy Guardrails Agent | Read (evaluation context, e.g. the pre-update version of a `doc_id`) | Either collection, matching the document under verification | | AIAC Agent `fetch_policy` | Read (similarity search, top-N chunks) | `aiac-policies` | | AIAC Agent `fetch_domain_knowledge` | Read (similarity search, top-N chunks) | `aiac-domain-knowledge` | diff --git a/aiac/docs/specs/demo/github-agent.md b/aiac/docs/specs/demo/github-agent.md index d06939e18..86a90b4bd 100644 --- a/aiac/docs/specs/demo/github-agent.md +++ b/aiac/docs/specs/demo/github-agent.md @@ -42,7 +42,7 @@ RFC-8693 token exchange, and the `github-tool` MitM swaps the exchanged token fo - Existing (issue-only) agent card: [`../../analysis/github-agent-card.json`](../../analysis/github-agent-card.json) - `github-tool` MCP tool catalog (44 tools): [`../../analysis/github-mcp-tools-summary.json`](../../analysis/github-mcp-tools-summary.json) - Reference agent: `agent-examples/a2a/git_issue_agent/` -- Reference deployment: `kagenti-extensions/authbridge/demos/github-issue/k8s/` +- Reference deployment: `cortex/authbridge/demos/github-issue/k8s/` - **Sibling tool spec (UC-1 onboarding fixture):** [`github-tool.md`](github-tool.md) — a simplified 4-tool stub (`source-read`, `source-write`, `issues-read`, `issues-write`) deployed as Service `github-tool`. **This is not the tool this agent connects to.** The agent connects to the production @@ -57,7 +57,7 @@ Reuse the `git_issue_agent` stack verbatim — do not introduce a new framework: - **A2A SDK 1.x** server (route factories, `AgentInterface`, snake_case card fields). Binds `0.0.0.0` on `PORT` (default **8000**). `create_jsonrpc_routes(..., enable_v0_3_compat=True)` — **required** - because Kagenti uses A2A 0.3 client libraries — plus the agent card at both + because Rossoctl uses A2A 0.3 client libraries — plus the agent card at both `/.well-known/agent-card.json` and legacy `/.well-known/agent.json`. - **CrewAI** orchestration (`Agent`/`Crew`/`Task`, `Process.sequential`), LLM via **litellm** (`crewai.LLM`). - **`crewai-tools[mcp]` `MCPServerAdapter`** — per-request connection to the MCP tool over @@ -199,13 +199,13 @@ Manifests live under `aiac/demo/assets/agents/github_agent/k8s/`, adapted from t `team1` (installer-provided ConfigMaps/secrets assumed present). - **`github-agent-deployment.yaml`** — `ServiceAccount` + `Deployment` + `Service` + `AgentRuntime`: - - Pod labels `kagenti.io/inject: enabled`, `kagenti.io/spire: enabled`. + - Pod labels `rossoctl.io/inject: enabled`, `rossoctl.io/spire: enabled`. - Container port `8000`; env `MCP_URL=http://github-tool-mcp:9090/mcp`, - `JWKS_URI=http://keycloak-service.keycloak.svc:8080/realms/kagenti/protocol/openid-connect/certs`, + `JWKS_URI=http://keycloak-service.keycloak.svc:8080/realms/rossoctl/protocol/openid-connect/certs`, LLM vars, `PORT`, `LOG_LEVEL`; `/shared` `emptyDir` for operator-mounted client creds. - `Service` `8080 → 8000` (ClusterIP). - `AgentRuntime{ type: agent, targetRef: this Deployment }` — enrolls the workload (operator applies - `kagenti.io/type=agent`, registers a Keycloak client, injects the AuthBridge sidecar). + `rossoctl.io/type=agent`, registers a Keycloak client, injects the AuthBridge sidecar). - Image `github-agent:latest`, `imagePullPolicy: IfNotPresent` (kind-load; name is a documented knob). - **`configmaps.yaml`** — `authbridge-config` (Keycloak URL/realm/issuer) + `authproxy-routes` with the outbound token-exchange route: @@ -216,7 +216,7 @@ Manifests live under `aiac/demo/assets/agents/github_agent/k8s/`, adapted from t ``` - **Prerequisite (reused, not created here):** the existing **production** `github-tool` Deployment/Service (`authbridge/demos/github-issue/k8s/github-tool-deployment.yaml`, Service name `github-tool-mcp`) + - `github-tool-secrets`, a running Kagenti cluster (Keycloak realm `kagenti`, namespace `team1` — + `github-tool-secrets`, a running rossoctl cluster (Keycloak realm `rossoctl`, namespace `team1` — installer-provided and enrolled for AuthBridge injection). The sibling UC-1 stub at `aiac/demo/assets/tools/github_tool/` (Service `github-tool`) is a separate deployment for AIAC onboarding discovery and is **not** a runtime dependency of this agent. @@ -237,11 +237,11 @@ Service name; exchanged audience (`github-tool`) == tool `AUDIENCE`. 4. (Optional; needs a GitHub PAT + reachable LLM) `GITHUB_TOKEN=… MCP_URL=https://api.githubcopilot.com/mcp/`, send an A2A `message/send` read query and confirm a grounded, tool-cited answer. -**Cluster (HITL — live Kagenti + Keycloak + LLM + tool PAT):** -5. `kind load docker-image github-agent:latest --name kagenti`. +**Cluster (HITL — live Rossoctl + Keycloak + LLM + tool PAT):** +5. `kind load docker-image github-agent:latest --name rossoctl`. 6. Ensure `github-tool` + `github-tool-secrets` exist in `team1`. 7. `kubectl apply -f k8s/configmaps.yaml -f k8s/github-agent-deployment.yaml`. -8. Confirm AuthBridge injection + `kagenti.io/type=agent`; `kubectl port-forward svc/github-agent 8080:8080 -n team1`; +8. Confirm AuthBridge injection + `rossoctl.io/type=agent`; `kubectl port-forward svc/github-agent 8080:8080 -n team1`; send an authenticated A2A message; verify token exchange reaches `github-tool` and an answer returns. --- diff --git a/aiac/docs/specs/demo/github-tool.md b/aiac/docs/specs/demo/github-tool.md index bc13b07c4..44bd49ca5 100644 --- a/aiac/docs/specs/demo/github-tool.md +++ b/aiac/docs/specs/demo/github-tool.md @@ -31,10 +31,10 @@ From `analyze_tool` (`../components/aiac-agent/uc1-service-onboarding.md`): 1. **`classify_service`** resolves identity from the Keycloak client's `client.name` (the operator sets it to `"{namespace}/{workload_name}"`), splits on the first `/` → `namespace` + `workload_name`, - LISTs pods in the namespace, finds the pod owned by `workload_name`, and reads the `kagenti.io/type` + LISTs pods in the namespace, finds the pod owned by `workload_name`, and reads the `rossoctl.io/type` pod label. It **must be `tool`** → routes to `analyze_tool`. 2. **`analyze_tool`** does `read_service(workload_name, namespace)` (the K8s Service named - `workload_name`), **requires the `protocol.kagenti.io/mcp` label** on that Service (a deploy-time + `workload_name`), **requires the `protocol.rossoctl.io/mcp` label** on that Service (a deploy-time prerequisite — the operator does **not** stamp it), takes the Service's **first port**, and POSTs JSON-RPC `tools/list` to `http://{workload_name}.{namespace}.svc.cluster.local:{port}/mcp`. @@ -68,7 +68,7 @@ Phase 1 only discovers and evaluates them. - Scenario fixture (`TOOL_SCOPES`): [`../../../test/integration/scenario.py`](../../../test/integration/scenario.py) - Scenario spec: [`../integration-test/policy-pipeline.md`](../integration-test/policy-pipeline.md) - Sibling agent spec: [`github-agent.md`](github-agent.md) -- Reference deployment: `kagenti-extensions/authbridge/demos/github-issue/k8s/` +- Reference deployment: `cortex/authbridge/demos/github-issue/k8s/` --- @@ -209,7 +209,7 @@ ConfigMaps/secrets assumed present), consistent with the agent spec. below). Env `PORT`, `LOG_LEVEL`. Image `github-tool:latest`, `imagePullPolicy: IfNotPresent` (kind-load; name is a documented knob). No GitHub PAT / issuer / JWKS / audience env (unlike the github-issue tool). - - **Pod label `kagenti.io/type: tool`** — this is what `classify_service` reads. Applied by the + - **Pod label `rossoctl.io/type: tool`** — this is what `classify_service` reads. Applied by the operator via the `AgentRuntime` (see below); relying on the operator to stamp it, rather than hand-setting it, keeps it consistent with the operator's own discriminator. - **`Service`** named `github-tool` (ClusterIP), selecting the Deployment's pods. Its **first port** @@ -223,27 +223,27 @@ ConfigMaps/secrets assumed present), consistent with the agent spec. `9096`) clears every AuthBridge-fixed port (`8080`, `8081`, `9091`, `9093`, `9094`). The Service's **external** port stays `9090` (unaffected — `analyze_tool` and the `kubectl port-forward` examples below are unchanged); only `containerPort`/`PORT`/the probes/`targetPort` moved to `9095`. - - **Service label `protocol.kagenti.io/mcp` MUST be present** — a **deploy-time prerequisite** for + - **Service label `protocol.rossoctl.io/mcp` MUST be present** — a **deploy-time prerequisite** for `analyze_tool` (the operator does **not** stamp it; `analyze_tool` returns `502` if absent). Set it - explicitly on the Service metadata (e.g. `protocol.kagenti.io/mcp: "true"`). + explicitly on the Service metadata (e.g. `protocol.rossoctl.io/mcp: "true"`). - **`AgentRuntime{ type: tool, targetRef: { apiVersion: apps/v1, kind: Deployment, name: github-tool } }`** - — enrolls the workload so the kagenti-operator applies `kagenti.io/type=tool` to the pod and + — enrolls the workload so the rossoctl-operator applies `rossoctl.io/type=tool` to the pod and registers a Keycloak client with `client.name = "team1/github-tool"`. Unlike the github-issue demo - (which omits `kagenti.io/type` to skip AuthBridge entirely), this demo **needs** `type: tool` so the - pod carries `kagenti.io/type=tool` for UC-1 `classify_service`. + (which omits `rossoctl.io/type` to skip AuthBridge entirely), this demo **needs** `type: tool` so the + pod carries `rossoctl.io/type=tool` for UC-1 `classify_service`. - **No `configmaps.yaml` needed here** — the tool has no `authbridge-config` / `authproxy-routes` dependency (it neither validates inbound JWTs nor does outbound token exchange in phase 1). The agent spec's `authproxy-routes` still targets the **production** `github-tool-mcp` host and is unrelated to this stand-in. -- **Prerequisite (reused, not created here):** a running Kagenti cluster with the kagenti-operator +- **Prerequisite (reused, not created here):** a running rossoctl cluster with the rossoctl-operator (Keycloak realm as configured by the installer, namespace `team1`), so the `AgentRuntime` is reconciled into a Keycloak client + pod label. **Wiring invariant:** `AgentRuntime.targetRef.name` == `Deployment` name == `Service` name == -`github-tool`; the `Service` carries the `protocol.kagenti.io/mcp` label and exposes `/mcp` as its first -port; the operator-applied pod label is `kagenti.io/type=tool`; the operator-registered Keycloak +`github-tool`; the `Service` carries the `protocol.rossoctl.io/mcp` label and exposes `/mcp` as its first +port; the operator-applied pod label is `rossoctl.io/type=tool`; the operator-registered Keycloak `client.name` is `team1/github-tool`. --- @@ -264,13 +264,13 @@ port; the operator-applied pod label is `kagenti.io/type=tool`; the operator-reg 3. Confirm each returned tool's `description` is byte-identical to the matching `TOOL_SCOPES` entry in `scenario.py` (e.g. `jq '.result.tools[] | {name, description}'`). -**Cluster (HITL — live Kagenti + Keycloak + operator):** -4. `kind load docker-image github-tool:latest --name kagenti`. +**Cluster (HITL — live Rossoctl + Keycloak + operator):** +4. `kind load docker-image github-tool:latest --name rossoctl`. 5. `kubectl apply -f k8s/github-tool-deployment.yaml`. 6. Confirm the operator applied the pod label: `kubectl get pod -l app=github-tool -n team1 - -o jsonpath='{.items[0].metadata.labels.kagenti\.io/type}'` → `tool`. + -o jsonpath='{.items[0].metadata.labels.rossoctl\.io/type}'` → `tool`. 7. Confirm the Service carries the MCP label: `kubectl get svc github-tool -n team1 - -o jsonpath='{.metadata.labels.protocol\.kagenti\.io/mcp}'` → present. + -o jsonpath='{.metadata.labels.protocol\.rossoctl\.io/mcp}'` → present. 8. Confirm the operator registered a Keycloak client with `client.name = "team1/github-tool"` (via the Keycloak admin API / IdP `Configuration` library). 9. From inside the cluster (or via `kubectl port-forward svc/github-tool 9090:9090 -n team1`), POST diff --git a/aiac/docs/specs/integration-test/policy-pipeline.md b/aiac/docs/specs/integration-test/policy-pipeline.md index 6b23c7f71..4b08eef8e 100644 --- a/aiac/docs/specs/integration-test/policy-pipeline.md +++ b/aiac/docs/specs/integration-test/policy-pipeline.md @@ -54,7 +54,7 @@ the UC-1 ladder's `rego_out/uc1/`). `opa eval` then asserts the scenario truth table against **each** variant's Rego (step 7). Steps 1–6 below describe one such run. 1. **Set service URLs in env before importing the aiac libraries.** Export `AIAC_PDP_CONFIG_URL`, - `AIAC_POLICY_STORE_URL`, `AIAC_PDP_POLICY_URL`, and `KEYCLOAK_REALM` *before* importing the aiac + `AIAC_POLICY_MODEL_STORE_URL`, `AIAC_PDP_POLICY_URL`, and `KEYCLOAK_REALM` *before* importing the aiac libraries — the libraries read env at import time. This is the pattern `test/pdp/policy/generate_rego.py` already follows. (The PCE resolves its realm via `Configuration.for_default_realm()`, the single source of truth reading `KEYCLOAK_REALM`; the @@ -62,9 +62,9 @@ scenario truth table against **each** variant's Rego (step 7). Steps 1–6 below 2. **Spawn the three services as `uvicorn` subprocesses** (no Docker) and poll each `GET /health` until ready, with a bounded timeout: - IdP Configuration Service — `aiac.idp.service.configuration.keycloak.main:app` on `7071`. - - Policy Store — its ASGI app on `7074`, with `AGENTPOLICY_DB_PATH` pointed at a fresh temp dir. + - Policy Model Store — its ASGI app on `7074`, with `SERVICEPOLICY_DB_PATH` pointed at a fresh temp dir. - OPA Policy Writer — `aiac.pdp.service.policy.opa.main:app` on `7072`, with `REGO_OUTPUT_DIR` - (pointed at the current variant's `rego_out/policy_pipeline//`) and the Policy Store DB path in its env. + (pointed at the current variant's `rego_out/policy_pipeline//`) and the Policy Model Store DB path in its env. 3. **Provision Keycloak** (idempotent — delete-if-exists the realm first, then create): - via **`python-keycloak` `KeycloakAdmin`** (test fixture): create realm `AIAC_TEST_REALM`; create users `dev-user`, `test-user`, and `devops-user`; create realm roles `developer`, `tester`, and @@ -213,16 +213,16 @@ Role → access (confirmed with the user; the fixed facts that both `policy.md` | `AIAC_TEST_REALM` | Realm the test provisions | `aiac-pp` | | `KEYCLOAK_REALM` | Realm the PCE reads back, via `Configuration.for_default_realm()` (single source of truth; = `AIAC_TEST_REALM`) | `aiac-pp` | | `AIAC_PDP_CONFIG_URL` | IdP Configuration Service base URL (set before import) | `http://127.0.0.1:7071` | -| `AIAC_POLICY_STORE_URL` | Policy Store base URL (set before import) | `http://127.0.0.1:7074` | +| `AIAC_POLICY_MODEL_STORE_URL` | Policy Model Store base URL (set before import) | `http://127.0.0.1:7074` | | `AIAC_PDP_POLICY_URL` | OPA Policy Writer base URL (set before import) | `http://127.0.0.1:7072` | | `REGO_OUTPUT_DIR` | Base dir the OPA stub subprocess writes `.rego` to; the test points it at `rego_out/policy_pipeline//` per variant and leaves the files on disk | operator-chosen local dir | -| `AGENTPOLICY_DB_PATH` | Policy Store DB path for the subprocess (fresh temp dir) | temp | +| `SERVICEPOLICY_DB_PATH` | Policy Model Store DB path for the subprocess (fresh temp dir) | temp | | `AIAC_POLICY_FILE` | PRB whole-file policy — path to the `policy.md` variant fed to the PRB; the test sets it per variant (`policy.explicit.md`, `policy.abstract.md`) | `/etc/aiac/policy.md` | | `LLM_BASE_URL` / `LLM_MODEL` / `LLM_API_KEY` | PRB LLM (pinned `temperature=0`) | — (required) | | `OPA_BIN` | Path to the standalone `opa` binary used as the verification oracle; else `PATH` (`shutil.which`), else the test `pytest.skip`s | — (optional; PATH lookup) | -> When the test is written, confirm the Policy Store's ASGI import path and its DB-path env-var -> name against the Policy Store component spec / issue — `AGENTPOLICY_DB_PATH` is the placeholder used +> When the test is written, confirm the Policy Model Store's ASGI import path and its DB-path env-var +> name against the Policy Model Store component spec / issue — `SERVICEPOLICY_DB_PATH` is the placeholder used > here; use the real one. `AIAC_POLICY_FILE` selects which `policy.md` variant (see > *[Scenario inputs](#scenario-inputs-prb-functional-inputs)*) the PRB reads. @@ -246,7 +246,7 @@ Keycloak, a real LLM, and an `opa` binary on `PATH` (or `$OPA_BIN`). The suite `pytest.skip`s when no `opa` binary is found (`$OPA_BIN` → `PATH`). Eyeball the persisted Rego against the adjusted package shapes in [../components/pdp-policy-writer-opa.md](../components/pdp-policy-writer-opa.md); optionally inspect the -Policy Store DB and the provisioned Keycloak realm. +Policy Model Store DB and the provisioned Keycloak realm. ## Testing Decisions @@ -273,7 +273,7 @@ Policy Store DB and the provisioned Keycloak realm. the test reads each service back via `Configuration.get_service` and asserts its `.type` before running the pipeline, aborting on mismatch. This is a **provisioning** sanity check, distinct from the Rego-decision assertions. -- **Self-contained subprocess lifecycle.** The test spawns IdP (7071), Policy Store (7074), and OPA +- **Self-contained subprocess lifecycle.** The test spawns IdP (7071), Policy Model Store (7074), and OPA (7072) as `uvicorn` subprocesses, polls each `GET /health` before use, and tears them all down in `finally`. Keycloak and the LLM are **external** (reached via env); `opa` is an external binary. - **LLM nondeterminism, contained.** The PRB LLM is pinned to `temperature=0`, and the **explicit** @@ -366,7 +366,7 @@ are **resolved**, so this test is ready to be written. Component prerequisites: - OPA filesystem stub — `pdp-policy-writer/1.14-pdp-policy-writer-opa-stub.md` - Rego package generator — `pdp-policy-writer/1.10-rego-package-generator.md` - pdp-policy library — `library/pdp/8.9-pdp-policy-library-rename.md` -- Policy Store library / service — `policy/store/8.7-policy-store-library.md` / +- Policy Model Store library / service — `policy/store/8.7-policy-store-library.md` / `policy/store/8.5-policy-store-service.md` ## Scenario inputs (PRB functional inputs) @@ -381,7 +381,7 @@ The descriptions are **generic and keyword-free** — they describe what each en carry no policy grant ("Resolves to…") and no owning-client naming, and stay within Keycloak's 255-char cap so they are written verbatim (no shortened renderings). Client `type` is **not** inferred from description prose: the test sets each client's `client.type` attribute directly — as a plain string -`"Agent"` / `"Tool"` written onto the client — rather than discovering it from a `kagenti.io/type` +`"Agent"` / `"Tool"` written onto the client — rather than discovering it from a `rossoctl.io/type` label, so `Service` type resolution ([../../../src/aiac/idp/configuration/models.py:79-87](../../../src/aiac/idp/configuration/models.py#L79-L87)) tags each client from the attribute without touching the TEMP description-keyword fallback. diff --git a/aiac/docs/specs/integration-test/uc1-onboarding-pipeline.md b/aiac/docs/specs/integration-test/uc1-onboarding-pipeline.md index 419b9ff7a..708a20502 100644 --- a/aiac/docs/specs/integration-test/uc1-onboarding-pipeline.md +++ b/aiac/docs/specs/integration-test/uc1-onboarding-pipeline.md @@ -51,7 +51,7 @@ generated Rego decides correctly using the standalone `opa eval` binary as the o Phase-1 is explicit that **live enforcement / live traffic is out of scope** — correctness is shown by **evaluating the generated rules**, not by routing requests. So each rung is *onboard + evaluate*: the -workloads are really deployed and really discovered by UC-1 (classify from the `kagenti.io/type` label, +workloads are really deployed and really discovered by UC-1 (classify from the `rossoctl.io/type` label, read the AgentCard / MCP `tools/list`, provision roles/scopes into Keycloak, model access, emit Rego), but **no A2A message is ever sent through the agent**. @@ -59,7 +59,7 @@ The generated Rego is the **artifact under test** — the LLM/PCE that produced tests never trust it. Expected verdicts are **computed from** the `scenario_uc1.py` pair-lists (the intended policy). A mismatch fails the test and names the exact cell. -Because they need a live Kagenti cluster + operator + Keycloak + a real LLM, they are +Because they need a live rossoctl cluster + operator + Keycloak + a real LLM, they are `@pytest.mark.integration` (out of the default unit run, `-m "not integration"`) and additionally `pytest.skip` when no `opa` binary is found. @@ -69,11 +69,10 @@ Because they need a live Kagenti cluster + operator + Keycloak + a real LLM, the Store + **OPA Policy Writer (filesystem stub)**, mounting the **single abstract** `policy.md`. AIAC runs in-cluster so UC-1's `analyze_tool` can reach the tool's MCP endpoint at its cluster-internal DNS name (`github-tool.{ns}.svc.cluster.local`); the tests trigger over `kubectl port-forward`. -- **OPA filesystem-stub writer, not the legacy Keycloak composite writer.** The stack must run +- **OPA filesystem-stub writer.** The stack must run `aiac-pdp-policy-opa` (the filesystem stub: writes `{slug}.inbound.rego` + `{slug}.outbound.rego` to `REGO_OUTPUT_DIR`, default `/rego`) — this is what the K8s Phase 1 Interface Pod actually deploys. - **Not** the superseded `aiac-pdp-policy-keycloak` composite writer, which manages Keycloak composite - roles and emits **no Rego at all**, and is not deployed by any current manifest. The `.rego` files are + The `.rego` files are the artifact under test; without the OPA writer there is nothing to capture. - **Rego capture.** `kubectl cp` the writer's `/rego` to a per-rung host dir (a `rung{1,2,3}` subfolder under the gitignored `test/integration/rego_out/uc1/` tree, so artifacts stay in the project for @@ -84,7 +83,7 @@ Because they need a live Kagenti cluster + operator + Keycloak + a real LLM, the - **Workloads deployed + registered.** Both `github-agent` and simplified `github-tool` are **already deployed** in `AIAC_DEMO_NAMESPACE` and **already registered as Keycloak clients** (`client.name = "{ns}/{workload}"`) into `AIAC_TEST_REALM`. The tests do **not** `kubectl apply` - manifests or wait for operator registration / `kagenti.io/type` labels / AgentCard / `tools/list` — that + manifests or wait for operator registration / `rossoctl.io/type` labels / AgentCard / `tools/list` — that is deployment's job. > **Resolving `{service_id}`.** The `POST /apply/service/{service_id}` route is a **single path > segment**, and the Controller resolves the trigger via `admin.get_client(service_id)` — which keys @@ -105,7 +104,7 @@ Because they need a live Kagenti cluster + operator + Keycloak + a real LLM, the provisioned realm roles + client scopes, leaving the clients registered exactly as before the first run; clear the writer's `/rego`. This gives every rung a clean slate and makes reruns converge. (With the OPA writer there are no composites — the only Keycloak mutations are the roles/scopes the onboarding - agent provisions — but cleaning both is harmless and future-proofs against the Keycloak writer.) + agent provisions — but cleaning both is harmless.) 2. **Onboard** in the rung's order via `POST /apply/service/{service_id}`, where `{service_id}` is the internal Keycloak UUID (`Service.id`), **not** the clientId — the onboard route is keyed on the UUID. - `POST /apply/service/{github-tool id}` → UC-1 classifies it a **Tool**, reads the MCP manifest, @@ -222,7 +221,7 @@ workloads. | Element | Value | |---------|-------| -| Realm | `AIAC_TEST_REALM` (must match the deployed stack's `KEYCLOAK_REALM`; default `kagenti`) | +| Realm | `AIAC_TEST_REALM` (must match the deployed stack's `KEYCLOAK_REALM`; default `rossoctl`) | | Agent | `github-agent` — **discovered** per-skill operator roles `github-agent.source_operations`, `github-agent.issue_operations` (mirroring the scopes); scopes `github-agent.source_operations`, `github-agent.issue_operations` (from AgentCard skills) | | Tool | `github-tool` (simplified) — **discovered** scopes `github-tool.{source-read, source-write, issues-read, issues-write}` (from MCP `tools/list`) | | Users | `dev-user` (`developer`), `test-user` (`tester`), `devops-user` (`devops`) | @@ -234,12 +233,12 @@ workloads. | Variable | Purpose | Default | |----------|---------|---------| -| `KUBECONFIG` | Kubeconfig for the live Kagenti/Kind cluster | — (required) | +| `KUBECONFIG` | Kubeconfig for the live rossoctl/Kind cluster | — (required) | | `AIAC_DEMO_NAMESPACE` | Namespace the demo workloads are deployed in (precondition) | `team1` | | `KEYCLOAK_URL` | External Keycloak base URL | — (required) | | `KEYCLOAK_ADMIN_REALM` | Realm the admin creds live in | `master` | | `KEYCLOAK_ADMIN_USERNAME` / `KEYCLOAK_ADMIN_PASSWORD` | Keycloak admin creds (user/realm-role provisioning + cleanup) | — (required) | -| `AIAC_TEST_REALM` | Realm the tests resolve/provision against. **Must match the deployed AIAC stack's `KEYCLOAK_REALM`** — the in-cluster Controller resolves the onboarding trigger in *its own* realm, so a harness on a different realm resolves a client UUID the Controller can't find (404 → onboard 502). The demo namespace's clients are registered into it. | `kagenti` | +| `AIAC_TEST_REALM` | Realm the tests resolve/provision against. **Must match the deployed AIAC stack's `KEYCLOAK_REALM`** — the in-cluster Controller resolves the onboarding trigger in *its own* realm, so a harness on a different realm resolves a client UUID the Controller can't find (404 → onboard 502). The demo namespace's clients are registered into it. | `rossoctl` | | `AIAC_CONTROLLER_URL` | Base URL of the in-cluster AIAC Controller (via port-forward) for `POST /apply/service/{id}` | `http://127.0.0.1:7070` | | `AIAC_OPA_POD` / `AIAC_OPA_SELECTOR` | OPA-writer pod (or label selector) to `kubectl cp` `.rego` from | — (resolved from labels) | | `AIAC_OPA_REGO_PATH` | Writer output dir inside the pod | `/rego` | @@ -253,12 +252,12 @@ workloads. ## Runbook -Runnable against a live Kagenti/Kind cluster (operator + Keycloak + SPIRE) with the AIAC stack running the +Runnable against a live rossoctl/Kind cluster (operator + Keycloak + SPIRE) with the AIAC stack running the **OPA filesystem-stub writer**, `github-agent` + `github-tool` **already deployed and registered** into `AIAC_TEST_REALM`, a real LLM, and an `opa` binary on `PATH` (or `$OPA_BIN`). ```bash -# env: KUBECONFIG + KEYCLOAK_URL + admin creds + LLM_* set; realm defaults to kagenti (match the stack's KEYCLOAK_REALM); opa on PATH or $OPA_BIN +# env: KUBECONFIG + KEYCLOAK_URL + admin creds + LLM_* set; realm defaults to rossoctl (match the stack's KEYCLOAK_REALM); opa on PATH or $OPA_BIN .venv/bin/pytest test/integration/ -m integration -k uc1_onboard -v # A failing node names the exact cell, e.g.: # test_outbound[test-user-github-tool.source-read] — expected deny, opa allowed @@ -277,8 +276,7 @@ The suite `pytest.skip`s when no `opa` binary is found. - **Deployment is a precondition.** The tests do not deploy or wait for registration; they cleanup → onboard → validate → cleanup, so reruns are hermetic and cheap. - **One stack, one policy, OPA filesystem stub.** Rungs 1–3 need only one AIAC stack; the OPA writer's - `/rego` output is what makes the pipeline observable. The Keycloak composite writer emits no Rego and is - not used. + `/rego` output is what makes the pipeline observable. - **Onboarding-order-independence is asserted, not assumed** (rungs 2 vs 3). A divergence is a bug. - **Per-scope two-gate AND.** UC-1's per-skill operator roles are mapped to the tool scopes by capability-match, so `target_ok` is populated; the outbound probe binds the real per-scope AND @@ -287,10 +285,10 @@ The suite `pytest.skip`s when no `opa` binary is found. - **Grant sets, semantic.** Equivalence is re-derived from the Rego data maps and compared as sets — the semantic-similarity guarantee, not byte-identity. - **Stack's realm, leave-in-place; per-rung cleanup.** UC-1 resolves/provisions against the deployed - stack's `KEYCLOAK_REALM` (default `kagenti`) and **never deletes** the realm/users/roles; only the + stack's `KEYCLOAK_REALM` (default `rossoctl`) and **never deletes** the realm/users/roles; only the provisioned agent/tool roles/scopes are cleaned up per rung so onboarding runs from a clean slate. (Contrast `5.3 policy-pipeline`, which owns a **throwaway** realm it `delete_realm`s + recreates each - run — that suite must never point `AIAC_TEST_REALM` at `kagenti`, or it destroys the demo clients.) + run — that suite must never point `AIAC_TEST_REALM` at `rossoctl`, or it destroys the demo clients.) - **LLM nondeterminism, contained.** PRB LLM pinned `temperature=0`; both cell-level and grant-set assertions; `@pytest.mark.integration`, out of default CI. - **Prior art, shared not copied.** Reuses the `5.3` shape (`opa` discovery/skip, scenario-as-oracle, @@ -328,7 +326,7 @@ Tracking issues: `testing/5.4-uc1-onboarding-integration-test.md` (epic) + `5.4. **Functional** inputs — the PRB reads the descriptions and the `policy.md` to produce the role→scope mappings. Descriptions are **generic and keyword-free**; client `type` is set by UC-1 from the -`kagenti.io/type` label. +`rossoctl.io/type` label. ### Discovered entities (what UC-1 provisions) diff --git a/aiac/k8s/agent-deployment.yaml b/aiac/k8s/agent-deployment.yaml index c9ccb3189..ae167f6d6 100644 --- a/aiac/k8s/agent-deployment.yaml +++ b/aiac/k8s/agent-deployment.yaml @@ -17,6 +17,11 @@ data: LLM_BASE_URL: "https://openai-compatible-endpoint.example.com/v1" LLM_MODEL: "your-model-name" AIAC_AC_MODEL: "RBAC" + # Per-request LLM timeout (seconds): a stalled connection raises after this instead + # of wedging /apply forever. Timeouts are transient, so _structured_call retries them + # up to UPSTREAM_MAX_RETRIES (default 3) with exponential backoff before surfacing. + LLM_REQUEST_TIMEOUT: "120" + UPSTREAM_MAX_RETRIES: "3" --- # Prerequisites: create this Secret before applying this manifest: @@ -42,7 +47,7 @@ rules: - apiGroups: [""] resources: ["services"] verbs: ["get"] - - apiGroups: ["agent.kagenti.dev"] + - apiGroups: ["agent.rossoctl.dev"] resources: ["agentcards"] verbs: ["list"] @@ -85,9 +90,17 @@ spec: fsGroup: 10001 seccompProfile: type: RuntimeDefault - # aiac-init init container is deferred to Phase 2 (issue 4.21). - # It will gate startup on NATS + IdP Config + PDP Policy + RAG Ingest health - # and provision the aiac-events JetStream stream. + initContainers: + # Gates Agent startup on NATS + IdP Config + PDP Policy (+ optional RAG + # Ingest) health, then provisions the aiac-events JetStream stream. + # Reuses the aiac-agent image (see src/aiac/agent/init/wait_and_provision.py). + - name: aiac-init + image: localhost/aiac-agent:local + imagePullPolicy: Never + command: ["python", "-m", "aiac.agent.init.wait_and_provision"] + envFrom: + - configMapRef: + name: aiac-pdp-config containers: - name: aiac-agent image: localhost/aiac-agent:local @@ -108,15 +121,17 @@ spec: limits: cpu: 500m memory: 512Mi - # The Controller exposes only /apply/* routes (no /health endpoint), - # so both probes are gated on the port accepting TCP connections. + # The Controller is stateless; GET /health is a bare liveness/readiness + # signal (200 {"status": "ok"} whenever the process is serving). readinessProbe: - tcpSocket: + httpGet: + path: /health port: 7070 initialDelaySeconds: 5 periodSeconds: 10 livenessProbe: - tcpSocket: + httpGet: + path: /health port: 7070 initialDelaySeconds: 10 periodSeconds: 20 diff --git a/aiac/k8s/aiac-deployment-guide.md b/aiac/k8s/aiac-deployment-guide.md index fd8900b6e..451315983 100644 --- a/aiac/k8s/aiac-deployment-guide.md +++ b/aiac/k8s/aiac-deployment-guide.md @@ -6,9 +6,10 @@ This guide covers the full AIAC deployment in the `aiac-system` namespace. | Manifest | Contents | Port(s) | |---|---|---| -| `pdp-interface-deployment.yaml` | Kagenti Interface Pod (IdP Configuration Service + PDP Policy Writer **Phase 1 rego-file mock** `aiac-pdp-policy-opa`) + 2 ClusterIP Services | 7071, 7072 | -| `policy-store-statefulset.yaml` | Policy Store StatefulSet + 1 Gi PVC + headless Service + ClusterIP Service | 7074 | -| `agent-deployment.yaml` | Agent Pod Deployment (AIAC Agent) + ClusterIP Service | 7070 | +| `pdp-interface-deployment.yaml` | Rossoctl Interface Pod (IdP Configuration Service + PDP Policy Writer **Phase 1 rego-file mock** `aiac-pdp-policy-opa`) + 2 ClusterIP Services | 7071, 7072 | +| `policy-model-store-statefulset.yaml` | Policy Model Store StatefulSet + 1 Gi PVC + headless Service + ClusterIP Service | 7074 | +| `event-broker-deployment.yaml` | NATS JetStream Event Broker Deployment + ClusterIP Service | 4222 | +| `agent-deployment.yaml` | Agent Pod Deployment (`aiac-init` init container + AIAC Agent) + ClusterIP Service | 7070 | ## Prerequisites @@ -18,7 +19,12 @@ This guide covers the full AIAC deployment in the `aiac-system` namespace. ## 1 — Build the images -Run from the repo root (`kagenti-extensions/`): +Run from the repo root (`cortex/`). + +> **Build context differs per image.** The two `keycloak` services are self-contained +> (their Dockerfiles `COPY requirements.txt .` / `COPY main.py .`), so their build context +> is the Dockerfile's own directory. The `opa`, `policy-model-store`, and `agent` images bundle the +> `aiac` package tree (`COPY . /app/src`), so their context is `aiac/src/`. ```bash # IdP Configuration Service (Interface Pod container 1) @@ -33,15 +39,17 @@ docker build -f aiac/src/aiac/pdp/service/policy/opa/Dockerfile \ -t localhost/aiac-pdp-policy-opa:local \ aiac/src/ -# Policy Store -docker build -f aiac/src/aiac/policy/store/service/Dockerfile \ - -t localhost/aiac-policy-store:local aiac/src/ +# Policy Model Store — context: aiac/src/ (bundles the aiac package) +docker build -f aiac/src/aiac/policy/model_store/service/Dockerfile \ + -t localhost/aiac-policy-model-store:local aiac/src/ -# AIAC Agent +# AIAC Agent (also used as the aiac-init init container, via a command override) — context: aiac/src/ docker build -f aiac/src/aiac/agent/controller/Dockerfile \ -t localhost/aiac-agent:local aiac/src/ ``` +The Event Broker uses the stock `nats:latest` image — no build step. + ## 2 — Load images into the cluster **Kind (local development)** @@ -49,13 +57,21 @@ docker build -f aiac/src/aiac/agent/controller/Dockerfile \ ```bash kind load docker-image localhost/aiac-pdp-config:local --name kind load docker-image localhost/aiac-pdp-policy-opa:local --name -kind load docker-image localhost/aiac-policy-store:local --name +kind load docker-image localhost/aiac-policy-model-store:local --name kind load docker-image localhost/aiac-agent:local --name ``` +`event-broker-deployment.yaml` also uses `imagePullPolicy: Never`, so the NATS image must +be side-loaded too, even though it isn't built — just pulled from Docker Hub first: + +```bash +docker pull nats:latest +kind load docker-image nats:latest --name +``` + **Remote registry** — tag, push, then update the `image:` fields in the manifests to match. -> **Note:** the manifests set `imagePullPolicy: Never` because images are side-loaded +> **Note:** all manifests set `imagePullPolicy: Never` because images are side-loaded > into a local Kind cluster (dev only). For a real cluster that pulls from a registry, > change these to `imagePullPolicy: IfNotPresent` (or `Always`). @@ -107,17 +123,20 @@ Edit the `aiac-pdp-config` ConfigMap in `pdp-interface-deployment.yaml` to match | Key | Default | Used by | |-----|---------|---------| -| `KEYCLOAK_URL` | `http://keycloak-service.keycloak.svc:8080` | IdP Configuration Service, PDP Policy Writer | -| `KEYCLOAK_REALM` | `kagenti` | PDP Policy Writer | +| `KEYCLOAK_URL` | `http://keycloak-service.keycloak.svc:8080` | IdP Configuration Service | +| `KEYCLOAK_REALM` | `rossoctl` | IdP Configuration Service | | `KEYCLOAK_ADMIN_REALM` | `master` | IdP Configuration Service | | `AIAC_PDP_CONFIG_URL` | `http://aiac-pdp-config-service:7071` | Agent | | `AIAC_PDP_POLICY_URL` | `http://aiac-pdp-policy-service:7072` | Agent | -| `AIAC_POLICY_STORE_URL` | `http://aiac-policy-store-service:7074` | Agent | -| `SERVICEPOLICY_DB_PATH` | `/data/policy_model.db` | Policy Store | -| `NATS_URL` | `nats://aiac-event-broker-service:4222` | Agent — **added in Phase 2** (Event Broker, issue 4.19) | +| `AIAC_POLICY_MODEL_STORE_URL` | `http://aiac-policy-model-store-service:7074` | Agent | +| `SERVICEPOLICY_DB_PATH` | `/data/policy_model.db` | Policy Model Store | +| `NATS_URL` | `nats://aiac-event-broker-service:4222` | Agent, `aiac-init` — Event Broker ClusterIP address | | `AIAC_RAG_INGEST_URL` | `http://aiac-rag-service:7073` | Init container — **added in Phase 3** (RAG Pod, issue 4.20) | | `AIAC_CHROMADB_URL` | `http://aiac-rag-service:8000` | Agent — **added in Phase 3** (RAG Pod, issue 4.20) | +`aiac-init` treats `AIAC_RAG_INGEST_URL` as optional and skips the RAG Ingest health check +when it is unset (the current phase has no RAG pod deployed yet). + ## 5 — Deploy Apply in dependency order: @@ -126,10 +145,13 @@ Apply in dependency order: # 1. Interface Pod — creates the namespace, ConfigMap, Secret, and ClusterIP Services kubectl apply -f aiac/k8s/pdp-interface-deployment.yaml -# 2. Policy Store — needs the aiac-system namespace -kubectl apply -f aiac/k8s/policy-store-statefulset.yaml +# 2. Event Broker — NATS JetStream, no dependencies +kubectl apply -f aiac/k8s/event-broker-deployment.yaml -# 3. Agent — depends on the Interface Pod + Policy Store already being healthy +# 3. Policy Model Store — needs the aiac-system namespace +kubectl apply -f aiac/k8s/policy-model-store-statefulset.yaml + +# 4. Agent — aiac-init waits for NATS + Interface Pod + Policy Model Store to be healthy kubectl apply -f aiac/k8s/agent-deployment.yaml ``` @@ -137,7 +159,8 @@ Wait for all pods to be ready: ```bash kubectl wait deployment/aiac-interface -n aiac-system --for=condition=Available --timeout=120s -kubectl wait statefulset/aiac-policy-store -n aiac-system --for=jsonpath='{.status.readyReplicas}'=1 --timeout=120s +kubectl wait deployment/aiac-event-broker -n aiac-system --for=condition=Available --timeout=120s +kubectl wait statefulset/aiac-policy-model-store -n aiac-system --for=jsonpath='{.status.readyReplicas}'=1 --timeout=120s kubectl wait deployment/aiac-agent -n aiac-system --for=condition=Available --timeout=120s ``` @@ -156,8 +179,8 @@ kubectl port-forward svc/aiac-pdp-policy-service 7072:7072 -n aiac-system & curl http://localhost:7072/health # {"status":"ok"} -# Policy Store -kubectl port-forward svc/aiac-policy-store-service 7074:7074 -n aiac-system & +# Policy Model Store +kubectl port-forward svc/aiac-policy-model-store-service 7074:7074 -n aiac-system & curl http://localhost:7074/health # {"status":"ok"} @@ -166,9 +189,29 @@ kubectl port-forward svc/aiac-agent-service 7070:7070 -n aiac-system & curl http://localhost:7070/health # {"status":"ok"} +# Clean up all the tunnels opened to the cluster pkill -f "port-forward" ``` +### NATS Event Broker — end-to-end check + +Requires the [`nats` CLI](https://github.com/nats-io/natscli). + +```bash +kubectl port-forward svc/aiac-event-broker-service 4222:4222 -n aiac-system & +nats context save aiac --server nats://localhost:4222 +nats context select aiac + +# Publish a test service-onboarding event (use a real IdP client UUID to see it +# processed end to end; any string will demonstrate delivery either way): +nats pub aiac.apply.service. '{"id":""}' + +# Confirm the Agent processed and acked it (no redelivery): +kubectl logs deployment/aiac-agent -n aiac-system -c aiac-agent --tail=50 + +pkill -f "port-forward.*4222" +``` + Run the IdP data smoke test: ```bash @@ -181,9 +224,10 @@ pkill -f "port-forward.*7071" ## Redeploying after a code change ```bash -# Rebuild the changed image, e.g. IdP Configuration Service: +# Rebuild the changed image, e.g. IdP Configuration Service (context: the service dir): docker build -f aiac/src/aiac/idp/service/configuration/keycloak/Dockerfile \ - -t localhost/aiac-pdp-config:local aiac/src/ + -t localhost/aiac-pdp-config:local \ + aiac/src/aiac/idp/service/configuration/keycloak/ kind load docker-image localhost/aiac-pdp-config:local --name # Restart the affected deployment: diff --git a/aiac/k8s/event-broker-deployment.yaml b/aiac/k8s/event-broker-deployment.yaml new file mode 100644 index 000000000..727807c81 --- /dev/null +++ b/aiac/k8s/event-broker-deployment.yaml @@ -0,0 +1,43 @@ +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: aiac-event-broker + namespace: aiac-system +spec: + replicas: 1 + selector: + matchLabels: + app: aiac-event-broker + template: + metadata: + labels: + app: aiac-event-broker + spec: + containers: + - name: nats + image: nats:latest + imagePullPolicy: Never + args: ["-js", "-sd", "/data/jetstream"] + ports: + - containerPort: 4222 + volumeMounts: + - name: jetstream-data + mountPath: /data/jetstream + volumes: + - name: jetstream-data + emptyDir: {} + +--- +apiVersion: v1 +kind: Service +metadata: + name: aiac-event-broker-service + namespace: aiac-system +spec: + selector: + app: aiac-event-broker + ports: + - name: nats + port: 4222 + targetPort: 4222 diff --git a/aiac/k8s/idp-configuration-keycloak-pod.yaml b/aiac/k8s/idp-configuration-keycloak-pod.yaml index f4f79973a..ecb88b5fb 100644 --- a/aiac/k8s/idp-configuration-keycloak-pod.yaml +++ b/aiac/k8s/idp-configuration-keycloak-pod.yaml @@ -3,7 +3,7 @@ # This manifest deploys the IdP Configuration Service as a standalone Pod for # isolated development and testing. It does NOT reflect the production topology. # -# In production both interface services run as containers in a single Kagenti Interface Pod +# In production both interface services run as containers in a single Rossoctl Interface Pod # defined in pdp-interface-deployment.yaml. --- diff --git a/aiac/k8s/opa-kind-driver.sh b/aiac/k8s/opa-kind-driver.sh new file mode 100755 index 000000000..070d998da --- /dev/null +++ b/aiac/k8s/opa-kind-driver.sh @@ -0,0 +1,580 @@ +#!/usr/bin/env bash +# opa-kind-driver.sh — execute aiac/k8s/opa-kind-runbook.md end-to-end. +# +# This is an automated driver for the AIAC "OPA Kind Cluster Runbook" +# (aiac/k8s/opa-kind-runbook.md). It runs every step of that runbook in +# order, prints each step and the result it obtained, prints the OPA `input` +# documents for BOTH the inbound and the outbound legs, and FAILS with a clear +# message the moment an observed result does not match the runbook's stated +# expectation (instead of silently continuing). +# +# What it does, mirroring the runbook 1:1: +# Step 1 enable OPA in both legs (opa-kind-enable.sh) +# Step 2 verify the starting point (pods, bundle-service, client-id) +# Part A inbound authorization +# A.1 dev-user token carries sub=dev-user +# A.2 baseline: dev-user AND alice both reach the app (HTTP 200) +# A.3 apply the client-scoped policy CR +# A.4 enforced: dev-user -> 200, alice -> 403 +# A.5 print the INBOUND OPA input + assert the decision result +# Part B outbound token-exchange + OPA +# B.1 add the github-tool outbound route +# B.2 grant github-agent the exchange scope (expect HTTP 204) +# B.3 restart github-agent to load the route +# B.4 outbound tools/list probe -> DENIED (JSON-RPC error frame at +# HTTP 200, or 403/503; outbound OPA present) +# B.5 print the OUTBOUND OPA input +# +# Requires: kubectl, helm, kind, python3, curl, and docker (or podman). +# Env vars (runbook defaults shown): +# OPERATOR_DIR path to the rossoctl/operator clone (default: ../operator) +# ROSSOCTL_DIR path to the rossoctl/rossoctl clone (default: ../rossoctl) +# NS agent namespace (default: team1) +# SYS_NS platform namespace (default: rossoctl-system) +# KC Keycloak base URL (default: http://keycloak.localtest.me:8080) +# REALM Keycloak realm (default: rossoctl) +# POLL_SECS max seconds to wait for OPA to poll a new bundle (default: 150). +# The OPA SDK bundle poller uses min_delay 10s / max_delay 120s, +# so a freshly applied CR can take up to ~120s to reach the +# agent; the default leaves margin above that worst case. +# SKIP_ENABLE if set to 1, skip Step 1's image rebuild + opa-kind-enable.sh +# and only verify OPA is already wired (fast path when iterating +# on the policy CR against an already-enabled cluster). +# KIND_CLUSTER name of the Kind cluster (default: rossoctl). +# Preflight uses this to re-export the kubeconfig if the cluster +# is unreachable — a Kind node reassigns its API-server host port +# on restart, which leaves the exported kubeconfig stale. +# +# Run from the repo root (cortex/): +# OPERATOR_DIR=../operator ROSSOCTL_DIR=../rossoctl ./aiac/k8s/opa-kind-driver.sh +# SKIP_ENABLE=1 ./aiac/k8s/opa-kind-driver.sh # skip the rebuild, just re-test + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +CORTEX_DIR="$(cd "$SCRIPT_DIR/../.." && pwd)" + +# ── Configuration (runbook defaults) ──────────────────────────────────────── +# Sibling repo clones the enable step needs; default to ../operator and +# ../rossoctl (relative to the cortex repo root) when not set, mirroring +# opa-kind-enable.sh. +OPERATOR_DIR="${OPERATOR_DIR:-$(cd "$CORTEX_DIR/../operator" 2>/dev/null && pwd || echo "")}" +ROSSOCTL_DIR="${ROSSOCTL_DIR:-$(cd "$CORTEX_DIR/../rossoctl" 2>/dev/null && pwd || echo "")}" + +NS="${NS:-team1}" +SYS_NS="${SYS_NS:-rossoctl-system}" +KC="${KC:-http://keycloak.localtest.me:8080}" +REALM="${REALM:-rossoctl}" +POLL_SECS="${POLL_SECS:-150}" +KIND_CLUSTER="${KIND_CLUSTER:-rossoctl}" + +AGENT_LABEL="app.kubernetes.io/name=github-agent" +EXPECTED_SPIFFE="spiffe://localtest.me/ns/${NS}/sa/github-agent" +POLICY_FILE="${CORTEX_DIR}/aiac/docs/examples/opa-team1-policy.yaml" +ENABLE_SCRIPT="${SCRIPT_DIR}/opa-kind-enable.sh" +RESTORE_SCRIPT="${SCRIPT_DIR}/opa-kind-restore.sh" + +# ── Output helpers ────────────────────────────────────────────────────────── +if [ -t 1 ]; then + C_RED=$'\033[31m'; C_GRN=$'\033[32m'; C_YEL=$'\033[33m' + C_CYN=$'\033[36m'; C_BLD=$'\033[1m'; C_RST=$'\033[0m' +else + C_RED=""; C_GRN=""; C_YEL=""; C_CYN=""; C_BLD=""; C_RST="" +fi + +STEP_N=0 +step() { STEP_N=$((STEP_N + 1)); printf '\n%s==> [%02d] %s%s\n' "$C_BLD$C_CYN" "$STEP_N" "$*" "$C_RST"; } +info() { printf ' %s\n' "$*"; } +pass() { printf ' %sPASS%s %s\n' "$C_GRN" "$C_RST" "$*"; } +warn() { printf ' %sWARN%s %s\n' "$C_YEL" "$C_RST" "$*"; } +die() { printf '\n%sFAIL:%s %s\n' "$C_RED$C_BLD" "$C_RST" "$*" >&2; exit 1; } + +# expect_eq