From 4e158bfa2ab2ecd1ef030da3057b0684cd4bca09 Mon Sep 17 00:00:00 2001 From: Tom Softreck Date: Sat, 19 Sep 2026 21:09:09 +0200 Subject: [PATCH 1/6] feat(mcp): import local servers as versioned native URI packages --- .governance/manifest.json | 7 +- Makefile | 7 +- docs/README.md | 1 + docs/information/mcp-uri-catalog.md | 271 +++++++ .../core/taskand.dev/v1/federation.mjs | 6 +- .../core/taskand.dev/v1/lifecycle.mjs | 18 +- .../registry/core/taskand.dev/v1/store.mjs | 46 +- generated/registry/registry.json | 6 +- mcp/.gitignore | 4 + mcp/bridge.py | 151 ++++ mcp/catalog.py | 223 ++++++ mcp/pyproject.toml | 9 + mcp/uv.lock | 728 ++++++++++++++++++ project/ticket-030/README.md | 23 + project/ticket-030/intent.json | 124 +++ tests/mcp_catalog_test.py | 214 +++++ tests/mcp_registry.test.mjs | 28 + 17 files changed, 1846 insertions(+), 20 deletions(-) create mode 100644 docs/information/mcp-uri-catalog.md create mode 100644 mcp/.gitignore create mode 100644 mcp/bridge.py create mode 100644 mcp/catalog.py create mode 100644 mcp/pyproject.toml create mode 100644 mcp/uv.lock create mode 100644 project/ticket-030/README.md create mode 100644 project/ticket-030/intent.json create mode 100644 tests/mcp_catalog_test.py create mode 100644 tests/mcp_registry.test.mjs diff --git a/.governance/manifest.json b/.governance/manifest.json index 96f7996..17de095 100644 --- a/.governance/manifest.json +++ b/.governance/manifest.json @@ -161,7 +161,12 @@ "generated/mcp/**", "packages/taskand-mcp-control/**", "tests/mcp_control_test.py", - "tests/fixtures/mcp_control_server.py" + "tests/fixtures/mcp_control_server.py", + "mcp/**", + "generated/registry/registry.json", + "tests/mcp_catalog_test.py", + "tests/mcp_registry.test.mjs", + "generated/registry/core/taskand.dev/v1/**" ] }, "interfaces": { diff --git a/Makefile b/Makefile index 1af57c6..7a2f5ea 100644 --- a/Makefile +++ b/Makefile @@ -14,7 +14,12 @@ status: docker compose ps @./bin/taskand status -test: conformance test-contracts test-negative test-twin test-web-twin test-cli +test: conformance test-contracts test-negative test-twin test-web-twin test-cli test-mcp + +.PHONY: test-mcp +test-mcp: + @uv run --frozen --project mcp python tests/mcp_catalog_test.py + @node --test tests/mcp_registry.test.mjs test-cli: @node --test tests/cli_routing.test.mjs diff --git a/docs/README.md b/docs/README.md index 297c85b..d55e3cb 100644 --- a/docs/README.md +++ b/docs/README.md @@ -47,3 +47,4 @@ standardu w CI i nie stanowi odbioru opisywanej funkcjonalności. - [Sekrety](SECRETS.md). - [Sterowanie MCP przez URI Taskand](information/mcp-process-control.md) — profile serwerów, dopuszczenia narzędzi, trwałe wyniki i izolowany pilot. +- [Lokalne MCP jako procesy URI](information/mcp-uri-catalog.md) — importer, katalog 45 serwerów, naprawione środowiska i testy pilota. diff --git a/docs/information/mcp-uri-catalog.md b/docs/information/mcp-uri-catalog.md new file mode 100644 index 0000000..f496898 --- /dev/null +++ b/docs/information/mcp-uri-catalog.md @@ -0,0 +1,271 @@ +--- +{ + "schema": "wellmanifest.docs/document/v1", + "id": "mcp-uri-catalog", + "kind": "information", + "version": 1, + "title": "Lokalne MCP jako natywne procesy URI", + "status": "proposed", + "owner": "semcod/taskand-glm53", + "created": "2026-09-19", + "updated": "2026-09-19", + "review_after": "2026-09-26", + "source_revision": "8804583ef82342ad7870659cf79e9ad8278e2369", + "affected_repositories": [ + "semcod/taskand-glm53" + ], + "evidence": [ + "repo://semcod/taskand-glm53/project/ticket-030/intent.json", + "repo://semcod/taskand-glm53/mcp/catalog.py", + "repo://semcod/taskand-glm53/tests/mcp_catalog_test.py", + "repo://semcod/taskand-glm53/tests/mcp_registry.test.mjs" + ] +} +--- + +# Lokalne MCP jako natywne procesy URI + + +## Cel + +Udostępnić narzędzia lokalnych serwerów MCP przez istniejący rejestr i gateway Taskand. +Importer tworzy samodzielne pakiety w `mcp/`, zgodne z układem `generated/**`. +Wynik ticket-030: 45 dostępnych serwerów, 422 zarejestrowane narzędzia. +Każdy pakiet ma wersjonowany URI; import rejestruje kandydatów. + + +## Zakres obserwacji + +Pomiar z 2026-09-19 obejmuje konfiguracje Codex, Cursor, Claude Desktop, +Claude CLI i VS Code na tym hoście oraz cztery doinstalowane implementacje +z repozytorium [modelcontextprotocol/servers](https://github.com/modelcontextprotocol/servers). +To wszystkie unikalne nazwy w tych konfiguracjach, nie katalog wszystkich MCP w internecie. +Sprawdzono również konfiguracje projektowe Claude; nie dodawały serwerów. +46 nazw obejmuje 45 serwerów oraz wykluczony adapter `taskand`, którego ponowny +import tworzyłby rekurencyjne połączenie Taskand → MCP → Taskand. + +Powtarzające się nazwy mają jawny priorytet: naprawiony profil, Codex, Cursor, +Claude Desktop, Claude CLI, VS Code, dodatkowe serwery. `--prefer-first` zapisuje +pominięte lokalizacje w raporcie; bez tej opcji sprzeczne konfiguracje są błędem. Niedostępny serwer zachowuje +częściowy raport i powoduje niezerowy kod zakończenia importera. +Nie zmieniono konfiguracji klientów ani kodu repozytoriów serwerów. + +Ticket-029/PR #38 pozostaje osobną zmianą kontrolera MCP. Po autoryzowanym +przekazaniu jego rezerwacji pracy nowy zakres przydzielono ticket-030. +Gateway 8077 i pilot 8082 zachowują swój dotychczasowy stan. + + +## Implementacja i użycie + +`mcp/catalog.py` odczytuje zaufane konfiguracje operatora, wykonuje inicjalizację +MCP i `tools/list`, a następnie projektuje kontrakty na pakiety: + +```text +mcp/ + catalog.py, bridge.py, pyproject.toml, uv.lock + mcp--/ + registry.json + -/taskand.dev/v1/ + proc.yaml + bin.mjs + bridge.py + tool.json +``` + +Przykład: +`proc://taskand.dev/mcp-filesystem-cbf61858/read-text-file-e862d61b/v1`. +Hash w nazwie zapobiega kolizjom po normalizacji znaków. Przestrzeń `mcp-*` +ma jeden katalog główny; istniejące procesy nadal korzystają z `generated/`. +Rejestr eksportuje `inputSchema`, opcjonalne `outputSchema` oraz metadane MCP. +Pakiety i prywatne profile są niezmienne: zmiana wymaga nowej wersji oraz nowego +pliku profili. Ponowne wykonanie identycznego importu jest idempotentne. + +Przykład importu z pliku JSON `mcpServers` lub TOML `mcp_servers`: + +```bash +uv sync --frozen --project mcp +uv run --frozen --project mcp python mcp/catalog.py \ + --config /absolute/path/client-config.json \ + --output /absolute/path/taskand-release/mcp \ + --profiles /absolute/private/state/profiles-v1.json \ + --report /absolute/private/state/import-v1.json --register +``` + +`--config` można powtarzać; `--only NAZWA` ogranicza import. Przy zmianie +kontraktu należy użyć np. `--version 2`. Katalog nadrzędny `mcp/` musi zawierać +Taskand z tą wersją registry/core. Profile zawierają konfigurację uruchomienia +oraz wymagane wartości środowiska, pozostają poza repozytorium z trybem `0600`. +Pakiety `mcp/mcp-*/` są ignorowanymi artefaktami wdrożenia, odtwarzanymi importerem; +Git przechowuje kompilator, lock zależności, integrację rejestru i testy. + +Gateway potrzebuje `TASKAND_MCP_PYTHON` wskazującego absolutną ścieżkę interpretera +z zależnościami `mcp/uv.lock` oraz `TASKAND_MCP_PROFILES` wskazującego właściwy +prywatny snapshot. Sam import nie przyznaje praw do wykonywania narzędzi. +Operator dopuszcza wybrany URI istniejącą akcją rejestru `approve` i nadaje grant +`call` temu URI. Zwykłe wywołanie korzysta z `/api/proc/call`: + +```json +{"uri":"proc://taskand.dev/mcp-filesystem-cbf61858/read-text-file-e862d61b/v1", + "data":{"path":"/absolute/allowed/workspace/example.txt"},"timeout":35} +``` + +Most używa oficjalnego SDK: stdio oraz Streamable HTTP. Sprawdza schemat wejścia, +odcisk profilu i świeży kontrakt narzędzia przed wywołaniem; waliduje też wynik +strukturalny, jeśli serwer deklaruje jego schemat. Nie pobiera zewnętrznych +referencji JSON Schema. Limity: 500 narzędzi/serwer, katalog 1 MiB, +wejście/wynik 256 KiB, wywołanie MCP 25 s, wrapper 30 s. HTTPS jest dopuszczony; +zwykły HTTP wyłącznie na loopback. Brak obsługi nagłówków uwierzytelniających +HTTP jest zgłaszany jawnie. Wyjątek po wysłaniu wywołania daje +`OUTCOME_UNKNOWN`, `outcomeKnown:false`, bez automatycznego ponowienia. +`taskSuccessVerified:false` odróżnia odpowiedź narzędzia od niezależnego odbioru zadania. + +## Naprawione środowiska i źródła + +| Serwer | Potwierdzony problem i rozwiązanie | +| --- | --- | +| diff-dsl | Nieistniejący interpreter z konfiguracji Cursor. Osobna kopia `git archive` rewizji `c47973075671047a9d4be413d3e259bb9a5ebca5`, uruchamiana jako `python -m diff_dsl.mcp_server`. Ta przypięta rewizja udostępnia 2 narzędzia; roboczy katalog źródłowy udostępniał 4. | +| tillm | Brak lokalnego `.venv/bin/mcp2tillm`; drugi znaleziony launcher również importował brakujący moduł. Osobny runtime z rewizji `26c87bfcdd6f2c26f34035cc2a987ec9443d9e44`, z pakietami tillm, dsl2tillm, nlp2tillm i mcp2tillm. | +| microsoft/markitdown | `markitdown-mcp==0.0.1a4` używa MCP 1.8.x i nie obsługuje poprawnie sondy `server/discover`. Osobny runtime i jawne `taskand_protocol_mode: legacy` przywracają inicjalizację. | + +Dodatkowe implementacje mają przypięte wersje potwierdzone w +[wydaniach projektu](https://github.com/modelcontextprotocol/servers/releases): + +| Serwer | Zainstalowany pakiet | Ograniczenie lokalnego profilu | +| --- | --- | --- | +| filesystem | `@modelcontextprotocol/server-filesystem@2026.8.31` | Osobny katalog testowy. | +| memory | `@modelcontextprotocol/server-memory@2026.8.31` | Osobny plik grafu pamięci. | +| git | `mcp-server-git==2026.8.18` | Repozytorium ticket-030; test tylko odczytu statusu. | +| fetch | `mcp-server-fetch==2026.8.18` | Test pobiera publiczny README źródłowego projektu. | + +npm zainstalowano z `--save-exact --ignore-scripts`; Python w osobnych środowiskach uv. +Lock npm i środowiska serwerów pozostają lokalnymi artefaktami instalacji. +Import istniejącej konfiguracji przypina polecenie, argumenty, środowisko i schemat; +nie zamraża całego drzewa zależności ani zawartości każdej istniejącej instalacji. +W szczególności konfiguracje operatora zawierające ruchome wersje nadal wymagają +kontrolowanej aktualizacji. Pin profilu nie jest podpisem ani izolacją systemową. + +## Katalog lokalny + +| Serwer | Narzędzia | +| --- | ---: | +| algitex-aider | 3 | +| algitex-code2llm | 4 | +| algitex-planfile | 6 | +| algitex-proxym | 4 | +| algitex-vallm | 6 | +| chrome-devtools | 29 | +| code2logic | 4 | +| code2schema | 4 | +| curllm | 7 | +| data2dsl | 4 | +| diff-dsl | 2 | +| doql | 12 | +| env2llm | 7 | +| fetch | 1 | +| filesystem | 14 | +| gillm | 7 | +| git | 12 | +| hillm | 3 | +| ifuri-marketing | 14 | +| imgl | 3 | +| intract | 7 | +| iterun | 10 | +| koru | 36 | +| llx | 17 | +| memory | 9 | +| microsoft/markitdown | 1 | +| monag | 11 | +| nexu | 16 | +| nlp2cmd | 5 | +| nlp2dsl | 4 | +| nlp2env | 19 | +| nlp2uri | 15 | +| op3 | 4 | +| planfile | 9 | +| pycharm | 42 | +| redup | 8 | +| subactor-search | 3 | +| sumd | 13 | +| testql | 9 | +| text2dsl | 3 | +| tillm | 6 | +| todomat | 6 | +| toonic | 5 | +| vallm | 7 | +| vql | 11 | + + +## Walidacja i uruchomiony pilot + +- Dotychczasowy adapter Taskand MCP: 9/9 testów; kontroler ticket-029: 11/11. +- Pierwszy `make test` przeszedł: konformacja 4/4, kontrakty 30/30, negatywne 17/17, + twin 9, web twin 8, CLI 7, MCP Python 10 i rejestr MCP Node 1. + Końcowy `test-mcp` po rozszerzeniu CLI: Python 11 i Node 1, wszystkie przeszły. + Powtórny pełny test po rejestracji 422 kandydatów ujawnił 2 błędy kontraktów + doctor/prescribe i doctor/heal (28/30): duży JSON jest ucinany przez natychmiastowe + zakończenie procesu. Wymagana osobna naprawa ograniczona do tej regresji. +- `./project/governance-check.sh`: zero błędów i ostrzeżeń. +- Testy MCP uruchamiają prawdziwe serwery SDK stdio/HTTP i rzeczywisty rejestr; + sprawdzają schematy, zmianę profilu/kontraktu, kandydaturę/dopuszczenie, + niezmienność pakietów, kolizje nazw, symlinki oraz brak ponowienia niepewnego efektu. +- Pełna droga adapter Taskand MCP → gateway 8084 → natywny URI → filesystem MCP + przeszła test; `describe_process` udostępnia schemat, a `call_process` zwraca + oczekiwaną zawartość znanego pliku. +- Import 45 serwerów zakończył się bez niedostępnych integracji i zarejestrował + 422 kandydatów w roboczym `mcp/`. +- Pilot `http://127.0.0.1:8084`, jednostka użytkownika + `taskand-mcp030-pilot.service`, ma 7 dopuszczonych narzędzi i 415 kandydatów. + Profile, token i surowe odpowiedzi przechowuje prywatny magazyn hosta. +- Rzeczywiste wywołania URI: odczyt pliku filesystem, odczyt memory, + status git, fetch publicznego README, konwersja lokalnego HTML MarkItDown, + tillm health i analiza dwóch manifestów diff-dsl. Odpowiedzi sprawdzono + względem znanych danych testowych; health tillm jest diagnostyką zależności. +- Pierwsza analiza diff-dsl dostała pusty katalog bez wymaganych dwóch manifestów. + Serwer zwrócił błąd JSON-RPC, zaklasyfikowany zachowawczo jako `OUTCOME_UNKNOWN`. + Po odczycie źródła potwierdzono odczytowy charakter operacji i wykonano nowy + test na dwóch odrębnych, poprawnych manifestach. Nie ponawiano go automatycznie. + +Powtarzalne dowody są w testach repozytorium. Lokalne obserwacje wdrożenia: +- `final-import.json`: SHA-256 `f75187b4f538fcff8e529513e0242f03274330286b6aea61fc31438cac3d4b51`. +- `gateway-canaries-verified.json`: SHA-256 `d4ab73a6998e2691636e36d72cb848a9434aae605c1218ffd3223e90ece28b93`. + +Magazyn operacyjny: `~/.local/state/taskand/mcp-local-import-20260919/`; +instalacje i pilot: `~/.local/share/taskand/mcp-import/`. +Te ścieżki pomagają operatorowi lokalnemu; nie zastępują dostępnego w repo kodu +ani niezależnych dowodów publikacji. Rewizja w metadanych wskazuje bazę zmiany; +implementację identyfikuje commit zawierający ten dokument. + + +## Ograniczenia odbioru + +`tools/list` sprawdzono dla wszystkich 45 serwerów; logikę narzędzi sprawdzono +na siedmiu wybranych wywołaniach, nie na wszystkich 422. Nie wykonano działań +płatnych, publikacji ani arbitralnych operacji zapisu z importowanych katalogów. +Tillm health zgłasza brak opcjonalnych pakietów nlp2dsl, intract, redsl, proxym, +llx i env2llm w jego izolowanym środowisku. Nie dowodzi to gotowości jego +komend wykonujących zadania; odpowiednie samodzielne MCP są osobno w katalogu. + +Most uruchamia zaufane programy operatora pod jego kontem; nie jest sandboxem +systemowym. Schemat MCP ani adnotacja readOnly nie przyznają uprawnień. +Utrata odpowiedzi może pozostawić efekt i wymaga uzgodnienia stanu przed kolejnym +wywołaniem. Hostowe profile mogą zawierać sekrety i nie trafiają do Git. + +Integracja nie jest scalona z main. Dotychczasowy PR #38 czeka na chronione +lokalne CI: brak `/run/onedev-docker-gate/taskand-browser.socket`, zależność +[onedev-agent #304](https://github.com/subactor/onedev-agent/issues/304). +Nowa implementacja nie obchodzi tej kontroli. Audyt dokumentacji wykazuje +cztery istniejące wcześniej problemy metadanych/placeholderów w innych dokumentach; +zapisano je w kolejce PLF-010. Nowy dokument nie dodaje takiego problemu. + + +## Obsługa i dalsza dostawa + +Dodatkowe narzędzia dopuszczać indywidualnie po ocenie konkretnego działania; +import sam zachowuje stan candidate. Zmiany konfiguracji importować jako nowe +wersje z osobnym snapshotem profili. Pełna publikacja wymaga chronionego +przeglądu i działającego lokalnego wykonawcy CI. + +Rollback pilota: `systemctl --user stop taskand-mcp030-pilot.service`. +Zachować katalog pilota, profile i receipts do uzgodnienia efektów; istniejące +instancje 8077/8082 pozostają niezależne. Jednostka jest lokalnym pilotem sesji, +nie deklaracją wdrożenia produkcyjnego ani konfiguracji startu po restarcie hosta. diff --git a/generated/registry/core/taskand.dev/v1/federation.mjs b/generated/registry/core/taskand.dev/v1/federation.mjs index 97354bd..6d02079 100644 --- a/generated/registry/core/taskand.dev/v1/federation.mjs +++ b/generated/registry/core/taskand.dev/v1/federation.mjs @@ -8,7 +8,11 @@ import { register } from './lifecycle.mjs'; export function exportCatalog() { const processes = allEntries() .filter(e => e.status === 'active') - .map(({ uri, organism, desc, kind, hash, origin, files }) => ({ uri, organism, desc, kind, hash, origin, files })); + .map(({ uri, organism, desc, kind, hash, origin, files, inputSchema, outputSchema, mcp }) => ({ + uri, organism, desc, kind, hash, origin, files, + ...(inputSchema ? { inputSchema } : {}), ...(outputSchema ? { outputSchema } : {}), + ...(mcp ? { mcp } : {}) + })); return { ok: true, standard: 'taskand-registry/1', node: NODE_ID, generated: new Date().toISOString(), processes }; } diff --git a/generated/registry/core/taskand.dev/v1/lifecycle.mjs b/generated/registry/core/taskand.dev/v1/lifecycle.mjs index 1a74db3..0d2897b 100644 --- a/generated/registry/core/taskand.dev/v1/lifecycle.mjs +++ b/generated/registry/core/taskand.dev/v1/lifecycle.mjs @@ -1,11 +1,12 @@ // Cykl życia wpisu: register (candidate|active wg polityki) → approve → deprecate; verify i scan -import { readdirSync, existsSync } from 'node:fs'; +import { readdirSync, existsSync, readFileSync } from 'node:fs'; import { join, relative } from 'node:path'; -import { ROOT, GEN, parseUri, organisms, allEntries, findEntry, updateRegistry, policy, addToGenome, audit } from './store.mjs'; +import { ROOT, organismDir, parseUri, organisms, allEntries, findEntry, updateRegistry, policy, addToGenome, audit } from './store.mjs'; import { packageFiles, packageHash, readManifest, checkPackage } from './package.mjs'; // builtin → active; evolved → wg policy.evolution (auto|manual); peer:* → wg policy.peers (domyślnie manual) function initialStatus(origin) { + if (origin === 'mcp') return 'candidate'; if (origin.startsWith('peer:')) return policy('peers', 'manual') === 'auto' ? 'active' : 'candidate'; if (origin === 'evolved') return policy('evolution', 'manual') === 'auto' ? 'active' : 'candidate'; return 'active'; @@ -24,6 +25,10 @@ export function register({ uri, origin, hold = false }) { if (existing) return { ok: true, entry: existing, unchanged: true }; const m = readManifest(loc.dir); const src = origin || m.origin || 'builtin'; + const contract = src === 'mcp' ? JSON.parse(readFileSync(join(loc.dir, 'tool.json'), 'utf8')) : null; + if (contract && (contract.uri !== uri || !contract.tool?.inputSchema || typeof contract.tool.inputSchema !== 'object')) { + return { ok: false, error: 'Invalid MCP tool contract' }; + } const entry = { uri, organism: loc.organism, @@ -37,7 +42,10 @@ export function register({ uri, origin, hold = false }) { files: packageFiles(loc.dir), hash, status: hold ? 'candidate' : initialStatus(src), - registered: new Date().toISOString() + registered: new Date().toISOString(), + ...(contract ? { inputSchema: contract.tool.inputSchema, + ...(contract.tool.outputSchema ? { outputSchema: contract.tool.outputSchema } : {}), + mcp: { server: contract.server, tool: contract.tool.name, profilePin: contract.profilePin } } : {}) }; updateRegistry(loc.organism, reg => { reg.processes[uri] = entry; @@ -110,8 +118,8 @@ export function verify() { // Pakiety na dysku bez wpisu w rejestrze; {adopt:true} rejestruje je (origin z proc.yaml) export function scan({ adopt = false } = {}) { const found = organisms().flatMap(org => - readdirSync(join(GEN, org), { withFileTypes: true }).filter(d => d.isDirectory()).flatMap(cap => { - const base = join(GEN, org, cap.name, 'taskand.dev'); + readdirSync(organismDir(org), { withFileTypes: true }).filter(d => d.isDirectory()).flatMap(cap => { + const base = join(organismDir(org), cap.name, 'taskand.dev'); return existsSync(base) ? readdirSync(base).map(v => `proc://taskand.dev/${org}/${cap.name}/${v}`) : []; }) ); diff --git a/generated/registry/core/taskand.dev/v1/store.mjs b/generated/registry/core/taskand.dev/v1/store.mjs index fa10de5..2105a48 100644 --- a/generated/registry/core/taskand.dev/v1/store.mjs +++ b/generated/registry/core/taskand.dev/v1/store.mjs @@ -1,11 +1,12 @@ // Rejestry organizmów: generated//registry.json (jedyny zapisujący: registry/core, z blokadą) -import { readFileSync, writeFileSync, readdirSync, existsSync, openSync, closeSync, unlinkSync, statSync, renameSync, appendFileSync, chownSync } from 'node:fs'; +import { readFileSync, writeFileSync, readdirSync, existsSync, openSync, closeSync, unlinkSync, statSync, lstatSync, renameSync, appendFileSync, chownSync } from 'node:fs'; import { fileURLToPath } from 'node:url'; import { join } from 'node:path'; import { hostname } from 'node:os'; export const ROOT = fileURLToPath(new URL('../../../../..', import.meta.url)).replace(/\/$/, ''); export const GEN = join(ROOT, 'generated'); +export const MCP = join(ROOT, 'mcp'); export const NODE_ID = process.env.TASKAND_NODE || hostname(); const GENOME = join(ROOT, 'genome.yaml'); const URI_RE = /^proc:\/\/taskand\.dev\/([a-z0-9][a-z0-9-]*)\/([a-z0-9][a-z0-9-]*)\/(v[0-9]+)$/; @@ -17,10 +18,31 @@ export function parseUri(uri) { const m = URI_RE.exec(String(uri || '')); if (!m) return null; const [, organism, capability, version] = m; - return { uri, organism, capability, version, dir: join(GEN, organism, capability, 'taskand.dev', version) }; + const root = organismDir(organism); + const dir = join(root, capability, 'taskand.dev', version); + for (const path of [join(root, capability), join(root, capability, 'taskand.dev'), dir]) rejectSymlink(path); + return { uri, organism, capability, version, dir }; } -const registryFile = organism => join(GEN, organism, 'registry.json'); +function rejectSymlink(path) { + try { + if (lstatSync(path).isSymbolicLink()) throw new Error('REGISTRY_SYMLINK'); + } catch (error) { + if (error.code !== 'ENOENT') throw error; + } +} + +// The reserved mcp-* namespace has one canonical root, never a fallback path. +export function organismDir(organism) { + if (!/^[a-z0-9][a-z0-9-]*$/.test(organism)) throw new Error('INVALID_ORGANISM'); + const root = organism.startsWith('mcp-') ? MCP : GEN; + const dir = join(root, organism); + for (const path of [root, dir]) rejectSymlink(path); + if (organism.startsWith('mcp-') && existsSync(join(GEN, organism))) throw new Error('MCP_NAMESPACE_COLLISION'); + return dir; +} + +const registryFile = organism => join(organismDir(organism), 'registry.json'); export function readRegistry(organism) { try { @@ -31,10 +53,16 @@ export function readRegistry(organism) { } export function organisms() { - return readdirSync(GEN, { withFileTypes: true }) - .filter(d => d.isDirectory() && /^[a-z0-9]/.test(d.name)) - .map(d => d.name) - .sort(); + const names = [GEN, MCP].flatMap(root => { + if (!existsSync(root)) return []; + if (lstatSync(root).isSymbolicLink()) throw new Error('REGISTRY_SYMLINK'); + return readdirSync(root, { withFileTypes: true }) + .filter(d => d.isDirectory() && /^[a-z0-9][a-z0-9-]*$/.test(d.name) + && (root === MCP ? d.name.startsWith('mcp-') : true)) + .map(d => d.name); + }); + for (const name of names) organismDir(name); + return names.sort(); } export function allEntries() { @@ -61,7 +89,7 @@ const sleep = ms => Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, // Zapis rejestru organizmu pod blokadą O_EXCL (równoległe ewolucje nie nadpisują się nawzajem) export function updateRegistry(organism, mutate) { - const lock = join(GEN, organism, '.registry.lock'); + const lock = join(organismDir(organism), '.registry.lock'); const deadline = Date.now() + LOCK_WAIT_MS; let fd; while (fd === undefined) { @@ -84,7 +112,7 @@ export function updateRegistry(organism, mutate) { const tmp = `${registryFile(organism)}.${process.pid}.tmp`; writeFileSync(tmp, JSON.stringify(reg, null, 2) + '\n'); renameSync(tmp, registryFile(organism)); - adoptOwnership(join(GEN, organism)); + adoptOwnership(organismDir(organism)); return result; } finally { closeSync(fd); diff --git a/generated/registry/registry.json b/generated/registry/registry.json index f846cf3..7b33aba 100644 --- a/generated/registry/registry.json +++ b/generated/registry/registry.json @@ -22,12 +22,12 @@ "store.mjs", "vault.mjs" ], - "hash": "sha256:b62cc9bd58d213ddac6e2f8e4376e39fb57b3b75acfb244476d37c9c333a942d", + "hash": "sha256:257418b5bd5995e2fdbb4a6f41771a5a5af0d85d8f62a68601213ec8da71b93c", "status": "active", "registered": "2026-09-12T19:09:43.334Z", - "refreshedAt": "2026-09-13T07:02:41.464Z", + "refreshedAt": "2026-09-19T18:35:44.561Z", "capability": "Rejestr węzła: wywołania przez URI (broker env/sekretów), cykl życia pakietów, wymiana rejestrów z peerami" } }, - "updated": "2026-09-13T07:02:41.464Z" + "updated": "2026-09-19T18:35:44.561Z" } diff --git a/mcp/.gitignore b/mcp/.gitignore new file mode 100644 index 0000000..67d68ba --- /dev/null +++ b/mcp/.gitignore @@ -0,0 +1,4 @@ +.venv/ +__pycache__/ +# Materialized host-specific packages belong to a deployment, not the source tree. +mcp-*/ diff --git a/mcp/bridge.py b/mcp/bridge.py new file mode 100644 index 0000000..d7f5255 --- /dev/null +++ b/mcp/bridge.py @@ -0,0 +1,151 @@ +"""One bounded MCP call. Copied into each immutable native process package.""" +import asyncio +import hashlib +import json +import os +from pathlib import Path +import sys +from urllib.parse import urlsplit + +from jsonschema import Draft202012Validator +from mcp import Client, StdioServerParameters + +LIMIT = 262144 + + +class ContractError(Exception): + pass + + +def require(condition, code): + if not condition: + raise ContractError(code) + + +def canonical(value): + return json.dumps(value, sort_keys=True, separators=(",", ":"), ensure_ascii=True, allow_nan=False) + + +def digest(value): + return hashlib.sha256(canonical(value).encode()).hexdigest() + + +def load_json(path, limit=1048576): + path = Path(path) + require(not any(p.is_symlink() for p in (path, *path.parents)), "SYMLINK_REJECTED") + with path.open("rb") as stream: + raw = stream.read(limit + 1) + require(len(raw) <= limit, "FILE_TOO_LARGE") + return json.loads(raw) + + +def validate_schema(schema): + def walk(value): + if isinstance(value, dict): + for key, child in value.items(): + if key in {"$ref", "$dynamicRef"}: + require(isinstance(child, str) and child.startswith("#"), "EXTERNAL_SCHEMA_REFERENCE") + walk(child) + elif isinstance(value, list): + for child in value: + walk(child) + walk(schema) + Draft202012Validator.check_schema(schema) + + +def client(profile): + require(isinstance(profile, dict), "INVALID_PROFILE") + mode = profile.get("protocolMode", "auto") + require(mode in {"auto", "legacy"}, "INVALID_PROTOCOL_MODE") + if profile.get("transport") == "stdio": + command = profile.get("command") + require(isinstance(command, str) and Path(command).is_absolute(), "ABSOLUTE_COMMAND_REQUIRED") + require(isinstance(profile.get("args", []), list) and all(isinstance(v, str) for v in profile.get("args", [])), "INVALID_ARGV") + env = profile.get("env", {}) + require(isinstance(env, dict) and all(isinstance(k, str) and isinstance(v, str) for k, v in env.items()), "INVALID_ENV") + target = StdioServerParameters(command=command, args=profile.get("args", []), env=env, cwd=profile.get("cwd")) + else: + require(profile.get("transport") == "http", "UNSUPPORTED_TRANSPORT") + target = profile.get("url", "") + parsed = urlsplit(target) + require(parsed.scheme == "https" or (parsed.scheme == "http" and parsed.hostname in {"127.0.0.1", "localhost", "::1"}), "INVALID_ENDPOINT") + require(not parsed.username and not parsed.password and not parsed.fragment, "INVALID_ENDPOINT") + # This catalog does not silently discard HTTP credentials. + require(not profile.get("headers"), "HTTP_AUTH_UNSUPPORTED") + return Client(target, read_timeout_seconds=20, mode=mode) + + +async def tool_catalog(session): + tools, cursor, seen = [], None, set() + while True: + page = await session.list_tools(cursor=cursor) + tools.extend(t.model_dump(by_alias=True, exclude_none=True) for t in page.tools) + require(len(tools) <= 500 and len(canonical(tools).encode()) <= 1048576, "CATALOG_TOO_LARGE") + cursor = page.next_cursor + if not cursor: + break + require(cursor not in seen, "INVALID_PAGINATION") + seen.add(cursor) + names = [t["name"] for t in tools] + require(len(names) == len(set(names)), "DUPLICATE_TOOL_NAME") + return sorted(tools, key=lambda t: t["name"]) + + +async def invoke(descriptor, profiles, arguments): + dispatched = False + try: + require(isinstance(arguments, dict), "ARGUMENTS_OBJECT_REQUIRED") + require(len(canonical(arguments).encode()) <= LIMIT, "INPUT_TOO_LARGE") + profile = profiles.get(descriptor["server"]) + require(profile is not None and digest(profile) == descriptor["profilePin"], "PROFILE_CHANGED") + expected = descriptor["tool"] + validate_schema(expected["inputSchema"]) + require(not list(Draft202012Validator(expected["inputSchema"]).iter_errors(arguments)), "ARGUMENT_SCHEMA_INVALID") + if "outputSchema" in expected: + validate_schema(expected["outputSchema"]) + async with asyncio.timeout(25): + async with client(profile) as session: + tools = await tool_catalog(session) + actual = next((t for t in tools if t["name"] == expected["name"]), None) + require(actual is not None and digest(actual) == digest(expected), "TOOL_SCHEMA_CHANGED") + dispatched = True + response = await session.call_tool(expected["name"], arguments) + result = response.model_dump(by_alias=True, exclude_none=True) + require(len(canonical(result).encode()) <= LIMIT, "RESULT_TOO_LARGE") + if "outputSchema" in expected and not response.is_error: + require(response.structured_content is not None and not list(Draft202012Validator(expected["outputSchema"]).iter_errors(response.structured_content)), "OUTPUT_SCHEMA_INVALID") + value = {"ok": not response.is_error, "result": result, "outcomeKnown": True, "taskSuccessVerified": False} + if response.is_error: + value["errorType"] = "MCP_TOOL_ERROR" + return value + except Exception as error: + return {"ok": False, "errorType": "OUTCOME_UNKNOWN" if dispatched else error_code(error), + "outcomeKnown": not dispatched, "retried": False} + + +def error_code(error): + if isinstance(error, ContractError): + return str(error) + for child in getattr(error, "exceptions", ()): + code = error_code(child) + if code != "MCP_TRANSPORT_ERROR": + return code + return "MCP_TRANSPORT_ERROR" + + +def main(): + try: + descriptor = load_json(Path(__file__).with_name("tool.json")) + path = Path(os.environ["TASKAND_MCP_PROFILES"]) + require(path.is_absolute() and path.stat().st_mode & 0o077 == 0, "PRIVATE_PROFILES_REQUIRED") + profiles = load_json(path, 4194304) + raw = sys.stdin.buffer.read(LIMIT + 1) + require(len(raw) <= LIMIT, "INPUT_TOO_LARGE") + value = asyncio.run(invoke(descriptor, profiles, json.loads(raw))) + except Exception as error: + value = {"ok": False, "errorType": error_code(error), "outcomeKnown": True, "retried": False} + print(canonical(value)) + + +if __name__ == "__main__": + main() diff --git a/mcp/catalog.py b/mcp/catalog.py new file mode 100644 index 0000000..d050e52 --- /dev/null +++ b/mcp/catalog.py @@ -0,0 +1,223 @@ +"""Import trusted local MCP configurations into versioned Taskand URI packages. + +Discovery never calls tools. Registration creates candidates; URI approval and +gateway grants remain separate operator actions. +""" +import argparse +import asyncio +import hashlib +import json +import os +from pathlib import Path +import re +import shutil +import subprocess +import sys +import tempfile +import tomllib + +from bridge import canonical, client, digest, error_code, load_json, require, tool_catalog, validate_schema + +HERE = Path(__file__).resolve().parent +WRAPPER = '''#!/usr/bin/env node +import { readFileSync } from 'node:fs'; +import { fileURLToPath } from 'node:url'; +import { spawnSync } from 'node:child_process'; +const python = process.env.TASKAND_MCP_PYTHON; +let result; +if (!python?.startsWith('/')) result = {ok:false,errorType:'MCP_PYTHON_REQUIRED',outcomeKnown:true}; +else { + const child = spawnSync(python, [fileURLToPath(new URL('./bridge.py', import.meta.url))], { + input:readFileSync(0), encoding:'utf8', timeout:30000, maxBuffer:524288, env:process.env + }); + try { result = JSON.parse(child.stdout); } + catch { result = {ok:false,errorType:'OUTCOME_UNKNOWN',outcomeKnown:false,retried:false}; } +} +process.stdout.write(JSON.stringify(result)+'\\n'); +''' + + +def slug(name): + require(isinstance(name, str) and bool(name), "INVALID_NAME") + base = re.sub(r"[^a-z0-9]+", "-", name.lower()).strip("-")[:48] or "tool" + return base + "-" + hashlib.sha256(name.encode()).hexdigest()[:8] + + +def configurations(path): + path = Path(path) + raw = path.read_bytes() + require(len(raw) <= 4194304, "CONFIG_TOO_LARGE") + data = tomllib.loads(raw.decode()) if path.suffix == ".toml" else json.loads(raw) + servers = data.get("mcp_servers", data.get("mcpServers", data.get("servers"))) + require(isinstance(servers, dict), "MCP_SERVERS_REQUIRED") + return servers + + +def merged_configurations(paths, prefer_first=False): + result, aliases = {}, {} + for path in paths: + for name, spec in configurations(path).items(): + if name in result: + require(prefer_first or result[name] == spec, "CONFIGURATION_NAME_CONFLICT") + aliases.setdefault(name, []).append(str(path)) + else: + result[name] = spec + return result, aliases + + +def normalize(name, spec): + require(isinstance(spec, dict), "INVALID_CONFIGURATION") + require(spec.get("enabled", True) is True, "DISABLED") + # A Taskand-to-Taskand adapter would re-export the same catalog recursively. + require(name != "taskand" and Path(spec.get("command", "")).name != "taskand-mcp" + and "TASKAND_MCP_GATEWAY_URL" not in spec.get("env", {}), "SELF_REFERENCE_EXCLUDED") + mode = spec.get("taskand_protocol_mode", "auto") + require(mode in {"auto", "legacy"}, "INVALID_PROTOCOL_MODE") + if spec.get("url"): + require(not spec.get("http_headers") and not spec.get("env_http_headers") + and not spec.get("bearer_token_env_var"), "HTTP_AUTH_UNSUPPORTED") + return {"transport": "http", "url": spec["url"], "protocolMode": mode} + command = shutil.which(spec.get("command", "")) + require(command is not None, "COMMAND_NOT_FOUND") + env = {key: os.environ[key] for key in ("PATH", "HOME", "LANG", "USER", "TMPDIR") if key in os.environ} + configured = spec.get("env", {}) + require(isinstance(configured, dict), "INVALID_ENV") + for key, value in configured.items(): + require(isinstance(key, str) and isinstance(value, str), "INVALID_ENV") + env[key] = value + for key in spec.get("env_vars", []): + require(key in os.environ, "ENVIRONMENT_REQUIRED") + env[key] = os.environ[key] + return {"transport": "stdio", "command": str(Path(command).absolute()), "args": spec.get("args", []), + "cwd": spec.get("cwd"), "env": env, "protocolMode": mode} + + +async def discover(servers, concurrency=3): + semaphore = asyncio.Semaphore(concurrency) + profiles, catalogs, results = {}, {}, {} + + async def one(name, spec): + async with semaphore: + try: + profile = normalize(name, spec) + async with asyncio.timeout(35): + async with client(profile) as session: + tools = await tool_catalog(session) + for tool in tools: + validate_schema(tool["inputSchema"]) + if "outputSchema" in tool: + validate_schema(tool["outputSchema"]) + profiles[name], catalogs[name] = profile, tools + results[name] = {"status": "discovered", "tools": len(tools), "transport": profile["transport"]} + except Exception as error: + code = error_code(error) + results[name] = {"status": "excluded" if code in {"SELF_REFERENCE_EXCLUDED", "DISABLED"} else "unavailable", + "errorType": code, "tools": 0} + print(canonical({"server": name, **results[name]}), flush=True) + + await asyncio.gather(*(one(name, spec) for name, spec in sorted(servers.items()))) + return profiles, catalogs, results + + +def package_files(server, profile, tool, version): + require(type(version) is int and 1 <= version <= 9999, "INVALID_VERSION") + organism, capability = "mcp-" + slug(server), slug(tool["name"]) + uri = f"proc://taskand.dev/{organism}/{capability}/v{version}" + descriptor = {"schema": "taskand.mcp-tool/v1", "uri": uri, "server": server, + "profilePin": digest(profile), "tool": tool} + description = str(tool.get("description", tool["name"]))[:1000] + manifest = f"uri: {uri}\norganism: {organism}\nkind: task\norigin: mcp\ndesc: {json.dumps(description, ensure_ascii=True)}\nenv: [TASKAND_MCP_PYTHON, TASKAND_MCP_PROFILES]\n" + return uri, { + "bin.mjs": WRAPPER.encode(), "bridge.py": (HERE / "bridge.py").read_bytes(), + "proc.yaml": manifest.encode(), "tool.json": (canonical(descriptor) + "\n").encode(), + } + + +def emit(root, profiles, catalogs, version=1): + root = Path(root).absolute() + require(not any(p.is_symlink() for p in (root, *root.parents)), "SYMLINK_REJECTED") + planned = [] + for server, tools in sorted(catalogs.items()): + for tool in tools: + uri, files = package_files(server, profiles[server], tool, version) + organism, capability, release = uri.removeprefix("proc://taskand.dev/").split("/") + directory = root / organism / capability / "taskand.dev" / release + require(not any(p.is_symlink() for p in (directory, *directory.parents)), "SYMLINK_REJECTED") + if directory.exists(): + require(set(p.name for p in directory.iterdir()) == set(files) + and all(not (directory / name).is_symlink() and (directory / name).read_bytes() == value + for name, value in files.items()), "IMMUTABLE_VERSION_CONFLICT") + planned.append((uri, directory, files)) + # Preflight every collision before the first write; each package is atomic. + for uri, directory, files in planned: + if directory.exists(): + continue + directory.parent.mkdir(parents=True, exist_ok=True) + with tempfile.TemporaryDirectory(prefix=".mcp-build-", dir=directory.parent) as tmp: + for name, value in files.items(): + (Path(tmp) / name).write_bytes(value) + os.rename(tmp, directory) + return [uri for uri, _, _ in planned] + + +def write_private(path, value): + path = Path(path).absolute() + require(not any(p.is_symlink() for p in (path, *path.parents)), "SYMLINK_REJECTED") + encoded = (canonical(value) + "\n").encode() + path.parent.mkdir(mode=0o700, parents=True, exist_ok=True) + if path.exists(): + require(path.read_bytes() == encoded and path.stat().st_mode & 0o077 == 0, "PROFILE_FILE_CONFLICT") + return + fd = os.open(path, os.O_CREAT | os.O_EXCL | os.O_WRONLY, 0o600) + with os.fdopen(fd, "wb") as stream: + stream.write(encoded) + + +def register(root, uris): + registry = Path(root) / "generated/registry/core/taskand.dev/v1/bin.mjs" + require(registry.is_file(), "TASKAND_ROOT_REQUIRED") + for uri in uris: + proc = subprocess.run(["node", str(registry)], input=canonical({"action": "register", "uri": uri, "origin": "mcp", "hold": True}), + capture_output=True, text=True, timeout=15, cwd=root) + require(proc.returncode == 0, "REGISTRATION_FAILED") + response = json.loads(proc.stdout) + require(response.get("ok") is True, "REGISTRATION_FAILED") + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--config", type=Path, required=True, action="append", help="Trusted Codex TOML or MCP JSON; repeat for multiple clients") + parser.add_argument("--prefer-first", action="store_true", help="For duplicate names explicitly prefer the first client configuration") + parser.add_argument("--output", type=Path, required=True, help="mcp/ directory in an isolated Taskand deployment") + parser.add_argument("--profiles", type=Path, required=True, help="New private host profile snapshot, outside the repository") + parser.add_argument("--report", type=Path, required=True) + parser.add_argument("--only", action="append", help="Select exact server names; default inventories all") + parser.add_argument("--version", type=int, default=1) + parser.add_argument("--register", action="store_true", help="Register candidates in the output parent Taskand root") + args = parser.parse_args() + profiles_path = args.profiles.absolute() + require(not profiles_path.is_relative_to(args.output.absolute()), "PROFILES_OUTSIDE_PACKAGES_REQUIRED") + require(not any((parent / ".git").exists() for parent in profiles_path.parents), "PROFILES_OUTSIDE_REPOSITORY_REQUIRED") + servers, aliases = merged_configurations(args.config, args.prefer_first) + if args.only: + require(set(args.only) <= set(servers), "UNKNOWN_SERVER") + servers = {name: servers[name] for name in args.only} + # MCP servers own stderr; keep raw provider output in private operational logs. + profiles, catalogs, results = asyncio.run(discover(servers)) + write_private(args.profiles, profiles) + uris = emit(args.output, profiles, catalogs, args.version) + if args.register: + require(args.output.name == "mcp", "MCP_ROOT_REQUIRED") + register(args.output.parent, uris) + report = {"schema": "taskand.mcp-import/v1", "servers": results, "uris": uris, + "discovered": len(profiles), "total": len(servers), "registered": args.register, + "status": "candidate", "allAvailable": all(v["status"] != "unavailable" for v in results.values()), + "aliases": aliases} + args.report.parent.mkdir(parents=True, exist_ok=True) + args.report.write_text(json.dumps(report, indent=2) + "\n") + print(canonical({k: v for k, v in report.items() if k not in {"servers", "uris"}})) + return 0 if report["allAvailable"] else 1 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/mcp/pyproject.toml b/mcp/pyproject.toml new file mode 100644 index 0000000..e86ae8e --- /dev/null +++ b/mcp/pyproject.toml @@ -0,0 +1,9 @@ +[project] +name = "taskand-mcp-catalog" +version = "0.1.0" +description = "Native Taskand URI packages projected from local MCP servers" +requires-python = ">=3.11" +dependencies = ["mcp==2.2.0", "jsonschema==4.26.0"] + +[tool.uv] +package = false diff --git a/mcp/uv.lock b/mcp/uv.lock new file mode 100644 index 0000000..4f15d70 --- /dev/null +++ b/mcp/uv.lock @@ -0,0 +1,728 @@ +version = 1 +revision = 3 +requires-python = ">=3.11" +resolution-markers = [ + "python_full_version >= '3.14'", + "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'emscripten'", + "(python_full_version < '3.14' and sys_platform != 'emscripten') or (python_full_version < '3.12' and sys_platform == 'emscripten')", +] + +[[package]] +name = "annotated-types" +version = "0.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5f/56/a8120250d128bed162cd73c76d45f6ef9991f3e068f62a8ee060afa3104a/annotated_types-0.8.0.tar.gz", hash = "sha256:13b2beaad985e05e2d6407ee4c4f35590b11f8d693a258a561055cac8f64cab7", size = 15893, upload-time = "2026-07-23T20:16:13.995Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/99/91/8acff4f5e50511b911bbccb72b8628a49c68ce14148cd9f6431094859a90/annotated_types-0.8.0-py3-none-any.whl", hash = "sha256:f072f4d804ea359e4eaf198b1af7a8b0943881a87f31bb764f8bf219bb9419e0", size = 13427, upload-time = "2026-07-23T20:16:12.938Z" }, +] + +[[package]] +name = "anyio" +version = "4.15.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, + { name = "typing-extensions", marker = "python_full_version < '3.15'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a9/d2/f4d173e22df740bc37b1db102b386ba719b66e95b0f0d751f556b387e6d2/anyio-4.15.1.tar.gz", hash = "sha256:9f28306018cbd6d329e64a36d58256edff76dd996fe423bc957326e578b82a94", size = 276966, upload-time = "2026-09-05T10:42:39.44Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/12/b8/4bd346e22b28902df4d651910f5242c28d84e4a5c2435ca5c3f797ed7e2e/anyio-4.15.1-py3-none-any.whl", hash = "sha256:6152fdbbf9a77fdec97731721bebf7c4c44f7c29b424b0065826173efc7ed101", size = 132079, upload-time = "2026-09-05T10:42:37.923Z" }, +] + +[[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 = "cffi" +version = "2.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pycparser", marker = "implementation_name != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9e/ef/008a1939e372c06329a3fce4279c02f328488f3526744906eeec3da7ad5f/cffi-2.1.1.tar.gz", hash = "sha256:dd31f52ea1086513bb9df30f8fcee9b8918323ae067a3d5b78bc826a000712be", size = 530807, upload-time = "2026-08-03T21:21:18.939Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/70/d2/16d99a0c4948febc0ebd133a13b2f688ff7f8cb04da971e1128872ce0c03/cffi-2.1.1-cp311-cp311-macosx_10_15_x86_64.whl", hash = "sha256:c8d2c9fd1f2d16f780d15127abb050d13d1a76c03a4bd87d7e4980e45e511e12", size = 183838, upload-time = "2026-08-03T21:19:29.637Z" }, + { url = "https://files.pythonhosted.org/packages/cd/95/31b535a9f0220ae9f357de4a08d57ce89cb417653c2fd9f075f50822a388/cffi-2.1.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:398aff33cee2767e3e781d2554c54bd0dff386bb437581e0d8011fde1a942ec1", size = 184168, upload-time = "2026-08-03T21:19:30.764Z" }, + { url = "https://files.pythonhosted.org/packages/ad/5a/4707a0dc1f203f5dde5a907b0d4e3c25d71120241048bd5bc6f1bb9d4e71/cffi-2.1.1-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:154852545011f779917b11c78db2358d095da62a9a172b78ad0a583ee5adc0d0", size = 211805, upload-time = "2026-08-03T21:19:31.867Z" }, + { url = "https://files.pythonhosted.org/packages/ad/66/c19feabb28485b6e0bbaaafa90837a1ef5d302e90f2178bd33f17a49879b/cffi-2.1.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3311ed60d36f83378794e1009ac6258bafbf81f7888b4caa7b35a521e3f95813", size = 218716, upload-time = "2026-08-03T21:19:32.896Z" }, + { url = "https://files.pythonhosted.org/packages/a7/92/500760486c8baab49a7a8a58ba7fc3355ec3974b454b8a09e528efde9e1d/cffi-2.1.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:6e192623c49c94421616a5778fba35cf0d5a8d000650c1967ef4448ee5cdd990", size = 205569, upload-time = "2026-08-03T21:19:34.142Z" }, + { url = "https://files.pythonhosted.org/packages/a5/a7/a67c733254d6e7373f7822f8082d8d6beade791e0cf12a7611f376fa61c7/cffi-2.1.1-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:a6e721d4b0e45d5b65e87534470e67b18dcd092c83f68fba09f152b9cbc061af", size = 204907, upload-time = "2026-08-03T21:19:35.174Z" }, + { url = "https://files.pythonhosted.org/packages/f7/a4/4399daaf8f7dfee9d7c3327fdb0426ee041cc63edc358b93911ceb2bfc7a/cffi-2.1.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:34e261f78cb6ceaaa36f42f2613f4380d94d9c759a9c73c769ee6e0247364632", size = 217807, upload-time = "2026-08-03T21:19:36.286Z" }, + { url = "https://files.pythonhosted.org/packages/28/f7/dabe6da2466ecbd82dc62e7342dc6b1065dad990c06f00f0ede9ebf2a0ed/cffi-2.1.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7225e4514edb64eb6740324353e0da0711954fd8d7da4576755b1c6e09b697cd", size = 221252, upload-time = "2026-08-03T21:19:37.416Z" }, + { url = "https://files.pythonhosted.org/packages/ce/87/616202d8e51342c07d2534c510111c4cc37201775ce8f60802c9335d1edd/cffi-2.1.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:df913725b79db7bcf03448f36b7bf8815363417d5b58deecf9305e3e30f0f21a", size = 214214, upload-time = "2026-08-03T21:19:38.507Z" }, + { url = "https://files.pythonhosted.org/packages/b4/c6/ab025d75d2c26c19b087c0124e75ee31cb65032f4fe345d356d8c507ab97/cffi-2.1.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f5cfbc5fe74540d335175b656c725d74d90e3730c626d92575eea35029d9afaa", size = 219408, upload-time = "2026-08-03T21:19:39.809Z" }, + { url = "https://files.pythonhosted.org/packages/db/e2/7e8109f65445bdc673a7b54f02c677de462db75674220fd1335efc8eb598/cffi-2.1.1-cp311-cp311-win32.whl", hash = "sha256:f8ec5e643a9a937f64e1999eb9f75d072263751912dc5cd06d3c85f8f44be7c3", size = 174470, upload-time = "2026-08-03T21:19:41.246Z" }, + { url = "https://files.pythonhosted.org/packages/73/c0/77ba02423c2f7d7091143c45cd49e0e6575c4c1967394bb542bd923a9b74/cffi-2.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:42f6930c31dc7f50732c9ae793c2786c7b6b044195967bbdde40bb9be81c4cc0", size = 185096, upload-time = "2026-08-03T21:19:42.615Z" }, + { url = "https://files.pythonhosted.org/packages/7c/47/9f1f85f9672ceda4984dc6c4f8824e8558992a2972c3d3c81fb8eb28d4ba/cffi-2.1.1-cp311-cp311-win_arm64.whl", hash = "sha256:c7659f22557c5a0bc4855cd635f55edec690cc008a40768527762cb9fb263455", size = 179941, upload-time = "2026-08-03T21:19:43.747Z" }, + { url = "https://files.pythonhosted.org/packages/10/69/43965eccfdead3b9220015fd1320e117be8c6ed01a62ffab76eeb752f5d5/cffi-2.1.1-cp312-cp312-macosx_10_15_x86_64.whl", hash = "sha256:c8c69575568085ba0b1b10c0249d779a214aea6f6522e949a0fc9fb0fcb449d0", size = 184821, upload-time = "2026-08-03T21:19:44.887Z" }, + { url = "https://files.pythonhosted.org/packages/54/7d/16e5a096677b5e313ca80cd5e5170efa3ea44624a82bb111925522da64b1/cffi-2.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f81b3b8f3d4e343550fa4baa0e479bba9f2d29ce9c2e9b51d1ce1718d7442fcf", size = 184719, upload-time = "2026-08-03T21:19:46.129Z" }, + { url = "https://files.pythonhosted.org/packages/56/e6/8941622732edec876dd17d0453dce07317ae96db34f2ec1436c9d3785986/cffi-2.1.1-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:811bd1e21d32de12efca32393a0ab3f5133b54fce9bd44b8bd77ab07da14bf6a", size = 214799, upload-time = "2026-08-03T21:19:47.218Z" }, + { url = "https://files.pythonhosted.org/packages/44/de/f98430906df1545ffde0d543dd124a7a439bc2cd32b36b9c53f805df7333/cffi-2.1.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:68e62fe11f30d5ca8289242866f0a5291402d8529ca2178ab8afc5c9694ae890", size = 222389, upload-time = "2026-08-03T21:19:48.331Z" }, + { url = "https://files.pythonhosted.org/packages/6a/5b/717f1526b9957b34456313c31645c5b82b8fb5c3fe9e4752999be7128bfc/cffi-2.1.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:4a7c934f7360e8cd64fe9efadcbd10c7c6364f531e432b9a4bf5ccbc9e0e8b50", size = 210249, upload-time = "2026-08-03T21:19:49.543Z" }, + { url = "https://files.pythonhosted.org/packages/64/b3/f8aa4f3e34986c7e4ec45072d1b1b9dd295b6b18007b45518d79726dd725/cffi-2.1.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:3143d81e29e1e20a9ce10901ec369012947876596f75a222235965f2b7ae832e", size = 208775, upload-time = "2026-08-03T21:19:50.918Z" }, + { url = "https://files.pythonhosted.org/packages/b1/db/dceb9dd5b231e1da801793f8acc9f3c52a7e1afe40bb1aae37e02b0faad5/cffi-2.1.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c1453022f490d2459a11819d83ad1d586e9ff65a12ac3e705ffebd46d3685dcf", size = 221822, upload-time = "2026-08-03T21:19:52.054Z" }, + { url = "https://files.pythonhosted.org/packages/a0/d2/6cd24ae3be000a634109c247d1475d62e5616d0dc78c82770942ec384248/cffi-2.1.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:208f941bb9d18e768138677f0a6d2ce01f590df56043dda1df1535ac57c88517", size = 225232, upload-time = "2026-08-03T21:19:53.109Z" }, + { url = "https://files.pythonhosted.org/packages/cb/52/3fa190537004dd7f0ab860a6dc7c0175b8667f68d1e618a46f5498d30250/cffi-2.1.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:210019b6c7cf07f081b4c54635c8cf744377001350e29cc0f81c4377b4797735", size = 223597, upload-time = "2026-08-03T21:19:54.515Z" }, + { url = "https://files.pythonhosted.org/packages/80/fb/0bb75b7039588c074b37ae99f40d9bfddf990ecb2fbc346ebccd2e56b9be/cffi-2.1.1-cp312-cp312-win32.whl", hash = "sha256:046bfc24911b37851ee1b51aab8bffe713d89c68c6a057b09484ce9fd5f69b4e", size = 175292, upload-time = "2026-08-03T21:19:55.566Z" }, + { url = "https://files.pythonhosted.org/packages/d9/79/615cc094e2fb508cade7de88d3b4f6c4ec2bab695c97bce9153dc65aadf5/cffi-2.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:f53e442b08449d42821fa4a4fba000095af9f62742a500f978a9f557ec44339a", size = 185919, upload-time = "2026-08-03T21:19:56.89Z" }, + { url = "https://files.pythonhosted.org/packages/70/c6/d0ea84713fe46b243a436a18fcd47d639732747e21635c8a27191b06dc30/cffi-2.1.1-cp312-cp312-win_arm64.whl", hash = "sha256:7bde5e4cc5c10140859842b9d383af292b22639a4dffb725314baf45968cef80", size = 180093, upload-time = "2026-08-03T21:19:58.155Z" }, + { url = "https://files.pythonhosted.org/packages/9d/f4/035513d4117049066b4779dc3b7c0c0fdad175fa13731c9f4003f1cd1478/cffi-2.1.1-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:b5bdfd1c873d4e093aabc0ca84c4ca6dbc4f752afb5c86f146d9742580c9da2e", size = 194248, upload-time = "2026-08-03T21:19:59.399Z" }, + { url = "https://files.pythonhosted.org/packages/76/af/2aeb4dbb5fc41a04161ae9ff1518de7cec08e164f44a8ce6a4cf7fd2cd1d/cffi-2.1.1-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:31348097ff5bbe827ccc41795d4dd099d9f0625e7def00ee653c137a490c2a6c", size = 196908, upload-time = "2026-08-03T21:20:00.746Z" }, + { url = "https://files.pythonhosted.org/packages/a7/46/2e5fdde8555706dd98139a910ca11be02809f3f605ce956f655d0214e100/cffi-2.1.1-cp313-cp313-macosx_10_15_x86_64.whl", hash = "sha256:9d2055050ea716bd38b7f7f1579c275386646b4894c155a3e2f3cd62ed41b7c6", size = 184805, upload-time = "2026-08-03T21:20:02.02Z" }, + { url = "https://files.pythonhosted.org/packages/55/41/4c7042f317b9217502988f0873af87e16ad606dc20f84e546e3e6ce9764c/cffi-2.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:19ee6127ee34de7d83ce3d371ebc5ed91addbdcc39f9ab15ce4eb35a4e534971", size = 184764, upload-time = "2026-08-03T21:20:03.141Z" }, + { url = "https://files.pythonhosted.org/packages/43/1f/1c3d90d91811c8f86ced9ed637956c54bfe5b79ca98fe976d7f8c8979f6b/cffi-2.1.1-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:6a8dddef476fab96d066d578fc88526767b836ab5ab21754e1d5bf3879c31c7c", size = 214722, upload-time = "2026-08-03T21:20:04.377Z" }, + { url = "https://files.pythonhosted.org/packages/37/6f/3b5ce4c3b2192d250f04908f2bfd91ef34552ec8f7716a5d4abdb8d67bb2/cffi-2.1.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f16c709686a78c727bbbf059f92b0bf41c6fc60deec706d2dc19f529175a6125", size = 222369, upload-time = "2026-08-03T21:20:05.544Z" }, + { url = "https://files.pythonhosted.org/packages/02/10/4b3c75dde3d9663c9e02ba05c2668b954f671d4bbe346413ca8c696b295a/cffi-2.1.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:fcd22650c908d7b7da162bbfaab594a1227a15d1643a98c68b122ac642fa2264", size = 210175, upload-time = "2026-08-03T21:20:06.75Z" }, + { url = "https://files.pythonhosted.org/packages/df/62/14f74b9543e605d17701dc797b815958b8bb70b7624ce1b832ddad48ed6c/cffi-2.1.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:aa9511c62d14da7aacc9b4bf51f3f697a621e83b2d6919008243c3aad168eea3", size = 208670, upload-time = "2026-08-03T21:20:08.04Z" }, + { url = "https://files.pythonhosted.org/packages/95/95/86342356ff5953b3fb06f7ef7c5bee212d45e770abc7218d451b9148313c/cffi-2.1.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a931079504ecc49efed7744c476a5c343a92fabf66dec2db95edb1b2fdc770e2", size = 221824, upload-time = "2026-08-03T21:20:09.274Z" }, + { url = "https://files.pythonhosted.org/packages/eb/ff/7b3429ff53aafe931ed8a5fc69f481bbef7ba6de87ddcbb63d08f483f613/cffi-2.1.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a2d7755bef5a12ed488f4ef1f1b69ee9191d7396083b755a5d2295f6edb4768b", size = 225148, upload-time = "2026-08-03T21:20:10.7Z" }, + { url = "https://files.pythonhosted.org/packages/34/34/a95870b9221e09cf4f2ce3178b1a210abdfe63a1bd357da940418d7b8d15/cffi-2.1.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e0bcb7e0f677f543555d2adff3bf19c05f66cdb4796e5ff602442ab2fe3c4ef7", size = 223564, upload-time = "2026-08-03T21:20:12.165Z" }, + { url = "https://files.pythonhosted.org/packages/70/ea/839b50531021a647fb5e929f72cf97bc1ff702b5472166164b5b6e76b851/cffi-2.1.1-cp313-cp313-win32.whl", hash = "sha256:334644fbac4eff73d985a17a91226df55d0f394160c4cfb880e084c8f7161cac", size = 175263, upload-time = "2026-08-03T21:20:13.559Z" }, + { url = "https://files.pythonhosted.org/packages/60/a6/8b149b2c3f2e11aaa1618ef64500b45f50f22c57a977a4dff1aff1f91042/cffi-2.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:1aa5645c30469b09530c4ebca77ebf8f17618293c58f8549cb1a543a50236e7d", size = 185688, upload-time = "2026-08-03T21:20:14.69Z" }, + { url = "https://files.pythonhosted.org/packages/01/9a/11f687cb39d6a3504060d5242f04f48c735afb4d3d533958a20594890cb2/cffi-2.1.1-cp313-cp313-win_arm64.whl", hash = "sha256:63bbfd5ded17c4840ac07cd8f1c21ba9d9708141f840b324f422f41b207e3973", size = 180078, upload-time = "2026-08-03T21:20:15.917Z" }, + { url = "https://files.pythonhosted.org/packages/d3/7b/d6bbf82b8b96e7391438898c42f5bd96dd02030fd5b64937d248220003e2/cffi-2.1.1-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:7dbb61fe3a7699468030f71bbe5f8a0e326a151daa91beb11a6fc1f980c55e1c", size = 194064, upload-time = "2026-08-03T21:20:17.148Z" }, + { url = "https://files.pythonhosted.org/packages/94/e6/bcc91b283be94735e268487a054004f0aa19947b6348fa367db53230abc8/cffi-2.1.1-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:f24fb43132a4c6b4cb4eb029492919b2db645be6808d738f244fd146c03c32cb", size = 196720, upload-time = "2026-08-03T21:20:18.268Z" }, + { url = "https://files.pythonhosted.org/packages/d9/99/c4b0c17cacdc9c3b8f280026286a9826d6a208c0f047591a3c3ce99b91fd/cffi-2.1.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:d28630f5854ab07ab1fd4aba756de52326c82e6be15d414b12793f1975048b54", size = 184964, upload-time = "2026-08-03T21:20:19.708Z" }, + { url = "https://files.pythonhosted.org/packages/b3/a9/9db617d05d7367c1ad0ab00b3aa6e6f9281edd689b4ee9ea0e5a84e89c97/cffi-2.1.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:661c298b4821edebead0c91edd2b00374d67ad7c5a1f7a91d4442633b79d6a72", size = 184962, upload-time = "2026-08-03T21:20:20.833Z" }, + { url = "https://files.pythonhosted.org/packages/67/b8/b42132ca113dc567d37684437b46ca1dafc885902b02a110a02d5b511857/cffi-2.1.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:58acb8ab8e295e6c5ea12f888cbb13cf21511ef2a3303a23f4325c29d17fe5c1", size = 222328, upload-time = "2026-08-03T21:20:22.118Z" }, + { url = "https://files.pythonhosted.org/packages/80/10/c5c0cbf0a657aecf59ef511409734230bf556f05a0d6c9eed7aa5c0a0166/cffi-2.1.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:456a61fa52d579ebf9df2e9552ead5129855dbaff6c1e5a9b1bc408809bdc062", size = 209985, upload-time = "2026-08-03T21:20:23.401Z" }, + { url = "https://files.pythonhosted.org/packages/d5/6c/bfa0b87b03b9238148beca990292843c9396ba069b54496596594173de7b/cffi-2.1.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:a4f00aa42f75d6e4595e8866e748cc1705adc0cddfeb2ca86d0d03993d63ba03", size = 208530, upload-time = "2026-08-03T21:20:24.628Z" }, + { url = "https://files.pythonhosted.org/packages/e9/02/4e7d553a7ac4b4238b38b3c1b80d486e9d4436f8d2acbf87a0997fe3f402/cffi-2.1.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b0431303acaea1089ad4b3e9ce4e6518193def1118d4073ca848635ee4ea2e96", size = 221525, upload-time = "2026-08-03T21:20:25.758Z" }, + { url = "https://files.pythonhosted.org/packages/82/1d/a4aaf9babd75acb4d5f223bff71533bee748dd770a382619a798960ee9ba/cffi-2.1.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:64faea20f4e2613363a1a9b9c7dd73058f3ecd00133a511e72ad7c511658f527", size = 225053, upload-time = "2026-08-03T21:20:26.985Z" }, + { url = "https://files.pythonhosted.org/packages/81/10/5dc0e7bdd18e22107054288283380fc97a06ae3f1656a106908d666a3c88/cffi-2.1.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5c58fe613dc5e5336357eff555824a314d8e43282600435c8d1cb6a7a2fedd13", size = 223213, upload-time = "2026-08-03T21:20:28.277Z" }, + { url = "https://files.pythonhosted.org/packages/0b/e9/d0061c364cde06ee43168a0d076ac1da512cbc380d44767b844ba34fe2b6/cffi-2.1.1-cp314-cp314-win32.whl", hash = "sha256:1a18a57b58cfb21fc28d72e876acf10eaed67a1ed96226f92af4df681d571c4c", size = 177682, upload-time = "2026-08-03T21:20:44.288Z" }, + { url = "https://files.pythonhosted.org/packages/a7/06/1c3e01e3ba14c39f6d10bfbac52753b7e22259e38088e5cfe1d704918690/cffi-2.1.1-cp314-cp314-win_amd64.whl", hash = "sha256:3222ba5d678f80a030e6afbcc33dc1ae5cb45facabb61cee2c7016b8432fde48", size = 187949, upload-time = "2026-08-03T21:20:45.623Z" }, + { url = "https://files.pythonhosted.org/packages/87/5b/da4e39efe18eeb89cf580ea9cfc66b6a7c3eadb808fc0cc1d3a295cb5a5d/cffi-2.1.1-cp314-cp314-win_arm64.whl", hash = "sha256:ab36d55f9ed2d067327667c2fea18dda018eb628dd6347aa01dda6cf1f5d3836", size = 182947, upload-time = "2026-08-03T21:20:46.955Z" }, + { url = "https://files.pythonhosted.org/packages/23/59/40338bf421c5accea1d45158170c87006ef1cd371b05c077e76476949728/cffi-2.1.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:7750c6449dff7864bb9bb27ddfb0267756189201a3afc911d82b3caacd70dfc3", size = 188504, upload-time = "2026-08-03T21:20:29.495Z" }, + { url = "https://files.pythonhosted.org/packages/7d/47/5ecf1023850036e674c77ec4de86182d309ae344e39e7cba984b7df5d647/cffi-2.1.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0beceaabe56af686895136a2de78db54ecd8e4046b236b8fd6d6cb61389e9bf2", size = 188259, upload-time = "2026-08-03T21:20:31.291Z" }, + { url = "https://files.pythonhosted.org/packages/2a/9c/92934c3bea9f785b23eba304538c0b4d37a2a96d2431eb3a1bc87a11aa19/cffi-2.1.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:49cbc70e6542d4ccccb936558d1064a8012541e78f821f955cff24e357776c94", size = 223864, upload-time = "2026-08-03T21:20:32.571Z" }, + { url = "https://files.pythonhosted.org/packages/4d/45/ba4c93527bc38616a8bd36488acb69a2212d60486794f0c1f318949bbb76/cffi-2.1.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:e2d65b31f36619cda3999b78b2aa9632e76b78448e7a56fc4240824200e7c4fc", size = 211538, upload-time = "2026-08-03T21:20:33.808Z" }, + { url = "https://files.pythonhosted.org/packages/80/e9/b6ef565e452acb932fb0cb5443f44a78efbd1233e566f02b5a83855e9115/cffi-2.1.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:28907ab9bfb6aa13184cfc17c6b8e1023c5ab6fd7076d8c20a35e59fe04f8f29", size = 210688, upload-time = "2026-08-03T21:20:34.974Z" }, + { url = "https://files.pythonhosted.org/packages/9a/95/eff5f0cee78d2eabc7eebffec40d3fc1876b5f3c95582e018bb4b99601f2/cffi-2.1.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:51b31d1c98274844cfd7838ce00bfc27c7423a4dc00fc0772fc3331c2cc90676", size = 223803, upload-time = "2026-08-03T21:20:36.564Z" }, + { url = "https://files.pythonhosted.org/packages/fa/01/579d39fb8bef00a335a23d83757b44feb24cd6345a2c451b64cb67b9c362/cffi-2.1.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5e7cecbaadb83884793e05828cee59b210b24583b9c7425d0ba6a754fe22eb4e", size = 226763, upload-time = "2026-08-03T21:20:37.816Z" }, + { url = "https://files.pythonhosted.org/packages/8d/b0/0b44f47c60b01b57b6e2bbd92343f13a85a1d93bc46ccf6e47e244acd99c/cffi-2.1.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:25792eac27877609e7bb06d42ff88278a6624fff2ba9bbb523c09616b117e80f", size = 225688, upload-time = "2026-08-03T21:20:38.959Z" }, + { url = "https://files.pythonhosted.org/packages/eb/d2/3b7176cb570a1d3e27faf67b72f591af508036e0d8b2be2ef9af9e8c84bb/cffi-2.1.1-cp314-cp314t-win32.whl", hash = "sha256:8ef53b2de9bcb9197d31854256575d59dbac0cba72ac627bb291ef5eceb74be4", size = 182868, upload-time = "2026-08-03T21:20:40.388Z" }, + { url = "https://files.pythonhosted.org/packages/56/78/31f00c1bcd97c9bbf55f1bfdf5bc809a5de8887473e90bb9960dca825e80/cffi-2.1.1-cp314-cp314t-win_amd64.whl", hash = "sha256:616f097f2fe415bc92a247f02e11f634e1f9e9a83d327e3c915c15089c87869e", size = 194104, upload-time = "2026-08-03T21:20:41.725Z" }, + { url = "https://files.pythonhosted.org/packages/7b/1b/58496f2ed0a35de575250c02a43ab3cc2c04d494a88fed31c1cabc0fd176/cffi-2.1.1-cp314-cp314t-win_arm64.whl", hash = "sha256:ad2c86c495b899d862ea0f4b42891b8713a3bd45dd4105c7fd51c2a72f39f3a5", size = 186402, upload-time = "2026-08-03T21:20:43.042Z" }, + { url = "https://files.pythonhosted.org/packages/c1/8f/9ebe220eab48a093d1a5a5e339ab0dc7316eef3bb04d63c42f0251b61f50/cffi-2.1.1-cp315-cp315-ios_13_0_arm64_iphoneos.whl", hash = "sha256:dddad92b554513a31f272570678ba307fb9f618f05e3d4a5eacafff9eae03e1d", size = 194043, upload-time = "2026-08-03T21:20:48.179Z" }, + { url = "https://files.pythonhosted.org/packages/ff/69/844bad3ece306c4782c2ecb93597035b6690d48704b803914c199da1e8b3/cffi-2.1.1-cp315-cp315-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:da0e573f9f97159390c89d9f1a9e41908b66d408cc5b58d08cf3847d844c531b", size = 196737, upload-time = "2026-08-03T21:20:49.457Z" }, + { url = "https://files.pythonhosted.org/packages/1b/8a/af668013284634733f02d683458a0728739c7d6ddb5e14cb0c20832266fe/cffi-2.1.1-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:fb92203a88b3d3053034db775110081c49d28be6551923805e039924093761e4", size = 184933, upload-time = "2026-08-03T21:20:50.639Z" }, + { url = "https://files.pythonhosted.org/packages/0c/75/2f5207ff6d1a613133b23a5203cc0c2a628313b5eb3974d7956ae3c57950/cffi-2.1.1-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:2ae64be792b8966f2c69538199728b290e34726562896df1e5dc8ffd8d8188e8", size = 185002, upload-time = "2026-08-03T21:20:52.173Z" }, + { url = "https://files.pythonhosted.org/packages/e2/31/9e1313b0a6e30e91b3b3d3fff51ae99c857c07738e3afcce1f7334e1b7ab/cffi-2.1.1-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:507a24c282e0f42f8ed737cf048572cbf580468da5555764a8331735e9c736b6", size = 222271, upload-time = "2026-08-03T21:20:53.462Z" }, + { url = "https://files.pythonhosted.org/packages/50/e3/f6234a833e6e08c7007003074723c406559eecf9b48dfc97471e5a8eb7a0/cffi-2.1.1-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:246fa40ce8645a614ff682e0b70f37134e460eaf93a775e0cbe3cca585a67a80", size = 209919, upload-time = "2026-08-03T21:20:54.783Z" }, + { url = "https://files.pythonhosted.org/packages/0d/fc/5f74e293fced6edb51af3a46c4ccf6c23c9943774ecb375ddbd522c76add/cffi-2.1.1-cp315-cp315-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:471cee653ae88de62096552e6d24ccb4a5adb8c8c9f10b5054d0122c15bf2779", size = 208529, upload-time = "2026-08-03T21:20:56.066Z" }, + { url = "https://files.pythonhosted.org/packages/44/16/29e6d01b388bef055ecd6ca8244b3f4d336bd09e92d5d892187b9601084e/cffi-2.1.1-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:aeae0e330c9f6acd681f647d46cefd30c29f93e3392882e792e82080c9691399", size = 221630, upload-time = "2026-08-03T21:20:57.336Z" }, + { url = "https://files.pythonhosted.org/packages/a4/18/fa7f1f6857d5eb88a4ca99ffcbfb7c387a287ccc154c64a73e86314745d7/cffi-2.1.1-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:42a494cee34437f05546455144f2b5d9ac09b1face62bcfce597d2e521066688", size = 225134, upload-time = "2026-08-03T21:20:58.675Z" }, + { url = "https://files.pythonhosted.org/packages/e0/9f/e8e3dfa04a1b4c241f8c91faacad872b4d4efd051d49764ad4e2fd4b9fea/cffi-2.1.1-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:cc572dace3f60ef98d7b12ff411d20f5362feb31a0439eab0085bbfd349982d7", size = 223197, upload-time = "2026-08-03T21:20:59.968Z" }, + { url = "https://files.pythonhosted.org/packages/f8/7e/8debeb04f1ab9fe2a6963964cd6f1aaf7192627b83926586a6a4e089c9fa/cffi-2.1.1-cp315-cp315-win32.whl", hash = "sha256:4f42141fc14250de6dde5ee7ea4432be017252d91f19c5ad043c084cea629cac", size = 177683, upload-time = "2026-08-03T21:21:14.901Z" }, + { url = "https://files.pythonhosted.org/packages/e0/31/5158704cc474ab65c1647932e88be78dc0873f47130e253be38bcaf13d01/cffi-2.1.1-cp315-cp315-win_amd64.whl", hash = "sha256:e6e8cff14d6fb0be70a09c0bdc58096f501952d04624ebf867e0e56da2df8960", size = 187897, upload-time = "2026-08-03T21:21:16.108Z" }, + { url = "https://files.pythonhosted.org/packages/cc/4b/b3a2da8570c704ffc0f9762cdc3ec0f02c8573798e0b5cf7f11c82bbb70f/cffi-2.1.1-cp315-cp315-win_arm64.whl", hash = "sha256:27350daa11d4f10c540e6e89dada4c54feb7256ad03e9a4dc075ebad7ba360d1", size = 182935, upload-time = "2026-08-03T21:21:17.271Z" }, + { url = "https://files.pythonhosted.org/packages/d0/ef/5443574510a1207e6f6bc38ba6e1f1de36cb48fef07b2728bb896a21f430/cffi-2.1.1-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:c26608d2222fb1e94487e4a387d85f13eb55d5ed725cb25a0c589ac4ee60e7bc", size = 188464, upload-time = "2026-08-03T21:21:01.163Z" }, + { url = "https://files.pythonhosted.org/packages/7e/ae/a56fa8c4686ad50e148fcbc8d3ae0d03915ff5c30d795058988c24118cef/cffi-2.1.1-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:4be96343e422f2dfcd12ab5c9f5aebe03f82f737c6bffeca6830b3875cb44aab", size = 188262, upload-time = "2026-08-03T21:21:02.382Z" }, + { url = "https://files.pythonhosted.org/packages/53/b2/6187f46f2912276a3ae284076109cc5c8680482f11f766ccf26db4a86427/cffi-2.1.1-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:937c0052c05a31ca1daf18de3158eed4dbfcb9cc107adbea227728d647be701e", size = 223779, upload-time = "2026-08-03T21:21:03.553Z" }, + { url = "https://files.pythonhosted.org/packages/8a/f6/c3ad28bd19f77047a03084424fbd4cbe997303267c14423737324be0385d/cffi-2.1.1-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:df423d40ee8654634421812bc3b196da3f9bd7d32929da813f8394c4348a5358", size = 211520, upload-time = "2026-08-03T21:21:04.863Z" }, + { url = "https://files.pythonhosted.org/packages/a0/cd/ccac9013a5bd9fd764de118674ab9c805b5ca10c19270d90ee273f8b2240/cffi-2.1.1-cp315-cp315t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:a730a083190634c65cca36ba5f489531576ebd79bcd5c8e172130f6453127231", size = 210673, upload-time = "2026-08-03T21:21:06.223Z" }, + { url = "https://files.pythonhosted.org/packages/52/86/2976131c639aead931c5bee5aba67e4b09fbeb8018b6f282f70803f923a7/cffi-2.1.1-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:363e05fa78e15116c3c32c210ee36884fd6b9afa6d440e47112c3bd511d64cb6", size = 223835, upload-time = "2026-08-03T21:21:07.539Z" }, + { url = "https://files.pythonhosted.org/packages/ac/0c/33a7aeab2f9c76918c52e084beb39c570db3588133412929e8ec06fab90b/cffi-2.1.1-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:770de9db11e84213beec501cfcaa013b019820ca881e03344dea5844f7876d94", size = 226705, upload-time = "2026-08-03T21:21:08.774Z" }, + { url = "https://files.pythonhosted.org/packages/e3/26/2cde30fdde421130bfc18f70395731a6e6b2053c6a1978a5258ff04e72fa/cffi-2.1.1-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:7da0c5eff80f0197f3b3d1232ec5a682a9325f4ae9016a78f5f5ca35f9ced1f5", size = 225539, upload-time = "2026-08-03T21:21:09.911Z" }, + { url = "https://files.pythonhosted.org/packages/6d/cd/a361394c94b2129d604bb846f624a8e88255a3ee33129c434a00d715e64f/cffi-2.1.1-cp315-cp315t-win32.whl", hash = "sha256:06c72bb76605a4b0cd0aad6930b69d4baf7dd5d806cfc409b824191099700e66", size = 182707, upload-time = "2026-08-03T21:21:11.226Z" }, + { url = "https://files.pythonhosted.org/packages/9b/b5/ba2b299993c26577d529b6ae29841f9e15b9fcf004d65f423f4fcf94ade9/cffi-2.1.1-cp315-cp315t-win_amd64.whl", hash = "sha256:d9c275eaacd24aa73f94ffd6de08fc3f932424d8b6c376f4bed7cde376fe7bc3", size = 193772, upload-time = "2026-08-03T21:21:12.39Z" }, + { url = "https://files.pythonhosted.org/packages/aa/29/35e016098c814cd93de9cd320c66b5bfba14dc6ecedd3cb518fa7c408c69/cffi-2.1.1-cp315-cp315t-win_arm64.whl", hash = "sha256:d18e5ac0f2f03f4f518d3e23db0f0cad7faa1da8620e9c09461d443bbf6e6692", size = 186360, upload-time = "2026-08-03T21:21:13.636Z" }, +] + +[[package]] +name = "click" +version = "8.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c7/0e/7fa0ef50764b67090eca4114772a2abf8b6148198475e54c660b97caeee6/click-8.5.0.tar.gz", hash = "sha256:ba0d2089de75ea0310e2dde03160e6ca10009947fb95a182f9b54021bb272e34", size = 382235, upload-time = "2026-08-26T13:33:14.56Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/58/50/6c0d534c5f134586a8e1ba4e330569e32f057e33372ae556463212fb4cd3/click-8.5.0-py3-none-any.whl", hash = "sha256:255bc9599cf7748b4b1a446ccc735421bd08a2ae529a8b88597d3de5664ee360", size = 125251, upload-time = "2026-08-26T13:33:12.928Z" }, +] + +[[package]] +name = "cryptography" +version = "50.0.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bb/ad/5d6702db60b1e40b41ef513b6967ff5848f307d50f8449baf1634f5908f1/cryptography-50.0.1.tar.gz", hash = "sha256:5dd9bda1c12b4162f6ff568eeb5e0ff956c28d14406e875cfe8a63a2d414ff20", size = 880381, upload-time = "2026-08-25T19:45:45.499Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ba/19/797e2aaac9df6a66f1550f49979dc1b1e39ecd2077501c30efa81e8d5d67/cryptography-50.0.1-cp311-abi3-macosx_11_0_arm64.whl", hash = "sha256:b8f852c65863251b9e3a1b8c150ce21e59b522dbb6a7d4bc80e680d38388e986", size = 4010153, upload-time = "2026-08-25T19:44:03.155Z" }, + { url = "https://files.pythonhosted.org/packages/90/34/9ce9a62ed9dc82ca9fd6a34445b6904af56e5f38b3eae2ed32e49c36053d/cryptography-50.0.1-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:53e279950892dc102c6b4e52af03ae5ea92fac572a1ddab78ca73a997f62b69f", size = 4723133, upload-time = "2026-08-25T19:44:05.461Z" }, + { url = "https://files.pythonhosted.org/packages/57/26/e6d4fc8512a51a5f9ee7bfdbfb853bce1197087df40c9ad993ad370b846f/cryptography-50.0.1-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ff838d62ec1bfce4f9ba7fa16f4a7b554cd8d0c299e6be37502161a660c84eef", size = 4712478, upload-time = "2026-08-25T19:44:07.375Z" }, + { url = "https://files.pythonhosted.org/packages/e6/de/d3cdc2815697aae84126cbd6a030ca7b6b452e28a88b501b836bd3aa7a86/cryptography-50.0.1-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:e74591e283fe6eb956416c929eb58262a719fe0311fd9054c62c3350ed8760d8", size = 4730726, upload-time = "2026-08-25T19:44:09.294Z" }, + { url = "https://files.pythonhosted.org/packages/55/32/38c0d344b98c06d34b5df8946565a9c0d6dbf32c8e0730a7f05f0a3c6cab/cryptography-50.0.1-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:5fe002589592ed749ce77fe0695fcbd3500dd61d7d6db5858a7544c612fa8e45", size = 5353524, upload-time = "2026-08-25T19:44:11.96Z" }, + { url = "https://files.pythonhosted.org/packages/e1/1b/82f0f0d8858d4432be1af790477edf62aef90324041aa07c57e57bef1af7/cryptography-50.0.1-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:51593d180cf6d179bde5c5d065bed81386b1f381656ae7d042b7ffc87a9895ad", size = 4746720, upload-time = "2026-08-25T19:44:14.051Z" }, + { url = "https://files.pythonhosted.org/packages/29/ba/042ca458b8c64348c768284b5d23e69b92ed53d057ab779fee628564676d/cryptography-50.0.1-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:359e62deae718bce96170e223fdcb6357e4fbd3bb7a3a75f4430763532560e49", size = 4361866, upload-time = "2026-08-25T19:44:16.167Z" }, + { url = "https://files.pythonhosted.org/packages/39/3b/e96c1ef71edef71057c7e3c3d982ce8fda554e0c52d0cc19c18845cde3eb/cryptography-50.0.1-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:e2ca8fd1b6b4b82a1c4cb02841d0837e3c12336c2e24b520ab8ab3b969733d8f", size = 4730028, upload-time = "2026-08-25T19:44:18.085Z" }, + { url = "https://files.pythonhosted.org/packages/e3/38/45abd72ef63f2e7d0754a6cacf97bd8b69512ace7f6130d24c39ece65da2/cryptography-50.0.1-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:76de83fbd91ac49c0feaaa983d0748fd7a53176afac5fb3bf7478d244f0eb527", size = 5308405, upload-time = "2026-08-25T19:44:20.197Z" }, + { url = "https://files.pythonhosted.org/packages/85/66/6ccca4722987ddedaa7fc9c3f4708af7431f5535666c174350830888c6b7/cryptography-50.0.1-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:51afcfceb15597cf2635068e4ac9a56b2abde622edde17f37d85fd7b5306497a", size = 4746230, upload-time = "2026-08-25T19:44:22.376Z" }, + { url = "https://files.pythonhosted.org/packages/13/0e/b1f92e013228111413f2e6743948b80bc24dfd3c1b87ba98ceea16f5df89/cryptography-50.0.1-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:be224a65493ec5b74a158ff22a5522ce4a5ca1e543c647a3a4730d4a09e5f959", size = 4862596, upload-time = "2026-08-25T19:44:24.472Z" }, + { url = "https://files.pythonhosted.org/packages/7e/22/c3654cccc856e9d682817b04ac3ee79731cb09ca6f95996a95c904de2883/cryptography-50.0.1-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:9ebcdd5519be9b652a46f507817a74591774fc3d6923ac364e4dfa64e36b291b", size = 5014082, upload-time = "2026-08-25T19:44:26.709Z" }, + { url = "https://files.pythonhosted.org/packages/42/8b/cb12b1b60c91b074ca6bf0fdd59aa8f10d8bc5f73af8faece86ef0421b37/cryptography-50.0.1-cp311-abi3-win_amd64.whl", hash = "sha256:aed8db4f6d71c51efb89530e12d9464e7bf2923d46c3205dc794a2a93f8c0648", size = 3842826, upload-time = "2026-08-25T19:44:28.784Z" }, + { url = "https://files.pythonhosted.org/packages/5b/f0/424cb557d99aa86ac55da5e2add02e2882e44047b6264f93ade1b975a993/cryptography-50.0.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:30a125032e5642a21ff816e021152bd4e7e94f03eff3f4b7fca41cd22bc3110f", size = 3973525, upload-time = "2026-08-25T19:44:30.7Z" }, + { url = "https://files.pythonhosted.org/packages/4d/72/3a2711d967977ab5fc80b782837c7e8d1ac7445e764c20c381a265c57ef3/cryptography-50.0.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:a0b1a59e3a089064a0ec309e9428c8e3ae4e161419d20ac33600767e83fc658a", size = 4708817, upload-time = "2026-08-25T19:44:32.773Z" }, + { url = "https://files.pythonhosted.org/packages/b4/f2/bb1f56e10815b789df0b409a69fa4992ff3d3fef9c72747f4a6b26fed38e/cryptography-50.0.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8921d58f426793c5f1b47f0b59575780de9a095214958d0eb37d909593db8367", size = 4697300, upload-time = "2026-08-25T19:44:35.144Z" }, + { url = "https://files.pythonhosted.org/packages/08/bd/ed5396be499ffcf8807a585bfe38b71a1fbdd1c342b4f9b6d0ef5162a946/cryptography-50.0.1-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:a8f40ea47330e71b594a7e246898f93177c259490c63183dbaf9e571d71ed9a5", size = 4716039, upload-time = "2026-08-25T19:44:37.192Z" }, + { url = "https://files.pythonhosted.org/packages/f6/6e/1cf405c5c8e8df7545378048e954792f00b7f2367af8863ce8b8f3e10607/cryptography-50.0.1-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:a255449073358275b64b67d3f595f268bbef70e72b6edb65e0c70c735bf739c9", size = 5332388, upload-time = "2026-08-25T19:44:39.16Z" }, + { url = "https://files.pythonhosted.org/packages/47/92/b4317e8c32c4f47b062f5398bd79106b220a124546f42be83bf32b761e2a/cryptography-50.0.1-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:8df2de9102026855887e4587084f6eabd80ed0f345b8ad8a7ac27ab9bf4723e0", size = 4730293, upload-time = "2026-08-25T19:44:41.298Z" }, + { url = "https://files.pythonhosted.org/packages/39/0d/a1e7633e2c744d0f2983320a27e924ef2264c79c56e1a58d5fb0a1cfd413/cryptography-50.0.1-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:ac02b07824d4d1001bd4367599f839c19cb171924c796e52c23508ac14c2c0cc", size = 4346031, upload-time = "2026-08-25T19:44:43.245Z" }, + { url = "https://files.pythonhosted.org/packages/88/dd/b215616f9bab3fc18510c78a4e5c9f362d77838503c363dc747c7d4f5c6f/cryptography-50.0.1-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:cbf74a81765ee67413503ca6e26dcc4f6f5a519822436cc0a1b97aab6c1b8a17", size = 4715344, upload-time = "2026-08-25T19:44:45.291Z" }, + { url = "https://files.pythonhosted.org/packages/b1/1b/ec3ebd31741d0e963612c4fe43caa39341b9b1e031e469820e42e4c83918/cryptography-50.0.1-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:16c5ecd954b3330ebfb6605eca4fd952da8bef376551d5cc264534e3770a9ee6", size = 5287201, upload-time = "2026-08-25T19:44:47.297Z" }, + { url = "https://files.pythonhosted.org/packages/1a/01/0127d11a762b31a9ee0221894f540318761783f3fdc4bc5d057698caebd5/cryptography-50.0.1-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:79bf008d1f9af6071c797ad133e39915dfee7614f18f18f4db9072eb715064a3", size = 4730023, upload-time = "2026-08-25T19:44:49.435Z" }, + { url = "https://files.pythonhosted.org/packages/9e/b9/e7425ebfb599241a0c1d7000f1b466c3062da66c19d9525031315dff7213/cryptography-50.0.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:330fbb252391c596f1ae42c5754449dc924e6ad012dca8efe0d703f9f2d12ec6", size = 4847362, upload-time = "2026-08-25T19:44:51.94Z" }, + { url = "https://files.pythonhosted.org/packages/2d/fd/60d0ddf4defa12e482c9d5e0f554384d6e8ab25341fd15f060028fd92e6a/cryptography-50.0.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:42be3bb70596b3abe4ac097b75be223e8b3ab614a0e5de068e3dcc54d71d6149", size = 4999247, upload-time = "2026-08-25T19:44:53.876Z" }, + { url = "https://files.pythonhosted.org/packages/4d/56/bc4f2b209e766c93372cfcd59b781a0b2b59700f62a969580415b699c2b2/cryptography-50.0.1-cp314-cp314t-win_amd64.whl", hash = "sha256:f74455bb086a85d5e81246412602aaa97ed095e504cd40dd261ef50be42205bf", size = 3825806, upload-time = "2026-08-25T19:44:56.209Z" }, + { url = "https://files.pythonhosted.org/packages/84/a9/ee16a903f13755e914d1eecc482fe64d1f10761c3960e5d8fa6837377aff/cryptography-50.0.1-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:ca83d00d9e69cd5eb63f2e69c3a5a59e0cecae5ae14c6ae0b35830fe3b37bad0", size = 4035307, upload-time = "2026-08-25T19:44:58.305Z" }, + { url = "https://files.pythonhosted.org/packages/5e/a5/9ec7e81e8526c0d7a387d73386b2daed3f39e10d81a85930bd1b6bfba65c/cryptography-50.0.1-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:05ba322c4da95b262a212c345af888ef2c37c88c0509756ea00a0e6d68850f23", size = 4751900, upload-time = "2026-08-25T19:45:00.401Z" }, + { url = "https://files.pythonhosted.org/packages/7e/3c/0e77bd5ffcf078e9dd27d3074aad6c030d9b10d0bf69329d573c927a188c/cryptography-50.0.1-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e22dfed744bd4002e909464cb23d2f0b05c6f3113a79ef2e9864a53db737c733", size = 4738357, upload-time = "2026-08-25T19:45:02.786Z" }, + { url = "https://files.pythonhosted.org/packages/27/3a/3c5f80daa4dcd47323c7af8a2fcb90de27a33564d4fcac69846c0972691a/cryptography-50.0.1-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:4c4188f7c0cf655be5c06342b817ed0f9595b69ffa2b12026e5353eed29dea88", size = 4758474, upload-time = "2026-08-25T19:45:04.889Z" }, + { url = "https://files.pythonhosted.org/packages/6e/2b/214cf0cf93db9628c3c20c896b229f327f6fb1b20e4b3743d8ad3f00af8b/cryptography-50.0.1-cp39-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:2ebbfb0f1fed745e91796e3e1080a1440423fdae8ece1b995a1d80883a409054", size = 5375862, upload-time = "2026-08-25T19:45:07.163Z" }, + { url = "https://files.pythonhosted.org/packages/d6/51/3f9701867a46b6c1740c9b52fc4d3bed6cbdcfedcc9b6e64305c07f39cff/cryptography-50.0.1-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:407fe2b6db00939c05c0e945e9914238f2f0a430974839429dafc82b1ee6bee5", size = 4772942, upload-time = "2026-08-25T19:45:09.396Z" }, + { url = "https://files.pythonhosted.org/packages/0d/5c/13ea642e08e2544d0f5396122055f4820cfacb3203562197b5967125ea97/cryptography-50.0.1-cp39-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:2b34d76a652ea2b6faf777c35df230c5637842cd904e04f16230c3f9f03e4361", size = 4383347, upload-time = "2026-08-25T19:45:11.659Z" }, + { url = "https://files.pythonhosted.org/packages/84/d5/7d1fe1cb93f91c428093ff234e128c89ba8ea61a6f26aab406081f9b996e/cryptography-50.0.1-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:01f41478cf33fc605a6a089cd56d28b45c6c0b45a1928b61797f2621a04bac71", size = 4758050, upload-time = "2026-08-25T19:45:13.745Z" }, + { url = "https://files.pythonhosted.org/packages/dd/04/557fc5ead96a829e0bc812a3b9dc4a52a2f27e4f7f5950da7ff27653a805/cryptography-50.0.1-cp39-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:fc3ed7ebd2a8c96f5b166de0ab9b624996bef3b07bbeb19364dfb78222c22c80", size = 5332955, upload-time = "2026-08-25T19:45:16.193Z" }, + { url = "https://files.pythonhosted.org/packages/8c/eb/5d7124083e8d8cda8f5b348f544b71ad6f707ad63193758ef4d8e569da02/cryptography-50.0.1-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:9dde0a357190eb3b1da1bb9ab750e9c85cba82ca5977aa0836cbb94e92611239", size = 4772694, upload-time = "2026-08-25T19:45:18.315Z" }, + { url = "https://files.pythonhosted.org/packages/63/8e/f1f955e0921dd2b6d22eae7e8d24a4c4b638d10735ffbf6a71f99eb0fcb8/cryptography-50.0.1-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:fd3718b960d0b5dd213cdf03f3bcb7000e69dda0de8b956061947ff6bcff5558", size = 4888413, upload-time = "2026-08-25T19:45:20.4Z" }, + { url = "https://files.pythonhosted.org/packages/1f/ab/89e2b798d2c3925f82e2bb72d5979f3d2f6da2dd22ef4a8cd8b70d920039/cryptography-50.0.1-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:2a93d05e34d5f67fba6f891fe85d929999baa7195e853923ea6d7576c9e68c5e", size = 5044355, upload-time = "2026-08-25T19:45:22.353Z" }, + { url = "https://files.pythonhosted.org/packages/99/89/87ef49ffe383ef4e147d27b7bf2088fb0b54ea409dd87b5a89442e5828a5/cryptography-50.0.1-cp39-abi3-win_amd64.whl", hash = "sha256:55d16b1ef3ee0958d893a977b19777887e546c9954ea81b200c3301a864013f2", size = 3875429, upload-time = "2026-08-25T19:45:24.418Z" }, + { url = "https://files.pythonhosted.org/packages/c7/27/8d207af749c453ee17ea087340b3f2b4adef75aadd1d277b1b129bdda84e/cryptography-50.0.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:9cb3cb952cf5a8abd50c782a98a89d71699715e802fe349704b47f2425b42a94", size = 3974350, upload-time = "2026-08-25T19:45:26.551Z" }, + { url = "https://files.pythonhosted.org/packages/14/9a/6d3a4d7852e22d657438b7bf51f66102c7d71c0e1fafeec652281d0403e5/cryptography-50.0.1-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:5fe939deeb161024a6be98229c953b6591fef1f41214497a78fe793a244c017f", size = 4698675, upload-time = "2026-08-25T19:45:28.658Z" }, + { url = "https://files.pythonhosted.org/packages/73/35/5c3717edf9e68a0550ce04e28eab493fe545eccd81742af03f6a75fe260b/cryptography-50.0.1-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:fb4b9672d389c738b175c4166e78310f8a70358886aacd9173ee03a85ffdc671", size = 4707410, upload-time = "2026-08-25T19:45:30.816Z" }, + { url = "https://files.pythonhosted.org/packages/1d/e0/e786934472e3ac4ecdecc7b129a0ca1a2a40dffdafcf2c3ea9d4397f8def/cryptography-50.0.1-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:d63ae8f6481fec907ac0f588eee8a90aefde112c633131fe540e5711ddbb5a4e", size = 4698378, upload-time = "2026-08-25T19:45:33.043Z" }, + { url = "https://files.pythonhosted.org/packages/51/cf/5b3f53a0b74d122f023476ede40ba5d3e70d5cf475f73b899740d26a4fb2/cryptography-50.0.1-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:804728ce710890870f3aaa344b2e161172d258d768ac139d02cfd9092d0d94e6", size = 4706889, upload-time = "2026-08-25T19:45:35.086Z" }, + { url = "https://files.pythonhosted.org/packages/71/44/711e61f7d014be825ef79b285b047292d1bf893732ac1bc030a351fb517f/cryptography-50.0.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:693c99b49bd37d0d096e4334c10232c77248c415b98d35236094cdf96d57258b", size = 3824006, upload-time = "2026-08-25T19:45:37.281Z" }, +] + +[[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 = "httpcore2" +version = "2.13.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "h11", marker = "python_full_version < '3.12' or python_full_version >= '3.14' or sys_platform != 'emscripten'" }, + { name = "truststore", marker = "python_full_version < '3.12' or python_full_version >= '3.14' or sys_platform != 'emscripten'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/15/8c/e925b1c92018abb3a1863ce1549d76d2381e334d21d65d4ac8f65dabd78a/httpcore2-2.13.0.tar.gz", hash = "sha256:2adc8be4fb285fbcd6d894298db3b52c177e74b6674eda3a76bd36be3292a3db", size = 67740, upload-time = "2026-09-14T14:18:04.717Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/0d/117a771a2bb91df334b66bf4da14cd02f21aefbcfe53180f336ce55e8f90/httpcore2-2.13.0-py3-none-any.whl", hash = "sha256:35ae5be347aa40467b4a5dc032ac67ebb6d27189fc97e8cebcf99616f6a1bb9e", size = 83162, upload-time = "2026-09-14T14:18:02.529Z" }, +] + +[[package]] +name = "httpx2" +version = "2.13.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio", marker = "sys_platform != 'emscripten'" }, + { name = "httpcore2", marker = "sys_platform != 'emscripten'" }, + { name = "httpx2-jsfetch", marker = "python_full_version >= '3.12' and sys_platform == 'emscripten'" }, + { name = "idna" }, + { name = "truststore", marker = "sys_platform != 'emscripten'" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b9/a0/e9deef4654132857b5a5dbe4eddd0ac59c2814500e11f2f5044cd81103ee/httpx2-2.13.0.tar.gz", hash = "sha256:81bd07dc67a3701729ef1f777a3c00c915d4539604fdb5afd327f8682f6b7b44", size = 100290, upload-time = "2026-09-14T14:18:05.486Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fe/d1/a0c72b0e006df654709fbc366cc5bcb53e5aee13e1e3395152c6dd293376/httpx2-2.13.0-py3-none-any.whl", hash = "sha256:fc12720cedf72faa26cca6b4ca394e05c894e7d7933fc45cafe767960804e49a", size = 95565, upload-time = "2026-09-14T14:18:03.553Z" }, +] + +[[package]] +name = "httpx2-jsfetch" +version = "1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cd/c4/0e5636363151a2a1795e0a77617168b9ca438e1748ec05fc9b5687f93d64/httpx2_jsfetch-1.0.tar.gz", hash = "sha256:70a0e3eabfef7cce5ad9c629f7d01ca05e418f586646f4ddf14782e4c1454c60", size = 6872, upload-time = "2026-08-07T00:13:07.492Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9b/43/832f631d32e4f1211caa2ba368317739fe71f0b8530e4c9d15dc454bac2a/httpx2_jsfetch-1.0-py3-none-any.whl", hash = "sha256:cb916b707601e69a07721aabc8f3f6659be3a6893bc1ff5c6f9e02241df2da32", size = 6382, upload-time = "2026-08-07T00:13:06.567Z" }, +] + +[[package]] +name = "idna" +version = "3.20" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f5/08/8eea9d4b8302028f3abb2c0813953f7aec26d33b7a8960ed760e65ff29fa/idna-3.20.tar.gz", hash = "sha256:a7db850025b95ded1eae8a46181a1a6c56c92c96f0e2b005d9ff8dc0210cab44", size = 216463, upload-time = "2026-09-17T14:11:04.752Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/58/a2/bb081bab032533a855d44de1d56f8e8426114ff1ba5d1f07a438a0a654f8/idna-3.20-py3-none-any.whl", hash = "sha256:ab7ae7122974553370f0bdb919e1a960b2cd1bc1ef0276416d896db81c14582c", size = 69583, upload-time = "2026-09-17T14:11:03.168Z" }, +] + +[[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 = "mcp" +version = "2.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "httpx2" }, + { name = "jsonschema" }, + { name = "mcp-types" }, + { name = "opentelemetry-api" }, + { name = "pydantic" }, + { 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/76/31/ac54fb0fdd5b37de704486e288bba4fbbb463f24cfcfedbede407b854513/mcp-2.2.0.tar.gz", hash = "sha256:2dc37ecb1974becdcebdbf7561e7c15a07dbbf20ba21ba16c3593b3038b3afbd", size = 4084129, upload-time = "2026-09-07T16:06:23.439Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1b/ff/8e7eade68b8a28f7da0ed1085544341b51f9c935dbf6b95c76b7edfea6a0/mcp-2.2.0-py3-none-any.whl", hash = "sha256:bde982589473a060ae145e3406e9a5333fe538c97229ba841f5a7f92be004f81", size = 365656, upload-time = "2026-09-07T16:06:19.711Z" }, +] + +[[package]] +name = "mcp-types" +version = "2.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ae/91/762d7755d971aff8a28d75f7961656148edf27875c8026e6385aaab08ae7/mcp_types-2.2.0.tar.gz", hash = "sha256:d3ed53703ddd10d9c6399f29d322bb66f3f67ab41348ac8556ba23e07fedefad", size = 65892, upload-time = "2026-09-07T16:06:25.187Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8f/d7/6ffba5d8cd5dd9b8a19478875c50e04945314ba5074e84d749283f27f62d/mcp_types-2.2.0-py3-none-any.whl", hash = "sha256:ea476b73ee86709ab5abc9452385ed36cc05907e582355622e294595c9a04f13", size = 69106, upload-time = "2026-09-07T16:06:21.461Z" }, +] + +[[package]] +name = "opentelemetry-api" +version = "1.44.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ee/8b/aa9e2d8b8dfa7c946f7dec5d1f8f6ba8eca062f43509a06bdb5ce93d26c0/opentelemetry_api-1.44.0.tar.gz", hash = "sha256:67647e5e9566edcf421166fdf022b3537f818635daa852b289e34604dc6fb33a", size = 72406, upload-time = "2026-07-16T15:25:32.678Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ca/6f/a04e900f465ff3221ccc395522503e2d10e79fa21f2723c8e177aae1e0d1/opentelemetry_api-1.44.0-py3-none-any.whl", hash = "sha256:94b98c893a91b88657eaac1e3ba89618cdb85be6918196705354f34728b2cdef", size = 60018, upload-time = "2026-07-16T15:25:11.657Z" }, +] + +[[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.13.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/53/ef/fc4f868f4e2cee79f863883abffceff107875f569b848507319842d2a681/pydantic-2.13.5.tar.gz", hash = "sha256:51a9c5f7b2f8e636f04c6cada605d9b6a3bf1348fdf945a3d8869b19bba0ee08", size = 845750, upload-time = "2026-08-28T14:04:00.916Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/eb/47/c95ffc2009878c7aac0c5e08528022dcb885933252a88b5f170058014464/pydantic-2.13.5-py3-none-any.whl", hash = "sha256:346a034f080da3755d8e9cb5e00e8b07de1d39e4f6e2c87d8ab7cafa0b269a73", size = 472589, upload-time = "2026-08-28T14:03:59.136Z" }, +] + +[[package]] +name = "pydantic-core" +version = "2.46.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/af/f9/8a06bea35ef8daf588f707784c973a7046e0034c8d8cfb08828eeffb8b75/pydantic_core-2.46.5.tar.gz", hash = "sha256:10416c15b8839ecc4ef4d0885da76da6fd0f67333a0eb8aff6d93c4b8f2910fc", size = 472262, upload-time = "2026-08-28T10:01:31.677Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a2/b6/81d2d19ea0be2c03664381b59f65fa72fc7969decedae00bc2c4ad835708/pydantic_core-2.46.5-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a1dee1b804ff4d11c663636cf15d2ea47e9f79cd56c033fb1cbf08924842a48f", size = 2074737, upload-time = "2026-08-28T09:57:57.711Z" }, + { url = "https://files.pythonhosted.org/packages/0c/18/b70da8300e292df4099684ea11b1958043580d2f50d2dc8bf7e542bdd84a/pydantic_core-2.46.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d625a186a65201c23a9e3b8ed9c47e90a026e03256608cc91851c6709096844f", size = 1921751, upload-time = "2026-08-28T09:57:59.265Z" }, + { url = "https://files.pythonhosted.org/packages/e7/1a/0d590341b6ffa4b4aca83508e6b8db4761aaeacfc15a25ca3815876d4797/pydantic_core-2.46.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4f8507560a9284e1370bb048ed4282012fbef4e8d109875b95e884d228552061", size = 1948231, upload-time = "2026-08-28T09:58:00.678Z" }, + { url = "https://files.pythonhosted.org/packages/7d/1d/02eb35761c51f2f7b1b042d6ab4cda6600f0c8c88a2243b3f734376201e5/pydantic_core-2.46.5-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5f93c5fe914d75fbec9a49209b00da5f08e9e467d69da2b1510c81940cfd10be", size = 2020708, upload-time = "2026-08-28T09:58:02.267Z" }, + { url = "https://files.pythonhosted.org/packages/4a/ea/f86073830e35d508cc8ddf9c3d9e6e6840fcb88d34bf726b0b4710186f27/pydantic_core-2.46.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:aca6c767f552b21b10f774aeac128e828eafb796adfa1b666a18bf6321453c3a", size = 2194914, upload-time = "2026-08-28T09:58:03.934Z" }, + { url = "https://files.pythonhosted.org/packages/bb/d7/fc36240d7791ce90939e51608568c33bfdae26202016f9770c229a487d86/pydantic_core-2.46.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:701b2e04b560eeb4bddf7a25ab8ca476176e34fdbd9a0e18196f0d12d4685f0b", size = 2235622, upload-time = "2026-08-28T09:58:05.516Z" }, + { url = "https://files.pythonhosted.org/packages/cf/bc/3fa2d76b83162820a17da7f645b28d1cba99fc8e1e5fc6517067ec450fa1/pydantic_core-2.46.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:49776eab08766a08dfff7012f8b422dcd7e25e43b316eedf0477c24fcfa84b7c", size = 2062091, upload-time = "2026-08-28T09:58:07.135Z" }, + { url = "https://files.pythonhosted.org/packages/ab/9a/095d557bb492c90cd8a70a6dd048bf793d433d03d86c81c11e912e4cd049/pydantic_core-2.46.5-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:a2468d93d181667a7abd66e1b64bb9f76f361b0fef8faddf687456453576f5ee", size = 2089904, upload-time = "2026-08-28T09:58:08.814Z" }, + { url = "https://files.pythonhosted.org/packages/24/98/7b76b1ad10a19a617a52aaa1d80e159115af939b095e86f8e756fd52e0df/pydantic_core-2.46.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:53feb344243bb9510a9dec7bf3cf1b64d88a98af5dc7872a5160465f8b198c8e", size = 2132244, upload-time = "2026-08-28T09:58:10.435Z" }, + { url = "https://files.pythonhosted.org/packages/20/32/7d6ca365fadba186a0c8f85de1a701663bce81efd309d9479be58687622f/pydantic_core-2.46.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:cd5214352ae68f3b5e9af7768bdc5253695ee069675db3480518420b3be881f2", size = 2143901, upload-time = "2026-08-28T09:58:12.033Z" }, + { url = "https://files.pythonhosted.org/packages/f8/09/eb9a6aa57f22fd1541a9c0aa2a1f3aeef3ec65347d33e10a6da2f43e0ee9/pydantic_core-2.46.5-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:9432f3598db432cb51c5b37fdbf29a60fcccc79e30d37a05022776a6bc4ab689", size = 2299425, upload-time = "2026-08-28T09:58:13.614Z" }, + { url = "https://files.pythonhosted.org/packages/8a/f9/548a5bb9d4ba8cd26e26daf48052236f6b38bb61e7b7241fbc3c995719eb/pydantic_core-2.46.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:8feeac04b5794e513e710af2f9c87d49f31a6dc47967bb264a1fed61a8989bec", size = 2318566, upload-time = "2026-08-28T09:58:15.199Z" }, + { url = "https://files.pythonhosted.org/packages/4a/20/06454d18834c02c406c9133f1a3b485305fd9ee984f9636c2f730bef6a9d/pydantic_core-2.46.5-cp311-cp311-win32.whl", hash = "sha256:892a881d5f68c2b9ea304b7a6c2c60d9343df578a311b0f86b94bc8f1ffe8129", size = 1954258, upload-time = "2026-08-28T09:58:16.813Z" }, + { url = "https://files.pythonhosted.org/packages/9e/c2/718b9deb4b72453b5d8c7447a3b14cb77bef36917ef5f514e0948a4096a0/pydantic_core-2.46.5-cp311-cp311-win_amd64.whl", hash = "sha256:40375c2d05acec10323e45dfe2077ac44bc74659008614af5069034e2cfc781c", size = 2041030, upload-time = "2026-08-28T09:58:18.288Z" }, + { url = "https://files.pythonhosted.org/packages/67/ea/c1d1a5b72d6e1ff7f377a4d9199f6591f095beb5b409a8a5d89f7238d939/pydantic_core-2.46.5-cp311-cp311-win_arm64.whl", hash = "sha256:28a6a556cd3b6066bea827857f9d9cce027c96f776e512f544a581f9e42161f8", size = 2009234, upload-time = "2026-08-28T09:58:19.929Z" }, + { url = "https://files.pythonhosted.org/packages/82/3f/76358795aa7a8c6d4f36e2cb828ad1c90ee118e1393a9281664f5aade9d4/pydantic_core-2.46.5-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:b9fe6fb92520e3fd61f2e49000b6911b188824f089b75973ea06d6267f0b476d", size = 2076516, upload-time = "2026-08-28T09:58:21.576Z" }, + { url = "https://files.pythonhosted.org/packages/db/50/26b091836076ce4cb2fac264186936acc069e0595772cfd02a563bc4761a/pydantic_core-2.46.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a39ac25a9a2fa4072efdb429833c4a4c8009a51ff9eea3eeae131713cd27991e", size = 1922874, upload-time = "2026-08-28T09:58:23.766Z" }, + { url = "https://files.pythonhosted.org/packages/09/f0/2a8ce3849e299d44e2d2c196b6082643a3235565a735cb51db7a6261f614/pydantic_core-2.46.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4fdc8b93a41521988916eeaa271173fcca7fa0803d62f87675aac8dcec1c8e29", size = 1951772, upload-time = "2026-08-28T09:58:25.435Z" }, + { url = "https://files.pythonhosted.org/packages/87/46/ac0dc8bdd9e6048183a14eb127764e7ad9240021c17513074a4711b0e31e/pydantic_core-2.46.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b98134087d9de723658d17a42c7d0da8d6e2ef08015dee7dc93889047315f5e4", size = 2031832, upload-time = "2026-08-28T09:58:27.102Z" }, + { url = "https://files.pythonhosted.org/packages/c4/c2/339de5bef7be36301a2231eaa52e62163742c2281f11b5f4892bc79785cd/pydantic_core-2.46.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e652ab17569c94bff5475520f907b7148b8c24036a8ebbe5cf7cf7493d28579a", size = 2208645, upload-time = "2026-08-28T09:58:28.948Z" }, + { url = "https://files.pythonhosted.org/packages/7b/a0/9ff22b797724262da14427abaed4dd1d864a139693fc5e7809114376a716/pydantic_core-2.46.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d925f3d9afd05a8c0fb3a1031463a8d59ebe5e2afad297e29c78be19e13b4e62", size = 2265935, upload-time = "2026-08-28T09:58:30.625Z" }, + { url = "https://files.pythonhosted.org/packages/c0/a4/eb9409ec0736e50aa70a412f16c204ed149516846912f7e6724d4c73ee53/pydantic_core-2.46.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0fc5be0abd4a407e200d844b404e33639a554e7bd0d448e7b9ae181be4789ac2", size = 2066284, upload-time = "2026-08-28T09:58:32.289Z" }, + { url = "https://files.pythonhosted.org/packages/c0/02/7f6156ffc926857f1c37c07d9a388682865a81830ab6a1b637082c25e399/pydantic_core-2.46.5-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:816ff0a6550ffc06c098ccd2e0698600f9aa7da192a79eaa6f9af504a35db869", size = 2105889, upload-time = "2026-08-28T09:58:33.986Z" }, + { url = "https://files.pythonhosted.org/packages/92/b1/e781d357ebe09fc929f995700f1b3503e8897f1cece183ecb1300d4d67e9/pydantic_core-2.46.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c7ea57fc63aa7da93a1bd2d644e6577befae10c52c4e36377635eea1056a74f5", size = 2158006, upload-time = "2026-08-28T09:58:35.647Z" }, + { url = "https://files.pythonhosted.org/packages/70/0a/644597d84ab400e50609c192120b85c9681c22d3a20461b9060a79be0a7a/pydantic_core-2.46.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:efd62a42486f1bda5d24cb4f63d15a3c7768375fe83d36f9417b4ad7a2fb20b3", size = 2158408, upload-time = "2026-08-28T09:58:37.38Z" }, + { url = "https://files.pythonhosted.org/packages/1e/ee/ca3b7b3a4b3769ffe9ce9432a7c9be755de9593a46d3b0d54d0409323e44/pydantic_core-2.46.5-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:2bc9419666990c06d7397831f2126a1ecc3594aaa3ff7de5bf2d066802f4e07b", size = 2309609, upload-time = "2026-08-28T09:58:39.22Z" }, + { url = "https://files.pythonhosted.org/packages/ce/52/39fa1f451486019524ca685020390e7ca351832fd874530ba30c8628e6dc/pydantic_core-2.46.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:18a09e1e1011b462f2e32774f25859ef1223d5c2b0546a633cf56654710721e0", size = 2342618, upload-time = "2026-08-28T09:58:40.89Z" }, + { url = "https://files.pythonhosted.org/packages/81/5e/468fc630568c61dcef3cd47ad32ffbeed9af643f49208d1ea86ab4f890c4/pydantic_core-2.46.5-cp312-cp312-win32.whl", hash = "sha256:5cb482e9e84c851f4e623fe4acc1ced89168cf1fe18f7089db4548c8f5bbb65b", size = 1939475, upload-time = "2026-08-28T09:58:42.591Z" }, + { url = "https://files.pythonhosted.org/packages/cf/c9/4c19f41b84cf6b622a72fbeed7665b25d47a187d68d47d0d430c07f23268/pydantic_core-2.46.5-cp312-cp312-win_amd64.whl", hash = "sha256:5e81740c09e310f5aa5cbd3e434a01c154d4bef93241c7877b39f211d2b78ba8", size = 2043140, upload-time = "2026-08-28T09:58:44.272Z" }, + { url = "https://files.pythonhosted.org/packages/af/dd/0c1a050299147c746e5256db16d645ab5efd4f78c59937d581a0524e74a2/pydantic_core-2.46.5-cp312-cp312-win_arm64.whl", hash = "sha256:f7b0ec93a2893de856652154d73b7ba622f26fa97726487dcac373de5f4c6084", size = 1997729, upload-time = "2026-08-28T09:58:46.13Z" }, + { url = "https://files.pythonhosted.org/packages/f5/37/5abe39a8372a61d3dc3c1338fc504281c01b32fdb3169cd7187153b56d3e/pydantic_core-2.46.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:b7ca9034437b6022f941f4857459562ee00a560b97e7cce8a0ec5a74fc6766e0", size = 2075885, upload-time = "2026-08-28T09:58:47.856Z" }, + { url = "https://files.pythonhosted.org/packages/21/43/6323b1f8b217780454c61304bcd2b38ae4762f50754414124603ccc90bb2/pydantic_core-2.46.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f332f0e72a5a0400141f830744e141bf9f97917878dbe968669e8a7fefea78ff", size = 1922768, upload-time = "2026-08-28T09:58:49.58Z" }, + { url = "https://files.pythonhosted.org/packages/0f/a3/c05ca796e1197618a774b01e596aeedfefc2f7d8c01ae3054e910b120e8a/pydantic_core-2.46.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:193375f3548919d3f0b60936ca113ada3e38f264f91b9b8e0508efaad57be931", size = 1951241, upload-time = "2026-08-28T09:58:51.511Z" }, + { url = "https://files.pythonhosted.org/packages/68/32/33bc39ac705c52cffc908e8389f9754fdb208aea5c69cceddf4eb3ce99af/pydantic_core-2.46.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:79bdfa52f843137045b2d081cc05c120ba6665d29b7559c2c47690906f39279f", size = 2031975, upload-time = "2026-08-28T09:58:53.166Z" }, + { url = "https://files.pythonhosted.org/packages/b0/70/2333e885c0f6a67bc105c5916965dac9b57f2718ee20d81d1a06a4ebdc13/pydantic_core-2.46.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:24922243639cbdac66c75fcb6fd6495a9cb52b213d62f9a0d16f0310b1ff8038", size = 2208542, upload-time = "2026-08-28T09:58:55.017Z" }, + { url = "https://files.pythonhosted.org/packages/f7/ea/296debfb4264207bbda5936133892e027c0a58875ad53ebd512fba8ec3a2/pydantic_core-2.46.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c76fe65e607be28c7fd4d56fc3c42b1583aa058ce3408b7ad0fd540171d31f9f", size = 2264692, upload-time = "2026-08-28T09:58:56.767Z" }, + { url = "https://files.pythonhosted.org/packages/d3/f2/9e4de77a6271e07a76d2d58b11c091a979c191ed2939bf80067568b369d2/pydantic_core-2.46.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6f7b393a8b3da82f5c1fc0751e6d01ac6c55b93c18226a60bdfba4a724efafd1", size = 2066633, upload-time = "2026-08-28T09:58:58.531Z" }, + { url = "https://files.pythonhosted.org/packages/8d/db/f9e9d0c97445987b2084823d5c240de88087338f04fc2cfaa2df186b8049/pydantic_core-2.46.5-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:7ac031912d54f3d83ef3b3eb98dfabc1608802e2202263d25957eeed40b94761", size = 2105235, upload-time = "2026-08-28T09:59:00.421Z" }, + { url = "https://files.pythonhosted.org/packages/07/c5/79169b047b3b2c3e99e04bc76372af9637e0bf6db638274fa927df96369e/pydantic_core-2.46.5-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:837b396ca3d7b74091ca623f6cbd8351bd42d670a79c2683e79fb089f06a2de5", size = 2157367, upload-time = "2026-08-28T09:59:02.442Z" }, + { url = "https://files.pythonhosted.org/packages/26/b5/ba6057afb7c291bd449f51b867f95aef2072941c4ce4e5c31d6ffd132d3b/pydantic_core-2.46.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:5ee239d575f80b08eca11f6e20f90c4c695de7825c67eefe6091fbf20dda648e", size = 2158420, upload-time = "2026-08-28T09:59:04.2Z" }, + { url = "https://files.pythonhosted.org/packages/6e/28/2057abecaafdc22912afa819603a51f0a62d40643b7c4871c51721fea9be/pydantic_core-2.46.5-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:e80675d75ae2cd14372cb65cad5400d9347a3d3f6c13000183f22dfd027283ed", size = 2309588, upload-time = "2026-08-28T09:59:06.048Z" }, + { url = "https://files.pythonhosted.org/packages/71/9d/881156dc404e27479c4246128d73538464cab4a239bec61995e227644c30/pydantic_core-2.46.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:9c4b71f10dd532fb7a5cbc8f58707779e64f03a258c2bf8bfbaecfcd9970b519", size = 2341866, upload-time = "2026-08-28T09:59:08.539Z" }, + { url = "https://files.pythonhosted.org/packages/5a/38/d66f443a259f84d13babdceae568e572b0ed26da17ca5d0a649ebb110a67/pydantic_core-2.46.5-cp313-cp313-win32.whl", hash = "sha256:97bf8de4d541598c94a59344eeb988a94c08ff76b5723c41f6567ec18c7892ea", size = 1938580, upload-time = "2026-08-28T09:59:10.402Z" }, + { url = "https://files.pythonhosted.org/packages/2c/1e/1d5371213f4cc9a7ed70c0bfcc7911de22311ee99a662a56077d7292d2ac/pydantic_core-2.46.5-cp313-cp313-win_amd64.whl", hash = "sha256:15f4a94963c95accac15b7b657bb177d3ad82bb90b0d0526d9a9b85079925db5", size = 2041980, upload-time = "2026-08-28T09:59:12.396Z" }, + { url = "https://files.pythonhosted.org/packages/5a/48/4222d90b1c67568bace4dec6dca6271449c66de3595d72b6d098f5fde597/pydantic_core-2.46.5-cp313-cp313-win_arm64.whl", hash = "sha256:d22a945598fb91236b4dd793a6e42e4f3dd7740bb5aace5ebd7d4c08d13bb575", size = 1997213, upload-time = "2026-08-28T09:59:14.245Z" }, + { url = "https://files.pythonhosted.org/packages/8e/8a/14596f2a8367da50cf7cbac48169ee5d9c8e11d486a3b527082384630c72/pydantic_core-2.46.5-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:c1c43ad4339643d70ebb8124e1305a7dab423001eff58bb41a0f731adbc98355", size = 2074081, upload-time = "2026-08-28T09:59:16.141Z" }, + { url = "https://files.pythonhosted.org/packages/ae/d5/d8a4eb6d6c7f66b91dd37c576d76e9e60fba900caf5372c17bcf949febc2/pydantic_core-2.46.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1a353f84de772f423b5ffb11d7ae352fbbef0f446f3c0b0af0f8236d7233606e", size = 1920497, upload-time = "2026-08-28T09:59:18.065Z" }, + { url = "https://files.pythonhosted.org/packages/8e/26/092079428f86e927e030b2c0ced87df69dbb1c875cdeaa67bf42ea2be746/pydantic_core-2.46.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5086029a57366b8cf81b130a43908738095c270c21a8d7f0e8bdfdb89718e2f3", size = 1952130, upload-time = "2026-08-28T09:59:20.476Z" }, + { url = "https://files.pythonhosted.org/packages/08/c3/8ec0e290a9ebaebd64047bf5fda94be835c6b1551b02437e4b76778fbcd7/pydantic_core-2.46.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:46c25dda9d092a06c08db76ffe0a197107904d0dfac653f7d5306bbcd6d6119c", size = 2026371, upload-time = "2026-08-28T09:59:22.227Z" }, + { url = "https://files.pythonhosted.org/packages/01/72/4fd20ad520fb8da0157f95b27a7eb05a72790ef08138e7701ac972c342ea/pydantic_core-2.46.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:37ea7b83c935e5b0d68c9449b82651accf78a10828b2c02b2f2d9e9496446c21", size = 2202822, upload-time = "2026-08-28T09:59:24.277Z" }, + { url = "https://files.pythonhosted.org/packages/31/b0/d16e0771206b29314f0d52198b720be21e8a99ab2bf11e3bc0d7c9cebdff/pydantic_core-2.46.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e64e88d5585bea9ce95861079de72006c7fa6d3df4e3a3b65ba31eb979c15c9f", size = 2262756, upload-time = "2026-08-28T09:59:26.608Z" }, + { url = "https://files.pythonhosted.org/packages/2c/9b/59634b7ac631c63b2a37760eb6943af3e29573d6b59a4abc5e7f019d4cee/pydantic_core-2.46.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:54d510bac3ee52247af28ed4bb18a1e799f040ac60fd2bf5ccd4c92f1fbe786f", size = 2068352, upload-time = "2026-08-28T09:59:29.044Z" }, + { url = "https://files.pythonhosted.org/packages/08/7c/570abb1ad2155348dc754ea91be22e5aaa18eb6d69a6068f7c6f2679a6ed/pydantic_core-2.46.5-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:a2a5e1d0ff29adddc9f6d6821a66302e4493f8ca898b715b6b1182c2c201ea0a", size = 2104777, upload-time = "2026-08-28T09:59:30.95Z" }, + { url = "https://files.pythonhosted.org/packages/8e/25/5bf74adc65a1ac5b7be3f6cb0bcb5433615c1598a801c19d830d84c98ded/pydantic_core-2.46.5-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:03b9666e41e35d8909852ba191a0607520f81b74eaf12ccf8737005dbb313821", size = 2156312, upload-time = "2026-08-28T09:59:32.604Z" }, + { url = "https://files.pythonhosted.org/packages/90/6a/2ef38830675e050121040618135564ed56b860b45433b02d9b4ebece46f3/pydantic_core-2.46.5-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:a91c17edf6eea2402cb5457b4c89e99bc5ed1004aa34c4adf1d4258c1a5c22c2", size = 2150067, upload-time = "2026-08-28T09:59:34.453Z" }, + { url = "https://files.pythonhosted.org/packages/90/ef/a7dbb03a14a64c2a4621f989c615ed9a892535a6cad938fc27079f919d80/pydantic_core-2.46.5-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:b49924c73a235e969511bf2aabdff3beebf9820931f646c80274d5d780010c47", size = 2304516, upload-time = "2026-08-28T09:59:36.194Z" }, + { url = "https://files.pythonhosted.org/packages/68/f8/6bb4c4b80e8a6fde1904c64a51c62a1d04fcdfa3ea521a66b2ddefa1d885/pydantic_core-2.46.5-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:2cbd9a5eff05e51c447c34dfa4632145b26b09120cf04bd0c871e44c1a5e1c9a", size = 2335223, upload-time = "2026-08-28T09:59:37.931Z" }, + { url = "https://files.pythonhosted.org/packages/2a/80/f46b8c681195190b2c1f1c7c0a81abce60663e987613e09ef64d433dd96b/pydantic_core-2.46.5-cp314-cp314-win32.whl", hash = "sha256:2d5d76654becf5efd62c9e51c3756c67b49498b0c9a40884934c40807adbd074", size = 1934827, upload-time = "2026-08-28T09:59:39.836Z" }, + { url = "https://files.pythonhosted.org/packages/f7/3c/60674207246bc0a4009d2391b7c7251c7159f279c8d2ab8aae8ef46f3dee/pydantic_core-2.46.5-cp314-cp314-win_amd64.whl", hash = "sha256:fa10ef4112775900e7a0661068635eb67b2ab824fbde764de6e0e21982a93db0", size = 2042648, upload-time = "2026-08-28T09:59:41.792Z" }, + { url = "https://files.pythonhosted.org/packages/69/0c/117c562c7c1babdf44576b72a5e496906506c93690387ecfbca7c729ae2e/pydantic_core-2.46.5-cp314-cp314-win_arm64.whl", hash = "sha256:045ab3b6d308439e32b81cc173bba5b9018bc6ed896afd0c65b3b009b1699af5", size = 1989652, upload-time = "2026-08-28T09:59:43.702Z" }, + { url = "https://files.pythonhosted.org/packages/e8/66/9336ae58f9eb68c41d121894e52c4c89eccb07eb8f602a04ee9c3f37736a/pydantic_core-2.46.5-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8816f3d218beb4b787de5c9759c259b8fa61f9dec42dc7811f320a33771778b7", size = 2065829, upload-time = "2026-08-28T09:59:45.364Z" }, + { url = "https://files.pythonhosted.org/packages/c5/02/bc19b47a96c2d3109760711acf22369e56bd7e405ca52f7ade164d2ead57/pydantic_core-2.46.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:bce57638e08ac148e5778cce7feb968307a727d66f8e2274a543d0cf0c9ad6a3", size = 1905716, upload-time = "2026-08-28T09:59:47.18Z" }, + { url = "https://files.pythonhosted.org/packages/52/a4/70b47c0509923dd98ccfed04fb3e32ea3849c82a0ff2205bb41009b43c00/pydantic_core-2.46.5-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:976e1128455aa595ea04c79ccfedff1aaeab96ee013fcc916bed120c4f0ad94f", size = 1934216, upload-time = "2026-08-28T09:59:49.241Z" }, + { url = "https://files.pythonhosted.org/packages/52/ab/aa03b65f7bb198585edf806b906c3223ecf1795543e39e23aec4cce27ad2/pydantic_core-2.46.5-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e7b891faeedeafba41b2983e5001a81b6a915b69544c7e7570d1989ce1c36ac7", size = 2010635, upload-time = "2026-08-28T09:59:51.692Z" }, + { url = "https://files.pythonhosted.org/packages/3c/8b/0da06343f30b84ec549aafd309c6456223d5dc8bd36af504c573faad561d/pydantic_core-2.46.5-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5f194189415698233dd1114a093a9b56e61e2c57e11b469be3b0506f46f0771c", size = 2209369, upload-time = "2026-08-28T09:59:53.582Z" }, + { url = "https://files.pythonhosted.org/packages/d6/5b/844c4defaa34a3df66eb9257087d121d70c201298b96abdf9f492fc2f1bf/pydantic_core-2.46.5-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:82a36973cf8a2ef5406f4fe2edbf8ed0c99629535d959e0b100c76a32535a111", size = 2253238, upload-time = "2026-08-28T09:59:55.484Z" }, + { url = "https://files.pythonhosted.org/packages/f4/64/a4e536cb16d7f61a7fd3120b46c577fc7fa7325992f69c4f52bc786d77d8/pydantic_core-2.46.5-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cdbb78909f52b981d3b2d56b97328d71eb0b974c36bd77c920123a7ebb192829", size = 2065740, upload-time = "2026-08-28T09:59:58.038Z" }, + { url = "https://files.pythonhosted.org/packages/5f/75/aaa38c6bc2d085f6605b34eabdc6a8a4e0b2e61fc9c8e6e52b28e97b3125/pydantic_core-2.46.5-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:52e24eacdb536cade636aa90fb851835222becff8484b7001fdc78cb0290f2aa", size = 2087425, upload-time = "2026-08-28T09:59:59.898Z" }, + { url = "https://files.pythonhosted.org/packages/55/ae/fcab4cfc39aba3689e1d20c8b5250ad280957022c09af2ed9cd585602a5e/pydantic_core-2.46.5-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:37ae34309d7bd8c0d61ab839668058f2a7962ea1fc51d105d2db228fe0618034", size = 2139306, upload-time = "2026-08-28T10:00:03.057Z" }, + { url = "https://files.pythonhosted.org/packages/2d/f4/f1d03a4bc9d9acbc62f4d742b8a319af52f71885079868b2ff8e48a651ee/pydantic_core-2.46.5-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:0cdbada856a1c69a7624a64d3d9aefe79300bd6ef827b43a4f265010b9b55184", size = 2144589, upload-time = "2026-08-28T10:00:05.645Z" }, + { url = "https://files.pythonhosted.org/packages/83/f3/7a53bb1356de514a4cd295f25b6ac39237895620c0462d2592b76c16e114/pydantic_core-2.46.5-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:545f26c504b27c3758439a5e6d9349931f0a04f855668d5fe323c89e82300a38", size = 2288882, upload-time = "2026-08-28T10:00:07.931Z" }, + { url = "https://files.pythonhosted.org/packages/cd/94/5a81583660c175c59d49ffb09f4b3a44debeaf86a19fca664ae1cdd9ee32/pydantic_core-2.46.5-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:ff218293c9c806138dca139765e3b067621be52bcd93cdc14c7711be7ddc90a9", size = 2335210, upload-time = "2026-08-28T10:00:10.177Z" }, + { url = "https://files.pythonhosted.org/packages/5a/9f/5d685c2693b972d1a59c998586e8823712b66603aeff47ee60a4bdaafd37/pydantic_core-2.46.5-cp314-cp314t-win32.whl", hash = "sha256:97cf3eb53a8cccacf9d46686a0926186c9bfb5574f2ed66d3639d5fe117cd3a9", size = 1921180, upload-time = "2026-08-28T10:00:12.35Z" }, + { url = "https://files.pythonhosted.org/packages/70/12/5c94ee16d65a37a15f9e869f5e6256df111154491173801a4c5e800ab548/pydantic_core-2.46.5-cp314-cp314t-win_amd64.whl", hash = "sha256:d2f9fc07a8042a8f95925b35c4f04f469707c981fc33245b6ca187cf5d2dd290", size = 2020515, upload-time = "2026-08-28T10:00:14.774Z" }, + { url = "https://files.pythonhosted.org/packages/63/19/67830dda664e6bdf9285ee2e40f355d0d7d6b92aa0c42e8d217bb8d33d36/pydantic_core-2.46.5-cp314-cp314t-win_arm64.whl", hash = "sha256:acf8a67ba51f4ca9ddbd0e6b3000a65ac51ab734661778b3e7ba64d99a710f2f", size = 1989276, upload-time = "2026-08-28T10:00:16.984Z" }, + { url = "https://files.pythonhosted.org/packages/af/1e/ecca01fce348f7e8afa9572441ff6f7d1cc70d21e4859f33944d10877e1e/pydantic_core-2.46.5-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:c14ad3bdc85ee7f318742c457ca3968a92126d144b15721c759033bfb06296c2", size = 2075342, upload-time = "2026-08-28T10:00:51.353Z" }, + { url = "https://files.pythonhosted.org/packages/1f/4c/af80c7a8032dfc897040ad5cb772bebde529a381186499e6e29987f23f8c/pydantic_core-2.46.5-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:0bddb4020d8f04175865ccd17eff3040874fc11fb593f424edb452653b4b947c", size = 1907219, upload-time = "2026-08-28T10:00:53.438Z" }, + { url = "https://files.pythonhosted.org/packages/be/3e/54d89e2b092e778716bf6153634ef479e955f48c261090be23aa1e0fb0b5/pydantic_core-2.46.5-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2471fd51c61c610e1dcf7de44d7299283661654d11264ab4802b303368d69c47", size = 1953393, upload-time = "2026-08-28T10:00:55.58Z" }, + { url = "https://files.pythonhosted.org/packages/ea/89/828ee90cda28ce17bdefaa3a6eaf74fe430e113295a10e6126beca559d6c/pydantic_core-2.46.5-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b10ec717381bdbfafef34607824db4c91de69ff085e4fca3b2af91b4fa17e68a", size = 2099024, upload-time = "2026-08-28T10:00:57.794Z" }, + { url = "https://files.pythonhosted.org/packages/df/dd/053c2e4303f791f3b8f8a14ab0b22008e8eb21d868c0c90b4f9be705b76a/pydantic_core-2.46.5-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:013d6f3483d81e02e7c328831808f336c8596ee33b4bd4026b9ffb1e960b8942", size = 2062540, upload-time = "2026-08-28T10:01:00.318Z" }, + { url = "https://files.pythonhosted.org/packages/d7/dd/a18df751a5e37dd51bfad7f68e766999125bebe68c9e1d10a493ad01bd63/pydantic_core-2.46.5-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:e9c134bb666dd54b778b9fc0d2b50cbb7f979b9e3716f26a88c9ab3b6fc1dd0f", size = 1902040, upload-time = "2026-08-28T10:01:02.529Z" }, + { url = "https://files.pythonhosted.org/packages/b7/13/01d40f9d07ce8a779fd6e0bd8ad4fba91309500dd67b869e2e219d261a6d/pydantic_core-2.46.5-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:347ec774390c87326a2e4929d58d3f7e8763a104d5d35f4cd595a4c952366433", size = 1967479, upload-time = "2026-08-28T10:01:05.004Z" }, + { url = "https://files.pythonhosted.org/packages/fa/04/c81d4841331c2178b6fb09ae225425e110ed72d990c9fe556c4ec03d1013/pydantic_core-2.46.5-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8e24d8f05fa2d28513d94e877e9c75ad66175376209b3977f916e240e623193c", size = 2111034, upload-time = "2026-08-28T10:01:07.345Z" }, + { url = "https://files.pythonhosted.org/packages/20/21/22102e9950b3049526d20e811b95396508377d87651edd2b80d2b3d28659/pydantic_core-2.46.5-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:ab4b66edffb32d9e951efb3814bd104b8367a7501b81b955cacb5726d897389f", size = 2071333, upload-time = "2026-08-28T10:01:09.636Z" }, + { url = "https://files.pythonhosted.org/packages/d8/18/87aefa427d191e6d3ab1447f1efc1cdcac86af1069239b133e8a0fd7f7c9/pydantic_core-2.46.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:337639ba62a11acde6ef3aeb08c8ea755f8ef1fe5e513356c0f36a2b0d7568b0", size = 1912713, upload-time = "2026-08-28T10:01:12.285Z" }, + { url = "https://files.pythonhosted.org/packages/1f/93/fd89e9ad49b1805ca94d24ce1088b7d305f05c35ffafcedb9819d03588a0/pydantic_core-2.46.5-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:413a717a410d0c817ef5b786a059415550b3794e1d0c2abffd9efb93a3d9f7b4", size = 2090926, upload-time = "2026-08-28T10:01:15.19Z" }, + { url = "https://files.pythonhosted.org/packages/6f/45/8e59dab6acf8d35f02f0a958980074f31038968bdb2c983fcae9d1efee03/pydantic_core-2.46.5-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1e449def1945a462c464331254e5a44fca7c3b4f9aedf59ec2f50f8066dd8e25", size = 2131303, upload-time = "2026-08-28T10:01:17.937Z" }, + { url = "https://files.pythonhosted.org/packages/d5/a5/e1d4dc5180dd887a9522efc1f8716b8692b7606b1d3273d7862eaf66be44/pydantic_core-2.46.5-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:a445486499897b88a7d6c310c88ed64dd37b1b59bfd7ae9107490bbb362f47d6", size = 2145128, upload-time = "2026-08-28T10:01:20.694Z" }, + { url = "https://files.pythonhosted.org/packages/c2/d7/ad493864a7fb21c0c4df98f965e2db430cb25a9d7369b5778d5016c09fd9/pydantic_core-2.46.5-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:2d330aaba8621b1edcec8ae2c4050f63b84ccf6d98723a8f212e9684713abf0e", size = 2294560, upload-time = "2026-08-28T10:01:23.495Z" }, + { url = "https://files.pythonhosted.org/packages/02/8e/b41c84c913f29973a268e6c2b5bbf13c95adb9956c126d10da11ba3b2bef/pydantic_core-2.46.5-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:b6acfb46a814762367fb7ba0828b0a17d441b92ce249a0e007474c9072662dda", size = 2317531, upload-time = "2026-08-28T10:01:26.334Z" }, + { url = "https://files.pythonhosted.org/packages/db/1d/068464f23075f66a8f1b806935e9cd9363ee446636ea70d2c22ee8659dbf/pydantic_core-2.46.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:d0a24b40877af2de4950252be9d21eaf7fb07660f3c2cae1f56c6b599ada5266", size = 2140686, upload-time = "2026-08-28T10:01:28.947Z" }, +] + +[[package]] +name = "pyjwt" +version = "2.14.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/af/c3/8a3b59c25070cc61dc517fbdfa5dc0904670c96f605cc69759dc09166b99/pyjwt-2.14.0.tar.gz", hash = "sha256:77283c83fb56ecf566a886c757a714bc83668e38156de2cce8263302f42e0b86", size = 113177, upload-time = "2026-09-11T13:11:54.638Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9c/97/672cb32ce0dfea44b740cb7b4f97038463b9cf7c0ead1aacf595572851d6/pyjwt-2.14.0-py3-none-any.whl", hash = "sha256:ad0cef71c756a56e74863c2919cf0985f72decbcfcb550ee2f422e7c62b5eedc", size = 32896, upload-time = "2026-09-11T13:11:53.409Z" }, +] + +[package.optional-dependencies] +crypto = [ + { name = "cryptography" }, +] + +[[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 = "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" }, + { url = "https://files.pythonhosted.org/packages/2d/41/12fbfd7f36ed2146d8bc9de96c2741296bf0d490b98508496cff322e274c/pywin32-312-cp313-cp313-win32.whl", hash = "sha256:7a27df850933d16a8eabfbaeb73d52b273e2da667f80d70b01a89d1f6828d02c", size = 6370184, upload-time = "2026-06-04T07:49:36.253Z" }, + { url = "https://files.pythonhosted.org/packages/ba/db/36a78e3403099d31d9746d13fdcde5accc43c1155f375a34d15983a479a7/pywin32-312-cp313-cp313-win_amd64.whl", hash = "sha256:c53e878d15a1c44788082bfe712a905433473aa38f86375b7cf8b45e3acbaaf9", size = 6914298, upload-time = "2026-06-04T07:49:38.876Z" }, + { url = "https://files.pythonhosted.org/packages/84/37/c1697194092b76de9ed47ca124323f02c57ffc8a45c06f88a3d5acaf01eb/pywin32-312-cp313-cp313-win_arm64.whl", hash = "sha256:59aba5d5940842075343a5ddc6b11f1cdf0d1567fe745290359dfbcc7c2eb831", size = 6727640, upload-time = "2026-06-04T07:49:41.083Z" }, + { url = "https://files.pythonhosted.org/packages/fc/2b/1f3cded5822fd49c02f40544cbb5f58c7cfd6b1694869fd476cb6170ee97/pywin32-312-cp314-cp314-win32.whl", hash = "sha256:a77a90fbb6881238d2ca9c6fd797b25817f3768fe78d214a90137ff055a75f5b", size = 6468928, upload-time = "2026-06-04T07:49:43.188Z" }, + { url = "https://files.pythonhosted.org/packages/21/82/3bf86d2e2808902013132e1ce905a7da0da53790f3836c64bf44d55e24f3/pywin32-312-cp314-cp314-win_amd64.whl", hash = "sha256:a4dd3a848290ef724347b19f301045831d8e802fa4464f491b98b1e0a081432e", size = 7024157, upload-time = "2026-06-04T07:49:45.34Z" }, + { url = "https://files.pythonhosted.org/packages/a4/0e/73f6d6800b4f27655abd9e9f6aaeaefcddb2b946e4674efa2bab184a7f7b/pywin32-312-cp314-cp314-win_arm64.whl", hash = "sha256:9fce94568364e0155e6dfb781ac5d95903be8baf28670632beab1b523f300daa", size = 6839598, upload-time = "2026-06-04T07:49:47.613Z" }, + { url = "https://files.pythonhosted.org/packages/eb/61/caa39686032d2ebdd04ff0ab5cbe163126c0066d98e00c9018646e42393b/pywin32-312-cp315-cp315-win32.whl", hash = "sha256:5c1fbe4a937a73ae9297384a3da38518cbc694c68ad8a809b2e19acd350f03ed", size = 6471159, upload-time = "2026-06-04T07:49:50.035Z" }, + { url = "https://files.pythonhosted.org/packages/0f/cd/7e1de64a4a6f69c04214169657ccab0d93a670ea50e35eb8f489d7378249/pywin32-312-cp315-cp315-win_amd64.whl", hash = "sha256:c2f03a0f73f804a13c2735b99392b0cd426bb4f2c4d0178e5ac966a0f21618d5", size = 7025293, upload-time = "2026-06-04T07:49:54.857Z" }, + { url = "https://files.pythonhosted.org/packages/23/ed/4532e9388e65fa16b46776ef47ad631a64eda1631884488af707666350ed/pywin32-312-cp315-cp315-win_arm64.whl", hash = "sha256:a8597d28f267b39074aef51fa593530082b39cbe5a074226096857b1fed2dfb9", size = 6840337, upload-time = "2026-06-04T07:49:57.531Z" }, +] + +[[package]] +name = "referencing" +version = "0.37.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "rpds-py" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/22/f5/df4e9027acead3ecc63e50fe1e36aca1523e1719559c499951bb4b53188f/referencing-0.37.0.tar.gz", hash = "sha256:44aefc3142c5b842538163acb373e24cce6632bd54bdb01b21ad5863489f50d8", size = 78036, 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 = "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/a4/9e/b818ee580026ec578138e961027a68820c40afeb1ec8f6819b54fb99e196/rpds_py-2026.6.3-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:3cfe765c1da0072636ca06628261e0ea05688e160d5c8a03e0217c3854037223", size = 343012, upload-time = "2026-06-30T07:15:36.005Z" }, + { url = "https://files.pythonhosted.org/packages/f3/6b/686d9dc4359a8f163cfbbf89ee0b4e586431de22fe8248edb63a8cf50d49/rpds_py-2026.6.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f4d78253f6996be4901669ad25319f842f740eccf4d58e3c7f3dd39e6dde1d8f", size = 338203, upload-time = "2026-06-30T07:15:37.462Z" }, + { url = "https://files.pythonhosted.org/packages/9e/9b/069aa329940f8207615e091f5eedbbd40e1e15eac68a0790fd05ccdf796c/rpds_py-2026.6.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:54f45a148e28767bf343d33a684693c70e451c6f4c0e9904709a723fafbdfc1f", size = 367984, upload-time = "2026-06-30T07:15:39.008Z" }, + { url = "https://files.pythonhosted.org/packages/14/db/34c203e4becff3703e4d3bc121842c00b8689197f398161203a880052f4e/rpds_py-2026.6.3-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:842e7b070435622248c7a2c44ae53fa1440e073cc3023bc919fed570884097a7", size = 374815, upload-time = "2026-06-30T07:15:40.253Z" }, + { url = "https://files.pythonhosted.org/packages/ee/7d/8071067d2cc453d916ad836e828c943f575e8a44612537759002a1e07381/rpds_py-2026.6.3-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8020133a74bd81b4572dd8e4be028a6b1ebcd70e6726edc3918008c08bee6ee6", size = 490545, upload-time = "2026-06-30T07:15:41.729Z" }, + { url = "https://files.pythonhosted.org/packages/a3/42/da06c5aa8f0484ff07f270787434204d9f4535e2f8c3b51ed402267e63c3/rpds_py-2026.6.3-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cdc7e35386f3847df728fbcb5e887e2d79c19e2fa1eba9e51b6621d23e3243af", size = 382828, upload-time = "2026-06-30T07:15:43.327Z" }, + { url = "https://files.pythonhosted.org/packages/57/d7/fe978efc2ae50abe48eb7464668ea99f53c010c60aeebb7b35ad27f23661/rpds_py-2026.6.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:acac386b453c2516111b50985d60ce46e7fadb5ea71ae7b25f4c946935bf27cf", size = 365678, upload-time = "2026-06-30T07:15:44.992Z" }, + { url = "https://files.pythonhosted.org/packages/69/9d/1d8922e1990b2a6eb532b6ff53d3e73d2b3bbffc84116c75826bee73dfc6/rpds_py-2026.6.3-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:425560c6fa0415f27261727bb20bd097568485e5eb0c121f1949417d1c516885", size = 377811, upload-time = "2026-06-30T07:15:46.523Z" }, + { url = "https://files.pythonhosted.org/packages/b1/3d/198dceafb4fb034a6a47347e1b0735d34e0bd4a50be4e898d408ee66cb14/rpds_py-2026.6.3-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a550fb4950a06dde3beb4721f5ad4b25bf4513784665b0a8522c792e2bd822a4", size = 395382, upload-time = "2026-06-30T07:15:47.955Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f1/13968e49655d40b6b19d8b9140296bbc6f1d86b3f0f6c346cf9f1adddf4b/rpds_py-2026.6.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4f4bca01b63096f606e095734dd56e74e175f94cfbf24ff3d63281cec61f7bb7", size = 543832, upload-time = "2026-06-30T07:15:49.33Z" }, + { url = "https://files.pythonhosted.org/packages/ac/ab/289bcb1b90bd3e40a2900c561fa0e2087345ecbb094f0b870f2345142b7c/rpds_py-2026.6.3-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ccffae9a092a00deb7efd545fe5e2c33c33b88e7c054337e9a74c179347d0b7d", size = 611011, upload-time = "2026-06-30T07:15:50.847Z" }, + { url = "https://files.pythonhosted.org/packages/1e/16/5043105e679436ccfbc8e5e0dd2d663ed18a8b8113515fd06a5e5d77c83e/rpds_py-2026.6.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1cf01971c4f2c5553b772a542e4aaf191789cd331bc2cd4ff0e6e65ba49e1e97", size = 572431, upload-time = "2026-06-30T07:15:52.394Z" }, + { url = "https://files.pythonhosted.org/packages/85/ed/adab103321c0a6565d5ae1c2998349bc3ee175b82ccc5ae8fc04cc413075/rpds_py-2026.6.3-cp313-cp313-win32.whl", hash = "sha256:8c3d1e9c15b9d51ca0391e13da1a25a0a4df3c58a37c9dc368e0736cf7f69df0", size = 201710, upload-time = "2026-06-30T07:15:53.894Z" }, + { url = "https://files.pythonhosted.org/packages/7b/ed/a03b09668e74e5dabbf2e211f6468e1820c0552f7b0500082da31841bf7b/rpds_py-2026.6.3-cp313-cp313-win_amd64.whl", hash = "sha256:9250a9a0a6fd4648b3f868da8d91a4c52b5811a62df58e753d50ae4454a36f80", size = 219454, upload-time = "2026-06-30T07:15:55.25Z" }, + { url = "https://files.pythonhosted.org/packages/27/17/b8642c12930b71bc2b25831f6708ccf0f75abcd11883932ec9ce54ba3a78/rpds_py-2026.6.3-cp313-cp313-win_arm64.whl", hash = "sha256:900a67df3fd1660b035a4761c4ce73c382ea6b35f90f9863c36c6fd8bf8b09bb", size = 215063, upload-time = "2026-06-30T07:15:56.573Z" }, + { url = "https://files.pythonhosted.org/packages/b6/36/7fbe9dcdaf857fb3f63c2a2284b62492d95f5e8334e947e5fb6e7f68c9be/rpds_py-2026.6.3-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:931908d9fc855d8f74783377822be318edb6dcb19e47169dc038f9a1bf60b06e", size = 344510, upload-time = "2026-06-30T07:15:57.921Z" }, + { url = "https://files.pythonhosted.org/packages/ba/54/f785cc3d3f60839ca57a5af4927a9f347b07b2799c373fc20f7949f87c7e/rpds_py-2026.6.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:d7469697dce35be237db177d42e2a2ee26e6dcc5fc052078a6fefabd288c6edd", size = 339495, upload-time = "2026-06-30T07:15:59.238Z" }, + { url = "https://files.pythonhosted.org/packages/63/ef/d4cdaf309e6b095b43597103cf8c0b951d6cca2acce68c474f75ec12e0c7/rpds_py-2026.6.3-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bcfbcf66006befb9fd2aeaa9e01feaf881b4dc330a02ba07d2322b1c11be7b5d", size = 369454, upload-time = "2026-06-30T07:16:01.021Z" }, + { url = "https://files.pythonhosted.org/packages/96/4a/9559a68b7ee15db09d7981212e8c2e219d2a1d6d4faa0391d813c3496a36/rpds_py-2026.6.3-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:847927daf4cffbd4e90e42bc890069897101edd015f956cb8721b3473372edda", size = 374583, upload-time = "2026-06-30T07:16:02.287Z" }, + { url = "https://files.pythonhosted.org/packages/ef/75/8964aa7d2c6e8ac43eba8eb6e6b0fdda1f46d39f2fc3e6aa9f2cb17f485d/rpds_py-2026.6.3-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:aca6c1ef08a82bfe327cc156da694660f599923e2e6665b6d81c9c2d0ac9ffc8", size = 492919, upload-time = "2026-06-30T07:16:03.723Z" }, + { url = "https://files.pythonhosted.org/packages/8f/97/6908094ac804115e65aedfd90f1b5fee4eebebd3f6c4cfc5419939267565/rpds_py-2026.6.3-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ae50181a047c871561212bb97f7932a2d45fb53e947bd9b57ebad85b529cbc53", size = 383725, upload-time = "2026-06-30T07:16:05.305Z" }, + { url = "https://files.pythonhosted.org/packages/d1/9c/0d1fdc2e7aba23e290d603bc494e97bd205bae262ce33c6b32a69768ed5e/rpds_py-2026.6.3-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dc319e5a1de4b6913aac94bf6a2f9e847371e0a140a43dd4991db1a09bc2d504", size = 367255, upload-time = "2026-06-30T07:16:07.086Z" }, + { url = "https://files.pythonhosted.org/packages/c4/fe/f0209ca4a9ed074bc8acb44dfd0e81c3122e94c9689f5645b7973a866719/rpds_py-2026.6.3-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:e4316bf32babbed84e691e352faf967ce2f0f024174a8643c37c94a1080374fc", size = 379060, upload-time = "2026-06-30T07:16:08.525Z" }, + { url = "https://files.pythonhosted.org/packages/c6/8d/f1cc54c616b9d8897de8738aac148d20afca93f68187475fe194d09a71b9/rpds_py-2026.6.3-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8c6e5a2f750cc71c3e3b11d71661f21d6f9bc6cebc6564b1466417a1ec03ec77", size = 395960, upload-time = "2026-06-30T07:16:09.989Z" }, + { url = "https://files.pythonhosted.org/packages/fb/04/aafff00f73aeca2945f734f1d483c64ab8f472d0864ab02377fd8e89c3b2/rpds_py-2026.6.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:4470ce197d4090875cf6affbf1f853338387428df97c4fb7b7106317b8214698", size = 545356, upload-time = "2026-06-30T07:16:11.816Z" }, + { url = "https://files.pythonhosted.org/packages/fd/cc/e229663b9e4ddac5a4acbe9085dd80a71af2a5d356b8b39d6bff233f24b0/rpds_py-2026.6.3-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ea964164cc9afa72d4d9b23cc28dafae93693c0a53e0b42acbff15b22c3f9ddd", size = 612319, upload-time = "2026-06-30T07:16:13.586Z" }, + { url = "https://files.pythonhosted.org/packages/e3/7a/8a0e6d3e6cd066af108b71b43122c3fe158dd9eb86acac626593a2582eb1/rpds_py-2026.6.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:639c8929aa0afe81be836b04de888460d6bed38b9c54cfc18da8f6bfabf5af5d", size = 573508, upload-time = "2026-06-30T07:16:15.23Z" }, + { url = "https://files.pythonhosted.org/packages/87/03/2a69ab618a789cf6cf85c86bb844c62d090e700ab1a2aa676b3741b6c516/rpds_py-2026.6.3-cp314-cp314-win32.whl", hash = "sha256:882076c00c0a608b131187055ddc5ae29f2e7eaf870d6168980420d58528a5c8", size = 202504, upload-time = "2026-06-30T07:16:16.893Z" }, + { url = "https://files.pythonhosted.org/packages/85/62/a3892ba945f4e24c78f352e5de3c7620d8479f73f211406a97263d13c7d2/rpds_py-2026.6.3-cp314-cp314-win_amd64.whl", hash = "sha256:0be972be84cfcaf46c8c6edf690ca0f154ac17babf1f6a955a51579b34ad2dc5", size = 220380, upload-time = "2026-06-30T07:16:18.108Z" }, + { url = "https://files.pythonhosted.org/packages/3d/e7/c2bd44dc831931815ad11ebb5f430b5a0a4d3caa9de837107876c30c3432/rpds_py-2026.6.3-cp314-cp314-win_arm64.whl", hash = "sha256:2a9c6f195058cb45335e8cc3802745c603d716eb96bc9625950c1aac71c0c703", size = 215976, upload-time = "2026-06-30T07:16:19.654Z" }, + { url = "https://files.pythonhosted.org/packages/79/9c/fff7b74bce9a091ec9a012a03f9ff5f69364eaf9451060dfc4486da2ffdd/rpds_py-2026.6.3-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:f90938e92afda60266da758ee7d363447f7f0138c9559f9e1811629580582d90", size = 346840, upload-time = "2026-06-30T07:16:21.268Z" }, + { url = "https://files.pythonhosted.org/packages/e9/44/77bcb1168b33704908295533d27f10eb811e9e3e193e8993dc99572211d3/rpds_py-2026.6.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ec829541c45bca16e61c7ae50c20501f213605beb75d1aba91a6ee37fbbb56a4", size = 340282, upload-time = "2026-06-30T07:16:22.875Z" }, + { url = "https://files.pythonhosted.org/packages/87/3c/7a9081c7c9e645b39efe19e4ffbeccd80add246327cd9b888aecffd72317/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:afd70d95892096cdb26f15a00c45907b17817577aa8d1c76b2dcc2788391f9e9", size = 370403, upload-time = "2026-06-30T07:16:24.415Z" }, + { url = "https://files.pythonhosted.org/packages/f7/69/af47021eb7dad6ff3396cb001c08f0f3c4d06c20253f75be6421a59fe6b7/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:29dfa0533a5d4c94d4dfa1b694fcb56c9c63aad8330ffdd816fd225d0a7a162f", size = 376055, upload-time = "2026-06-30T07:16:26.111Z" }, + { url = "https://files.pythonhosted.org/packages/81/fc/a3bcf517084396a6dd258c592567a3c011ba4557f2fde23dceaf26e74f2e/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:af05d726809bff6b141be124d4c7ce998f9c9c7f30edb1f46c07aa103d540b41", size = 494419, upload-time = "2026-06-30T07:16:27.596Z" }, + { url = "https://files.pythonhosted.org/packages/c9/eb/13d529d1788135425c7bf207f8463458ca5d92e43f3f701365b83e9dffc1/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9826217f048f620d9a712672818bf231442c1b35d96b227a07eabd11b4bb6945", size = 384848, upload-time = "2026-06-30T07:16:29.183Z" }, + { url = "https://files.pythonhosted.org/packages/8e/f4/b7ac49f30013aba8f7b9566b1dd07e81de95e708c1374b7bacc5b9bc5c9c/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:536bceea4fa4acf7e1c61da2b5786304367c816c8895be71b8f537c480b0ea1f", size = 371369, upload-time = "2026-06-30T07:16:30.912Z" }, + { url = "https://files.pythonhosted.org/packages/31/86/6260bafa622f788b07ddec0e52d810305c8b9b0b8c27f58a2ab04bf62b4f/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:bc0011654b91cc4fb2ae701bec0a0ba1e552c0714247fa7af6c59e0ccfa3a4e1", size = 379673, upload-time = "2026-06-30T07:16:32.486Z" }, + { url = "https://files.pythonhosted.org/packages/19/c3/03f1ee79a047b48daeca157c89a18509cde22b6b951d642b9b0af1be660a/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:539d75de9e0d536c84ff18dfeb805398e58227001ce09231a26a08b9aed1ee0e", size = 397500, upload-time = "2026-06-30T07:16:34.471Z" }, + { url = "https://files.pythonhosted.org/packages/f0/95/8ed0cd8c377dca12aea498f119fe639fc474d1461545c39d2b5872eb1c0f/rpds_py-2026.6.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:166cf54d9f44fc6ceb53c7860258dde44a81406646de79f8ed3234fca3b6e538", size = 545978, upload-time = "2026-06-30T07:16:36.45Z" }, + { url = "https://files.pythonhosted.org/packages/d3/f2/0eb57f0eaa83f8fc152a7e03de968ab77e1f00732bebc892b190c6eebde7/rpds_py-2026.6.3-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:d34c20167764fbcf927194d532dd7e0c56772f0a5f943fa5ef9e9afbba8fb9db", size = 613350, upload-time = "2026-06-30T07:16:38.213Z" }, + { url = "https://files.pythonhosted.org/packages/5b/de/e0674bdbc3ef7634989b3f854c3f34bc1f587d36e5bfdc5c378d57034619/rpds_py-2026.6.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ea7bb13b7c9a29791f87a0387ba7d3ad3a6d783d827e4d3f27b40a0ff44495e2", size = 576486, upload-time = "2026-06-30T07:16:39.797Z" }, + { url = "https://files.pythonhosted.org/packages/f2/f6/21101359743cd136ada781e8210a85769578422ba460672eea0e29739200/rpds_py-2026.6.3-cp314-cp314t-win32.whl", hash = "sha256:6de4744d05bd1aa1be4ed7ea1189e3979196808008113bbbf899a460966b925e", size = 201068, upload-time = "2026-06-30T07:16:41.316Z" }, + { url = "https://files.pythonhosted.org/packages/a6/b2/9574d4d44f7760c2aa32d92a0a4f41698e33f5b204a0bf5c9758f52c79d5/rpds_py-2026.6.3-cp314-cp314t-win_amd64.whl", hash = "sha256:c7b9a2f8f4d8e90af72571d3d495deebdd7e3c75451f5b41719aee166e940fc2", size = 220600, upload-time = "2026-06-30T07:16:43.091Z" }, + { url = "https://files.pythonhosted.org/packages/08/ae/f23a2697e6ee6340a578b0f136be6483657bef0c6f9497b752bb5c0964bb/rpds_py-2026.6.3-cp315-cp315-macosx_10_12_x86_64.whl", hash = "sha256:e059c5dde6452b44424bd1834557556c226b57781dee1227af23518459722b13", size = 344726, upload-time = "2026-06-30T07:16:44.5Z" }, + { url = "https://files.pythonhosted.org/packages/c3/63/e7b3a1a5358dd32c930a1062d8e15b67fd6e8922e81df9e91706d66ee5c8/rpds_py-2026.6.3-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:2f7c26fbc5acd2522b95d4177fe4710ffd8e9b20529e703ffbf8db4d93903f05", size = 339587, upload-time = "2026-06-30T07:16:46.255Z" }, + { url = "https://files.pythonhosted.org/packages/ec/64/10a85681916ca55fffb91b0a211f84e34297c109243484dd6394660a8a7c/rpds_py-2026.6.3-cp315-cp315-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a3086b538543802f84c843911242db20447de00d8752dd0efc936dbcf02218ba", size = 369585, upload-time = "2026-06-30T07:16:48.101Z" }, + { url = "https://files.pythonhosted.org/packages/76/c2/baf95c7c38823e12ba34407c5f5767a89e5cf2233895e56f608167ae9493/rpds_py-2026.6.3-cp315-cp315-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8f2e5c5ee828d42cb11760761c0af6507927bec42d0ad5458f97c9203b054617", size = 375479, upload-time = "2026-06-30T07:16:49.93Z" }, + { url = "https://files.pythonhosted.org/packages/6a/94/0aad06c72d65101e11d33528d438cda99a39ce0da99466e156158f2541d3/rpds_py-2026.6.3-cp315-cp315-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ed0c1e5d10cdc7135537988c74a0188da68e2f3c30813ba3744ab1e42e0480f9", size = 492418, upload-time = "2026-06-30T07:16:51.641Z" }, + { url = "https://files.pythonhosted.org/packages/b5/17/de3f5a479a1f056535d7489819639d8cd591ea6281d700390b43b1abd745/rpds_py-2026.6.3-cp315-cp315-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8c2642a7603ec0b16ed77da4555db3b4b472341904873788327c0b0d7b95f1bb", size = 384123, upload-time = "2026-06-30T07:16:53.622Z" }, + { url = "https://files.pythonhosted.org/packages/46/7d/bf09bd1b145bb2671c03e1e6d1ab8651858d90d8c7dfeadd85a37a934fd8/rpds_py-2026.6.3-cp315-cp315-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8e4320744c1ffdd95a603def63344bfab2d33edeab301c5007e7de9f9f5b3885", size = 367351, upload-time = "2026-06-30T07:16:55.241Z" }, + { url = "https://files.pythonhosted.org/packages/a3/ea/1bb734f314b8be319149ddee80b18bd41372bdcfbdf88d28131c0cd37719/rpds_py-2026.6.3-cp315-cp315-manylinux_2_31_riscv64.whl", hash = "sha256:a9f4645593036b81bbdb36b9c8e0ea0d1c3fee968c4d59db0344c14087ef143a", size = 378827, upload-time = "2026-06-30T07:16:56.841Z" }, + { url = "https://files.pythonhosted.org/packages/4b/93/d9611e5b25e26df9a3649813ed66193ace9347a7c7fc4ab7cf70e94851c0/rpds_py-2026.6.3-cp315-cp315-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e55d236be29255554da47abe5c577637db7c24a02b8b46f0ca9524c855801868", size = 395966, upload-time = "2026-06-30T07:16:58.557Z" }, + { url = "https://files.pythonhosted.org/packages/c3/cb/99d77e16e5534ae1d90629bbe419ba6ee170833a6a85e3aa1cc41726fbbc/rpds_py-2026.6.3-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:24e9c5386e16669b674a69c156c8eeefcb578f3b3397b713b08e6d60f3c7b187", size = 545680, upload-time = "2026-06-30T07:17:00.164Z" }, + { url = "https://files.pythonhosted.org/packages/59/15/11a29755f790cef7a2f755e8e14f4f0c33f39489e1893a632a2eee59672b/rpds_py-2026.6.3-cp315-cp315-musllinux_1_2_i686.whl", hash = "sha256:c60924535c75f1566b6eb75b5c31a48a43fef04fa2d0d201acbad8a9969c6107", size = 611853, upload-time = "2026-06-30T07:17:01.962Z" }, + { url = "https://files.pythonhosted.org/packages/68/86/0c27547e21644da938fb530f7e1a8148dd24d02db07e7a5f2567a17ce710/rpds_py-2026.6.3-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:38a2fea2787428f811719ceb9114cb78964a3138838320c29ac39526c79c16ba", size = 573715, upload-time = "2026-06-30T07:17:03.693Z" }, + { url = "https://files.pythonhosted.org/packages/29/71/4d8fcf700931815594bce892255bbd973b94efaf0fc1932b0590df18d886/rpds_py-2026.6.3-cp315-cp315-win32.whl", hash = "sha256:d483fe17f01ad64b7bf7cc38fcefff1ca9fb83f8c2b2542b68f97ffe0611b369", size = 202864, upload-time = "2026-06-30T07:17:05.746Z" }, + { url = "https://files.pythonhosted.org/packages/eb/62/b577562de0edbb55b2be85ce5fd09c33e386b9b13eee09833af4240fd5c4/rpds_py-2026.6.3-cp315-cp315-win_amd64.whl", hash = "sha256:67e3a721ffc5d8d2210d3671872298c4a84e4b8035cfe42ffd7cde35d772b146", size = 220430, upload-time = "2026-06-30T07:17:07.471Z" }, + { url = "https://files.pythonhosted.org/packages/c8/95/d6d0b2509825141eef60669a5739eec88dbc6a48053d6c92993a5704defe/rpds_py-2026.6.3-cp315-cp315-win_arm64.whl", hash = "sha256:6e84adbcf4bf841aed8116a8264b9f50b4cb3e7bd89b516122e616ac56ca269e", size = 215877, upload-time = "2026-06-30T07:17:09.008Z" }, + { url = "https://files.pythonhosted.org/packages/b7/bf/f3ea278f0afd615c1d0f19cb69043a41526e2bb600c2b536eb192218eb27/rpds_py-2026.6.3-cp315-cp315t-macosx_10_12_x86_64.whl", hash = "sha256:ae6dd8f10bd17aad820876d24caec9efdafd80a318d16c0a48edb5e136902c6b", size = 346933, upload-time = "2026-06-30T07:17:10.762Z" }, + { url = "https://files.pythonhosted.org/packages/9d/29/9907bdf1c5346763cf10b7f6852aad86652168c259def904cbe0082c5864/rpds_py-2026.6.3-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:bdbd97738551fca3917c1bd7188bec1920bb520104f28e7e1007f9ceb17b7690", size = 340274, upload-time = "2026-06-30T07:17:12.266Z" }, + { url = "https://files.pythonhosted.org/packages/6f/2c/8e03767b5778ef25cebf74a7a91a2c3806f8eced4c92cb7406bbe060756d/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8b95977e7211527ab0ba576e286d023389fbeeb32a6b7b771665d333c60e5342", size = 370763, upload-time = "2026-06-30T07:17:14.107Z" }, + { url = "https://files.pythonhosted.org/packages/2e/e1/df2a7e1ba2efd796af26194250b8d42c821b46592311595162af9ef0528d/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d15fde0e6fb0d88a60d221204873743e5d9f0b7d29165e62cd86d0413ad74ba6", size = 376467, upload-time = "2026-06-30T07:17:15.76Z" }, + { url = "https://files.pythonhosted.org/packages/6b/de/8a0814d1946af29cb068fb259aa8622f856df1d0bab58429448726b537f5/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a136d453475ac0fcbda502ef1e6504bd28d6d904700915d278deeab0d00fe140", size = 496689, upload-time = "2026-06-30T07:17:17.308Z" }, + { url = "https://files.pythonhosted.org/packages/df/f3/f19e0c852ba13694f5a79f3b719331051573cb5693feacf8a88ffffc3a71/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f826877d462181e5eb1c26a0026b8d0cab05d99844ecb6d8bf3627a2ca0c0442", size = 385340, upload-time = "2026-06-30T07:17:18.928Z" }, + { url = "https://files.pythonhosted.org/packages/e2/ae/7ec3a9d2d4351f99e37bcb06b6b6f954512646bfdbf9742e1de727865daf/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:79486287de1730dbaff3dbd124d0ca4d2ef7f9d29bf2544f1f93c09b5bcbbd12", size = 372179, upload-time = "2026-06-30T07:17:20.539Z" }, + { url = "https://files.pythonhosted.org/packages/d3/ac/9cee911dff2aaa9a5a8354f6610bf2e6a616de9197c5fff4f54f82585f1e/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_31_riscv64.whl", hash = "sha256:808345f53cb952433ca2816f1604ff3515608a81784954f38d4452acfe8e61d5", size = 379993, upload-time = "2026-06-30T07:17:22.212Z" }, + { url = "https://files.pythonhosted.org/packages/83/6b/7c2a07ba88d1e9a936612f7a5d067467ed03d971d5a06f7d309dff044a7e/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1967debc37f64f2c4dc90a7f563aec558b471966e12adcac4e1c4240496b6ebf", size = 398909, upload-time = "2026-06-30T07:17:23.66Z" }, + { url = "https://files.pythonhosted.org/packages/97/0b/776ffcb66783637b0031f6d58d6fb55913c8b5abf00aeecd46bf933fb477/rpds_py-2026.6.3-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:f0840b5b17057f7fd918b76183a4b5a0635f43e14eb2ce60dce1d4ee4707ea00", size = 546584, upload-time = "2026-06-30T07:17:25.264Z" }, + { url = "https://files.pythonhosted.org/packages/55/33/ba3bc04d7092bd553c9b2b195624992d2cc4f3de1f380b7b93cbee67bd79/rpds_py-2026.6.3-cp315-cp315t-musllinux_1_2_i686.whl", hash = "sha256:faa679d19a6696fd54259ad321251ad77a13e70e03dd834daa762a44fb6196ef", size = 614357, upload-time = "2026-06-30T07:17:26.888Z" }, + { url = "https://files.pythonhosted.org/packages/8b/71/14edf065f04630b1a8472f7653cad03f6c478bcf95ea0e6aed55451e33ea/rpds_py-2026.6.3-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:23a439f31ccbeff1574e24889128821d1f7917470e830cf6544dced1c662262a", size = 576533, upload-time = "2026-06-30T07:17:28.546Z" }, + { url = "https://files.pythonhosted.org/packages/ba/76/65002b08596c389105720a8c0d22298b8dc25a4baf89b2ce431343c8b1de/rpds_py-2026.6.3-cp315-cp315t-win32.whl", hash = "sha256:913ca42ccad3f8cc6e292b587ae8ae49c8c823e5dce51a736252fc7c7cdfa577", size = 201204, upload-time = "2026-06-30T07:17:30.193Z" }, + { url = "https://files.pythonhosted.org/packages/8c/97/d855d6b3c322d1f27e26f5241c42016b56cf01377ea8ed348285f54652f0/rpds_py-2026.6.3-cp315-cp315t-win_amd64.whl", hash = "sha256:ae3d4fe8c0b9213624fdce7279d70e3b148b682ca20719ebd193a23ebfa47324", size = 220719, upload-time = "2026-06-30T07:17:31.788Z" }, + { 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 = "sse-starlette" +version = "3.4.11" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "starlette" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2b/54/6767bb789b2f2fed6e0f953df949cd39dc263a384c1b65a95232598621d6/sse_starlette-3.4.11.tar.gz", hash = "sha256:1bae716c02f3e6f294be41ff333220692dae7c3cbab077c900f159676719dade", size = 34972, upload-time = "2026-09-05T12:11:04.607Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/98/6a/2ba3ed4a69babf3afdddf7d8314a48d87562c0a442206bbc2a1b50d5efc0/sse_starlette-3.4.11-py3-none-any.whl", hash = "sha256:c7b2244bdff016fe7f64e10075e89a3e6bbf899649cc89b0fe884b5545042453", size = 17122, upload-time = "2026-09-05T12:11:03.195Z" }, +] + +[[package]] +name = "starlette" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b5/b4/205b0d5241d934e8add0c38aa924c4f9fb7330834ff11e5444db964ec3f9/starlette-1.6.0.tar.gz", hash = "sha256:d4e3ac5e546444960c710297a3c9fc3f7ebae1b7e963f3d36173b49da535be9b", size = 2716969, upload-time = "2026-08-08T18:27:57.512Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c8/cb/6a6a47d5b464bd08695d254f3da6e7986cc70c9fa5d778eda57538edfe56/starlette-1.6.0-py3-none-any.whl", hash = "sha256:a86dd39d14bb45f85a3d18525215a9ef0cfd1f192ac793220e72598c90335f0c", size = 75969, upload-time = "2026-08-08T18:27:56.196Z" }, +] + +[[package]] +name = "taskand-mcp-catalog" +version = "0.1.0" +source = { virtual = "." } +dependencies = [ + { name = "jsonschema" }, + { name = "mcp" }, +] + +[package.metadata] +requires-dist = [ + { name = "jsonschema", specifier = "==4.26.0" }, + { name = "mcp", specifier = "==2.2.0" }, +] + +[[package]] +name = "truststore" +version = "0.10.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/53/a3/1585216310e344e8102c22482f6060c7a6ea0322b63e026372e6dcefcfd6/truststore-0.10.4.tar.gz", hash = "sha256:9d91bd436463ad5e4ee4aba766628dd6cd7010cf3e2461756b3303710eebc301", size = 26169, upload-time = "2025-08-12T18:49:02.73Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/19/97/56608b2249fe206a67cd573bc93cd9896e1efb9e98bce9c163bcdc704b88/truststore-0.10.4-py3-none-any.whl", hash = "sha256:adaeaecf1cbb5f4de3b1959b42d41f6fab57b2b1666adb59e89cb0b53361d981", size = 18660, upload-time = "2025-08-12T18:49:01.46Z" }, +] + +[[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.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a3/26/b09b8010994eccc3c09092e6b34058f36a460eea2d4c3e8b910c695975a0/typing_inspection-0.4.4.tar.gz", hash = "sha256:547274fa6b0a561ccf549cc9524b999a578e737d015d8709d021f9d0d13bea47", size = 76928, upload-time = "2026-08-12T12:37:25.997Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/67/81/4add07e5172b7ac40d8ed5ff580409a7801a4fe26d529bdd915401dabfbe/typing_inspection-0.4.4-py3-none-any.whl", hash = "sha256:65b8397ba37ccbce054456aaccddfc91e6e3083c92824df348d96ca832f3f147", size = 14750, upload-time = "2026-08-12T12:37:24.648Z" }, +] + +[[package]] +name = "uvicorn" +version = "0.53.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5d/ad/04bbb797c84fc1f26cb171f7394716f4865ffb8d8c5e1eef42565c2dfa6b/uvicorn-0.53.0.tar.gz", hash = "sha256:a9356f0cb89b3b8621529c5d5eebd69bfe154f4c3f68b4cf2de47e45fa855c2e", size = 110881, upload-time = "2026-09-14T07:44:23.815Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/76/18/0eea75741ee812e9f598b687619ce2454f6c3a1c5cd21ea990ec6bd26f45/uvicorn-0.53.0-py3-none-any.whl", hash = "sha256:e8dca71ec86dce5f04e333f0d56cdedf942446e6643b9cea1af0d6d3a02cb03e", size = 87081, upload-time = "2026-09-14T07:44:22.179Z" }, +] diff --git a/project/ticket-030/README.md b/project/ticket-030/README.md new file mode 100644 index 0000000..f0ded3c --- /dev/null +++ b/project/ticket-030/README.md @@ -0,0 +1,23 @@ +# Ticket 030: Local MCP URI package catalog + +- **ID**: ticket-030 +- **Owner**: codex +- **Status**: IN_PROGRESS +- **Workflow state**: PUBLICATION +- **Created**: 2026-09-19 + +SESSION_EXECUTION_AUTHORIZATION: user requests all local and suitable GitHub MCP servers as URI processes in taskand/glm53/mcp with generated-like structure, and explicitly authorizes handoff from codex-mcp029-20260919. Intake PLF-006. + +Prior ticket-029 source, deployed pilot and PR38 remain intact, awaiting its protected executor prerequisite. Its writer reservation was explicitly handed off (CAS revision 3/fence 316 -> cancelled revision 4/fence 317). This separately allocated native package slice respects the existing ticket file budget and does not edit its controller or dashboard. One integration writer; no parallel writes to shared registry/contracts. + +## Acceptance + +- [x] AC-01: Inventory every local registration; build deterministic, versioned MCP process packages with metadata/schema/source provenance and private host configuration. +- [ ] AC-02: Native registry discovers and invokes admitted MCP package URI; real stdio/HTTP and invalid-input regressions pass. +- [x] AC-03: Verify local deployments, document unavailable servers and credential prerequisites, preserve unknown outcomes and existing services. + +Canonical result: docs/information/mcp-uri-catalog.md. + +Local validation: 45 servers / 422 tool packages, all discovered and registered. Seven explicitly admitted native gateway canaries passed; tillm health reports optional client dependencies unavailable. Existing adapter and controller tests pass. Full make test and managed governance pass. Published baseline documents have pre-existing docs findings, tracked separately from the new document. Protected publication remains separate from local delivery. + +Validation follow-up: full inventory exposes truncated doctor/prescribe and doctor/heal JSON (28/30 contracts). MCP suite passes; local integration commit is not final acceptance. Preserve this ticket and allocate a bounded follow-up for stdout flushing before claiming full completion. diff --git a/project/ticket-030/intent.json b/project/ticket-030/intent.json new file mode 100644 index 0000000..4212107 --- /dev/null +++ b/project/ticket-030/intent.json @@ -0,0 +1,124 @@ +{ + "schema": "new-project.intent/v3", + "ticket": "ticket-030", + "summary": "Import locally configured MCP servers and pinned GitHub implementations as native versioned URI packages under mcp/", + "workstream": "integration", + "classification": { + "kind": "FEATURE", + "priority": "P2", + "origin": "requested" + }, + "allowedPaths": [ + "project/ticket-030/**", + "mcp/**", + "generated/registry/registry.json", + "tests/mcp_catalog_test.py", + "tests/mcp_registry.test.mjs", + "docs/information/mcp-uri-catalog.md", + "docs/README.md", + ".governance/manifest.json", + "generated/registry/core/taskand.dev/v1/**", + "Makefile" + ], + "forbiddenPaths": [ + "project/ticket-*/user-*.md" + ], + "stacks": [], + "dependsOn": [], + "conflictsWith": [], + "integrationTicket": null, + "delivery": { + "acceptedBaseSha": "8804583ef82342ad7870659cf79e9ad8278e2369", + "targetBranch": "main", + "outcome": "Discover all local MCP configurations; build versioned process packages and native registry catalogs under mcp; invoke real tools through Taskand URI with pinned schemas and bounded SDK transport.", + "nonGoals": [ + "Replacing or merging PR38 controller", + "Autonomous tool admission or arbitrary tool execution", + "Copying credentials into repository", + "Restarting the existing 8077/8082 gateways" + ], + "complexity": "L", + "estimatedMinutes": 120, + "budgets": { + "maxImplementationFiles": 15, + "maxAffectedComponents": 5, + "maxPublicInterfaceChanges": 3, + "maxRuntimeDependencies": 2 + }, + "architecture": { + "status": "accepted", + "decision": "Native process package projection from official MCP tools/list with a reusable SDK bridge. Treat local MCP configs as trusted operator inputs, keep host profiles private, pin tool schemas and source versions; register mcp root through existing registry semantics. Generated per-tool packages are deployment artifacts, produced in an isolated release and never counted as handwritten source. Existing ticket029 controller remains an independent optional dashboard; this slice uses native gateway URI grants.", + "components": [ + { + "name": "mcp-packages", + "paths": [ + "mcp/**" + ] + }, + { + "name": "registry", + "paths": [ + "generated/registry/**" + ] + }, + { + "name": "tests", + "paths": [ + "tests/mcp_catalog_test.py", + "tests/mcp_registry.test.mjs", + "Makefile" + ] + }, + { + "name": "docs", + "paths": [ + "docs/information/mcp-uri-catalog.md", + "docs/README.md" + ] + }, + { + "name": "ownership", + "paths": [ + ".governance/manifest.json" + ] + } + ], + "responsibilityChanges": false, + "interfaceChanges": [ + "mcp package catalog CLI", + "native URI resolution in mcp root" + ], + "dataChanges": [ + "Candidate MCP process registry under mcp in deployment" + ], + "ui": { + "impact": "none", + "states": [], + "evidence": [] + }, + "rollback": "Stop only the isolated integration pilot and retain its profiles and receipts; original gateways remain unchanged." + }, + "runtimeDependencies": [ + "mcp==2.2.0", + "jsonschema==4.26.0" + ], + "validation": [ + { + "criterion": "AC-01", + "commands": [ + "python tests/mcp_catalog_test.py" + ], + "evidence": "Real stdio and HTTP tool discovery, schema pinning, error and boundary tests" + }, + { + "criterion": "AC-02", + "commands": [ + "node --test tests/mcp_registry.test.mjs", + "make test", + "./project/governance-check.sh" + ], + "evidence": "Native URI round trip in isolated release and managed repository gates" + } + ] + } +} diff --git a/tests/mcp_catalog_test.py b/tests/mcp_catalog_test.py new file mode 100644 index 0000000..882330a --- /dev/null +++ b/tests/mcp_catalog_test.py @@ -0,0 +1,214 @@ +"""Real SDK servers and native registry round trips, using isolated state.""" +import asyncio +import json +import os +from pathlib import Path +import shutil +import socket +import subprocess +import sys +import tempfile +import unittest + +ROOT = Path(__file__).resolve().parents[1] +sys.path.insert(0, str(ROOT / "mcp")) + + +if "--fixture" in sys.argv: + from mcp.server import MCPServer + server = MCPServer("catalog-test", log_level="ERROR") + state = Path(sys.argv[sys.argv.index("--fixture") + 1]) + + @server.tool() + def add(left: int, right: int) -> dict: + """Add two integers and persist the observed result.""" + value = {"sum": left + right} + (state / "result.json").write_text(json.dumps(value)) + return value + + @server.tool() + def uncertain() -> dict: + """Disconnect after one visible write.""" + with (state / "calls").open("a") as stream: + stream.write("1\n") + os._exit(1) + + if "--http" in sys.argv: + server.run(transport="streamable-http", host="127.0.0.1", port=int(sys.argv[-1])) + else: + server.run(transport="stdio") + raise SystemExit + +import bridge +import catalog + + +class CatalogTests(unittest.IsolatedAsyncioTestCase): + def setUp(self): + self.tmp = tempfile.TemporaryDirectory() + self.addCleanup(self.tmp.cleanup) + self.root = Path(self.tmp.name) + self.state = self.root / "state" + self.state.mkdir() + self.profile = {"transport": "stdio", "command": sys.executable, + "args": [str(Path(__file__).resolve()), "--fixture", str(self.state)], "env": {}} + self.registry = self.root / "generated/registry/core/taskand.dev/v1" + shutil.copytree(ROOT / "generated/registry/core/taskand.dev/v1", self.registry) + (self.root / "genome.yaml").write_text("organisms:\n") + (self.root / "log").mkdir() + self.profiles = self.root / "profiles.json" + catalog.write_private(self.profiles, {"fixture": self.profile}) + + async def tools(self): + async with bridge.client(self.profile) as client: + return await bridge.tool_catalog(client) + + def registry_call(self, action, **data): + env = {**os.environ, "TASKAND_MCP_PROFILES": str(self.profiles), "TASKAND_MCP_PYTHON": sys.executable} + result = subprocess.run(["node", str(self.registry / "bin.mjs")], input=json.dumps({"action": action, **data}), + capture_output=True, text=True, timeout=35, cwd=self.root, env=env) + self.assertEqual(result.returncode, 0, result.stderr) + return json.loads(result.stdout) + + async def prepared(self, name="add"): + tool = next(t for t in await self.tools() if t["name"] == name) + uri = catalog.emit(self.root / "mcp", {"fixture": self.profile}, {"fixture": [tool]})[0] + catalog.register(self.root, [uri]) + return uri, tool + + async def test_native_uri_candidate_approval_schema_and_result(self): + uri, tool = await self.prepared() + self.assertEqual(self.registry_call("call", uri=uri, input={"left": 2, "right": 3})["errorType"], "DENIED") + self.assertTrue(self.registry_call("approve", uri=uri)["ok"]) + exported = self.registry_call("export")["processes"][0] + self.assertEqual(exported["inputSchema"], tool["inputSchema"]) + result = self.registry_call("call", uri=uri, input={"left": 350, "right": 480}) + self.assertTrue(result["ok"], result) + self.assertEqual(json.loads((self.state / "result.json").read_text()), {"sum": 830}) + self.assertFalse(result["taskSuccessVerified"]) + self.assertTrue(self.registry_call("verify")["ok"]) + + async def test_invalid_arguments_and_profile_drift_never_dispatch(self): + uri, tool = await self.prepared() + descriptor = {"server": "fixture", "profilePin": bridge.digest(self.profile), "tool": tool} + result = await bridge.invoke(descriptor, {"fixture": self.profile}, {"left": "bad", "right": 2}) + self.assertEqual(result["errorType"], "ARGUMENT_SCHEMA_INVALID") + changed = {**self.profile, "args": []} + result = await bridge.invoke(descriptor, {"fixture": changed}, {"left": 1, "right": 2}) + self.assertEqual(result["errorType"], "PROFILE_CHANGED") + self.assertFalse((self.state / "result.json").exists()) + + async def test_changed_tool_schema_rejected_before_call(self): + tool = next(t for t in await self.tools() if t["name"] == "add") + descriptor = {"server": "fixture", "profilePin": bridge.digest(self.profile), + "tool": {**tool, "description": "obsolete contract"}} + result = await bridge.invoke(descriptor, {"fixture": self.profile}, {"left": 1, "right": 2}) + self.assertEqual(result["errorType"], "TOOL_SCHEMA_CHANGED") + self.assertFalse((self.state / "result.json").exists()) + + async def test_legacy_initialize_protocol_is_explicit(self): + profile = {**self.profile, "protocolMode": "legacy"} + async with bridge.client(profile) as session: + tool = next(t for t in await bridge.tool_catalog(session) if t["name"] == "add") + result = await bridge.invoke({"server": "legacy", "profilePin": bridge.digest(profile), "tool": tool}, + {"legacy": profile}, {"left": 3, "right": 4}) + self.assertTrue(result["ok"], result) + self.assertEqual(json.loads((self.state / "result.json").read_text()), {"sum": 7}) + + async def test_uncertain_effect_not_retried(self): + uri, _ = await self.prepared("uncertain") + self.assertTrue(self.registry_call("approve", uri=uri)["ok"]) + result = self.registry_call("call", uri=uri, input={}) + self.assertEqual(result["errorType"], "OUTCOME_UNKNOWN", result) + self.assertFalse(result["outcomeKnown"]) + self.assertEqual((self.state / "calls").read_text(), "1\n") + + async def test_immutable_packages_and_untrusted_metadata(self): + tools = await self.tools() + outputs = catalog.emit(self.root / "mcp", {"fixture": self.profile}, {"fixture": tools}) + self.assertEqual(outputs, catalog.emit(self.root / "mcp", {"fixture": self.profile}, {"fixture": tools})) + changed = [{**tools[0], "description": "Changed"}, *tools[1:]] + with self.assertRaisesRegex(bridge.ContractError, "IMMUTABLE_VERSION_CONFLICT"): + catalog.emit(self.root / "mcp", {"fixture": self.profile}, {"fixture": changed}) + self.assertNotEqual(catalog.slug("tool_name"), catalog.slug("tool-name")) + self.assertNotIn("/", catalog.slug("../../escape")) + + async def test_http_discovery_and_invocation(self): + with socket.socket() as sock: + sock.bind(("127.0.0.1", 0)) + port = sock.getsockname()[1] + proc = subprocess.Popen([sys.executable, str(Path(__file__).resolve()), "--fixture", str(self.state), "--http", str(port)], + stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + try: + for _ in range(100): + try: + with socket.create_connection(("127.0.0.1", port), timeout=.1): + break + except OSError: + await asyncio.sleep(.05) + profile = {"transport": "http", "url": f"http://127.0.0.1:{port}/mcp"} + async with bridge.client(profile) as session: + tool = next(t for t in await bridge.tool_catalog(session) if t["name"] == "add") + result = await bridge.invoke({"server": "http", "profilePin": bridge.digest(profile), "tool": tool}, + {"http": profile}, {"left": 2, "right": 4}) + self.assertTrue(result["ok"], result) + self.assertEqual(json.loads((self.state / "result.json").read_text()), {"sum": 6}) + finally: + proc.terminate() + proc.wait(timeout=5) + + def test_duplicate_configuration_is_explicit(self): + paths = [self.root / "first.json", self.root / "second.json"] + for path, command in zip(paths, ["first", "second"]): + path.write_text(json.dumps({"mcpServers": {"same": {"command": command}}})) + with self.assertRaisesRegex(bridge.ContractError, "CONFIGURATION_NAME_CONFLICT"): + catalog.merged_configurations(paths) + servers, aliases = catalog.merged_configurations(paths, prefer_first=True) + self.assertEqual(servers["same"]["command"], "first") + self.assertEqual(aliases["same"], [str(paths[1])]) + + def test_unavailable_server_reports_partial_inventory_and_nonzero_exit(self): + config = self.root / "broken.json" + config.write_text(json.dumps({"mcpServers": { + "missing": {"command": str(self.root / "missing-command")}, + "taskand": {"command": sys.executable}, + }})) + report = self.root / "report.json" + result = subprocess.run([sys.executable, str(ROOT / "mcp/catalog.py"), + "--config", str(config), "--output", str(self.root / "mcp"), + "--profiles", str(self.root / "partial-profiles.json"), + "--report", str(report)], capture_output=True, text=True) + self.assertEqual(result.returncode, 1, result.stderr) + data = json.loads(report.read_text()) + self.assertFalse(data["allAvailable"]) + self.assertEqual(data["servers"]["missing"]["errorType"], "COMMAND_NOT_FOUND") + self.assertEqual(data["servers"]["taskand"]["status"], "excluded") + + def test_private_profiles_and_no_secret_in_package(self): + profile = {**self.profile, "env": {"SECRET": "test-sentinel-do-not-export"}} + tool = {"name": "add", "inputSchema": {"type": "object"}} + _, files = catalog.package_files("fixture", profile, tool, 1) + self.assertFalse(any(b"test-sentinel-do-not-export" in raw for raw in files.values())) + self.assertEqual(self.profiles.stat().st_mode & 0o777, 0o600) + with self.assertRaisesRegex(bridge.ContractError, "PROFILE_FILE_CONFLICT"): + catalog.write_private(self.profiles, {"fixture": profile}) + link = self.root / "link" + link.symlink_to(self.root / "mcp", target_is_directory=True) + with self.assertRaisesRegex(bridge.ContractError, "SYMLINK_REJECTED"): + catalog.emit(link, {"fixture": profile}, {"fixture": [tool]}) + + def test_config_validation_and_self_reference(self): + with self.assertRaisesRegex(bridge.ContractError, "SELF_REFERENCE_EXCLUDED"): + catalog.normalize("taskand", {"command": sys.executable}) + with self.assertRaisesRegex(bridge.ContractError, "DISABLED"): + catalog.normalize("disabled", {"enabled": False}) + with self.assertRaisesRegex(bridge.ContractError, "HTTP_AUTH_UNSUPPORTED"): + catalog.normalize("remote", {"url": "https://example.invalid/mcp", "bearer_token_env_var": "SECRET"}) + with self.assertRaisesRegex(bridge.ContractError, "EXTERNAL_SCHEMA_REFERENCE"): + bridge.validate_schema({"$ref": "https://example.invalid/schema"}) + with self.assertRaisesRegex(bridge.ContractError, "INVALID_ENDPOINT"): + bridge.client({"transport": "http", "url": "http://example.invalid/mcp"}) + + +if __name__ == "__main__": + unittest.main(verbosity=2) diff --git a/tests/mcp_registry.test.mjs b/tests/mcp_registry.test.mjs new file mode 100644 index 0000000..188d3cd --- /dev/null +++ b/tests/mcp_registry.test.mjs @@ -0,0 +1,28 @@ +import { test } from 'node:test'; +import assert from 'node:assert/strict'; +import { mkdtempSync, cpSync, mkdirSync, writeFileSync, rmSync, symlinkSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import { pathToFileURL } from 'node:url'; + +test('MCP has one reserved root and preserves generated lookup', async () => { + const root = mkdtempSync(join(tmpdir(), 'taskand-mcp-registry-')); + try { + const target = join(root, 'generated/registry/core/taskand.dev/v1'); + mkdirSync(target, { recursive: true }); + cpSync(new URL('../generated/registry/core/taskand.dev/v1/store.mjs', import.meta.url), join(target, 'store.mjs')); + const store = await import(pathToFileURL(join(target, 'store.mjs'))); + mkdirSync(join(root, 'mcp/mcp-fixture'), { recursive: true }); + writeFileSync(join(root, 'mcp/mcp-fixture/registry.json'), JSON.stringify({ processes: { example: { uri: 'fixture' } } })); + assert.equal(store.parseUri('proc://taskand.dev/mcp-fixture/add/v1').dir, join(root, 'mcp/mcp-fixture/add/taskand.dev/v1')); + assert.equal(store.parseUri('proc://taskand.dev/file/ops/v1').dir, join(root, 'generated/file/ops/taskand.dev/v1')); + assert.equal(store.parseUri('proc://taskand.dev/mcp-fixture/../v1'), null); + assert.ok(store.organisms().includes('mcp-fixture')); + assert.equal(store.allEntries().find(e => e.uri === 'fixture').uri, 'fixture'); + mkdirSync(join(root, 'generated/mcp-fixture')); + assert.throws(() => store.parseUri('proc://taskand.dev/mcp-fixture/add/v1'), /MCP_NAMESPACE_COLLISION/); + symlinkSync(join(root, 'generated/file'), join(root, 'mcp/mcp-link')); + // Even dangling symlinks must be rejected before a later target appears. + assert.throws(() => store.parseUri('proc://taskand.dev/mcp-link/add/v1'), /REGISTRY_SYMLINK/); + } finally { rmSync(root, { recursive: true, force: true }); } +}); From 934db1d6876026751db24c9c3ad2cc76760136ea Mon Sep 17 00:00:00 2001 From: Tom Softreck Date: Sat, 19 Sep 2026 21:15:22 +0200 Subject: [PATCH 2/6] fix(doctor): flush complete JSON for large MCP catalogs --- generated/doctor/heal/taskand.dev/v1/bin.mjs | 5 +- .../doctor/prescribe/taskand.dev/v1/bin.mjs | 7 +- generated/doctor/registry.json | 11 +-- project/ticket-031/README.md | 15 ++++ project/ticket-031/intent.json | 83 +++++++++++++++++++ tests/doctor_diagnostics.test.mjs | 54 ++++++++++++ 6 files changed, 165 insertions(+), 10 deletions(-) create mode 100644 project/ticket-031/README.md create mode 100644 project/ticket-031/intent.json diff --git a/generated/doctor/heal/taskand.dev/v1/bin.mjs b/generated/doctor/heal/taskand.dev/v1/bin.mjs index 05c7ca3..fd6c261 100644 --- a/generated/doctor/heal/taskand.dev/v1/bin.mjs +++ b/generated/doctor/heal/taskand.dev/v1/bin.mjs @@ -2,7 +2,7 @@ // proc://taskand.dev/doctor/heal/v1 — pętla samonaprawy: diagnose → prescribe → wykonanie recept "organism" → ponowna diagnoza. // in: { run?: true, max? }. Granice: policy.healing (auto|manual|off), max napraw na przebieg, odstęp między naprawami tego samego obiektu. // Recepty "human" (Docker, sekrety, kod builtin) są tylko raportowane. -import { readFileSync } from 'node:fs'; +import { readFileSync, writeFileSync } from 'node:fs'; import { fileURLToPath } from 'node:url'; import { registry, call } from './registry-client.mjs'; @@ -96,6 +96,7 @@ function report() { } function done(out) { - process.stdout.write(JSON.stringify(out) + '\n'); + // A forced exit must not discard the tail of a large JSON response on a pipe. + writeFileSync(1, JSON.stringify(out) + '\n'); process.exit(0); } diff --git a/generated/doctor/prescribe/taskand.dev/v1/bin.mjs b/generated/doctor/prescribe/taskand.dev/v1/bin.mjs index 4bf9eab..c8362b4 100755 --- a/generated/doctor/prescribe/taskand.dev/v1/bin.mjs +++ b/generated/doctor/prescribe/taskand.dev/v1/bin.mjs @@ -1,7 +1,7 @@ #!/usr/bin/env node // proc://taskand.dev/doctor/prescribe/v1 — recepty z ustaleń diagnozy (deterministyczne reguły). // Każda recepta: executor "organism" (remedies: kroki proc:// dla doctor/heal) albo "human" (polecenie + powód). -import { readFileSync } from 'node:fs'; +import { readFileSync, writeFileSync } from 'node:fs'; import { call } from './registry-client.mjs'; let input; @@ -58,13 +58,14 @@ const RULES = { const diag = input.diagnosis || call('proc://taskand.dev/doctor/diagnose/v1', {}, 90000); if (!Array.isArray(diag.findings)) { - process.stdout.write(JSON.stringify({ ok: false, error: `Brak diagnozy: ${diag.error}` }) + '\n'); + writeFileSync(1, JSON.stringify({ ok: false, error: `Brak diagnozy: ${diag.error}` }) + '\n'); process.exit(0); } const prescriptions = diag.findings.map(f => ({ finding: f, ...(RULES[f.code]?.(f) || human('brak reguły', `Nieznany kod ${f.code}`)) })); const byOrganism = prescriptions.filter(p => p.executor === 'organism').length; -process.stdout.write(JSON.stringify({ +// Complete pipe output before process.exit; large candidate catalogs exceed pipe capacity. +writeFileSync(1, JSON.stringify({ ok: true, healthy: diag.healthy, prescriptions, diff --git a/generated/doctor/registry.json b/generated/doctor/registry.json index 0bd5dda..b3217d4 100644 --- a/generated/doctor/registry.json +++ b/generated/doctor/registry.json @@ -37,11 +37,11 @@ "proc.yaml", "registry-client.mjs" ], - "hash": "sha256:4b743fc34f579ae7e347c517631456884e93cf30802c6079fcbf2069132c6151", + "hash": "sha256:107c79c5d68d62c99b99e533d24de3db5a9a1299682b6b3ecae40ca57581148f", "status": "active", "registered": "2026-09-12T19:09:04.970Z", "capability": "Recepty naprawcze wyliczane z bieżącej diagnozy", - "refreshedAt": "2026-09-13T06:38:40.925Z" + "refreshedAt": "2026-09-19T19:13:31.640Z" }, "proc://taskand.dev/doctor/heal/v1": { "uri": "proc://taskand.dev/doctor/heal/v1", @@ -58,10 +58,11 @@ "proc.yaml", "registry-client.mjs" ], - "hash": "sha256:07a34a366b1fa736157149ec339cf6f88f800bf4d170f1b167e69bbc6837e8e4", + "hash": "sha256:97afd41978e9581c3e269802d38e4cbd91a477f04f959abe21102825d2b86f1d", "status": "active", - "registered": "2026-09-13T05:42:54.873Z" + "registered": "2026-09-13T05:42:54.873Z", + "refreshedAt": "2026-09-19T19:13:31.687Z" } }, - "updated": "2026-09-14T09:48:53.622Z" + "updated": "2026-09-19T19:13:31.687Z" } diff --git a/project/ticket-031/README.md b/project/ticket-031/README.md new file mode 100644 index 0000000..d58e9cc --- /dev/null +++ b/project/ticket-031/README.md @@ -0,0 +1,15 @@ +# Ticket 031: Complete large doctor JSON responses for MCP catalogs + +- **ID**: ticket-031 +- **Owner**: codex +- **Status**: IN_PROGRESS +- **Workflow state**: VALIDATION +- **Created**: 2026-09-19 + +SESSION_EXECUTION_AUTHORIZATION: user requests completion of local MCP integration after stopping dashboard. This bounded dependency fixes the reproduced 8 KiB pipe truncation exposed by ticket030's 422 candidates. Ticket030 source is committed as 9384699; no concurrent implementation writer. + +- [x] AC-01: Both real doctor CLIs emit full valid JSON, preserve the final record and existing plan-only semantics; full MCP catalog contracts pass. + +Canonical integration report: docs/information/mcp-uri-catalog.md, to be updated under ticket030 after this dependency is joined. This ticket owns only doctor output and its regression. + +Validation: regression failed before correction and all 14 doctor tests pass after it. Full make test passes with 422 registered MCP candidates: 4 conformance, 30 contracts, 17 negative, 9 twin, 8 web twin, 7 CLI, 11 MCP Python and 1 MCP registry. Managed governance passes. diff --git a/project/ticket-031/intent.json b/project/ticket-031/intent.json new file mode 100644 index 0000000..1291535 --- /dev/null +++ b/project/ticket-031/intent.json @@ -0,0 +1,83 @@ +{ + "schema": "new-project.intent/v3", + "ticket": "ticket-031", + "summary": "Preserve complete doctor JSON on pipes when large MCP catalogs produce hundreds of findings", + "workstream": "integration", + "classification": { + "kind": "BUG", + "priority": "P2", + "origin": "health" + }, + "allowedPaths": [ + "project/ticket-031/**", + "generated/doctor/prescribe/taskand.dev/v1/bin.mjs", + "generated/doctor/heal/taskand.dev/v1/bin.mjs", + "generated/doctor/registry.json", + "tests/doctor_diagnostics.test.mjs" + ], + "forbiddenPaths": [ + "project/ticket-*/user-*.md" + ], + "stacks": [], + "dependsOn": [ + "ticket-030" + ], + "conflictsWith": [], + "integrationTicket": null, + "delivery": { + "acceptedBaseSha": "8804583ef82342ad7870659cf79e9ad8278e2369", + "targetBranch": "main", + "outcome": "Doctor prescribe and heal emit valid, complete JSON for a 422-tool MCP catalog; regression captures pipe output and confirms last record.", + "nonGoals": [ + "Changing diagnostic findings or healing authority", + "Editing MCP bridge, dashboard, shared manifests or documentation index" + ], + "complexity": "S", + "estimatedMinutes": 20, + "budgets": { + "maxImplementationFiles": 5, + "maxAffectedComponents": 2, + "maxPublicInterfaceChanges": 0, + "maxRuntimeDependencies": 0 + }, + "architecture": { + "status": "accepted", + "decision": "Synchronous stdout writes complete the JSON before existing process exits. Exercise real CLI programs via pipes against a deterministic diagnostic fixture and run the actual full MCP inventory. This is a serialized follow-up to ticket030, not a concurrent writer.", + "components": [ + { + "name": "doctor", + "paths": [ + "generated/doctor/**" + ] + }, + { + "name": "tests", + "paths": [ + "tests/doctor_diagnostics.test.mjs" + ] + } + ], + "responsibilityChanges": false, + "interfaceChanges": [], + "dataChanges": [], + "ui": { + "impact": "none", + "states": [], + "evidence": [] + }, + "rollback": "Revert only doctor output changes; preserve imported MCP packages and existing runtime receipts." + }, + "runtimeDependencies": [], + "validation": [ + { + "criterion": "AC-01", + "commands": [ + "node --test tests/doctor_diagnostics.test.mjs", + "make test", + "./project/governance-check.sh" + ], + "evidence": "Real CLI pipe output for large diagnostic payloads; full 422-candidate catalog contract checks." + } + ] + } +} diff --git a/tests/doctor_diagnostics.test.mjs b/tests/doctor_diagnostics.test.mjs index 98085af..163c07a 100644 --- a/tests/doctor_diagnostics.test.mjs +++ b/tests/doctor_diagnostics.test.mjs @@ -6,8 +6,62 @@ import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { pathToFileURL } from 'node:url'; import { setTimeout as delay } from 'node:timers/promises'; +import { spawnSync } from 'node:child_process'; import { diagnosticBudget, now, runChecks, probeService } from '../generated/doctor/diagnose/taskand.dev/v1/probes.mjs'; +test('large diagnosis survives real prescribe and heal stdout pipes without losing the last candidate', t => { + const root = mkdtempSync(join(tmpdir(), 'taskand-doctor-large-output-')); + t.after(() => rmSync(root, { recursive: true, force: true })); + for (const name of ['prescribe', 'heal']) { + const target = join(root, `generated/doctor/${name}/taskand.dev/v1`); + mkdirSync(target, { recursive: true }); + for (const file of ['bin.mjs', 'registry-client.mjs']) { + copyFileSync(new URL(`../generated/doctor/${name}/taskand.dev/v1/${file}`, import.meta.url), join(target, file)); + } + } + const diagnosis = { ok: true, healthy: false, summary: 'Large MCP catalog', + findings: Array.from({ length: 1024 }, (_, n) => ({ code: 'CANDIDATE_PENDING', origin: 'mcp', + subject: `proc://taskand.dev/mcp-fixture/tool-${n}/v1`, detail: 'Narzędzie oczekuje na przegląd. '.repeat(8) })) }; + writeFileSync(join(root, 'diagnosis.json'), JSON.stringify(diagnosis)); + const broker = join(root, 'generated/registry/core/taskand.dev/v1'); + mkdirSync(broker, { recursive: true }); + writeFileSync(join(broker, 'bin.mjs'), ` + import { readFileSync, writeFileSync } from 'node:fs'; + import { spawnSync } from 'node:child_process'; + const input = JSON.parse(readFileSync(0, 'utf8')); + let result; + if (input.action === 'policy') result = {ok:true, value:'manual'}; + else if (input.action === 'call' && input.uri === 'proc://taskand.dev/doctor/diagnose/v1') + result = JSON.parse(readFileSync('diagnosis.json', 'utf8')); + else if (input.action === 'call' && input.uri === 'proc://taskand.dev/doctor/prescribe/v1') { + const child = spawnSync(process.execPath, ['generated/doctor/prescribe/taskand.dev/v1/bin.mjs'], + {input:JSON.stringify(input.input), encoding:'utf8', maxBuffer:8*1024*1024, timeout:5000}); + if (child.status !== 0) throw new Error('prescribe failed'); + result = JSON.parse(child.stdout); + } else { writeFileSync('unexpected-effect', JSON.stringify(input)); throw new Error('unexpected dispatch'); } + writeFileSync(1, JSON.stringify(result)+'\\n'); + `); + for (const name of ['prescribe', 'heal']) { + const child = spawnSync(process.execPath, [`generated/doctor/${name}/taskand.dev/v1/bin.mjs`], { + cwd: root, input: JSON.stringify(name === 'prescribe' ? { diagnosis } : { run: true }), + encoding: 'utf8', maxBuffer: 8 * 1024 * 1024, timeout: 10000, + env: { PATH: process.env.PATH, HOME: process.env.HOME } + }); + assert.equal(child.status, 0, child.stderr); + const result = JSON.parse(child.stdout); + assert.equal(result.ok, true); + assert.ok(Buffer.byteLength(child.stdout) > 131072); + const entries = name === 'prescribe' ? result.prescriptions.map(p => p.finding) : result.human; + assert.equal(entries.length, diagnosis.findings.length); + assert.equal(entries.at(-1).subject, diagnosis.findings.at(-1).subject); + if (name === 'heal') { + assert.equal(result.mode, 'plan'); + assert.deepEqual(result.executed, []); + } + } + assert.equal(existsSync(join(root, 'unexpected-effect')), false); +}); + test('automatic CPU budget is bounded; explicit valid budgets are retained', () => { assert.equal(diagnosticBudget({}, 1).concurrency, 1); assert.equal(diagnosticBudget({}, 128).concurrency, 4); From 05642216ea442d90e10e828d2bd6cfe9dd9bcd39 Mon Sep 17 00:00:00 2001 From: Tom Softreck Date: Sat, 19 Sep 2026 21:24:26 +0200 Subject: [PATCH 3/6] docs(mcp): record full catalog validation and local deployment --- docs/information/mcp-uri-catalog.md | 52 ++++++++++++++++++++--------- project/ticket-030/README.md | 6 ++-- 2 files changed, 40 insertions(+), 18 deletions(-) diff --git a/docs/information/mcp-uri-catalog.md b/docs/information/mcp-uri-catalog.md index f496898..eb38c37 100644 --- a/docs/information/mcp-uri-catalog.md +++ b/docs/information/mcp-uri-catalog.md @@ -3,14 +3,14 @@ "schema": "wellmanifest.docs/document/v1", "id": "mcp-uri-catalog", "kind": "information", - "version": 1, + "version": 2, "title": "Lokalne MCP jako natywne procesy URI", - "status": "proposed", + "status": "implemented", "owner": "semcod/taskand-glm53", "created": "2026-09-19", "updated": "2026-09-19", "review_after": "2026-09-26", - "source_revision": "8804583ef82342ad7870659cf79e9ad8278e2369", + "source_revision": "8fa9d47b389a9c0ab4b7a12601fad1b8ac23b95b", "affected_repositories": [ "semcod/taskand-glm53" ], @@ -198,12 +198,18 @@ kontrolowanej aktualizacji. Pin profilu nie jest podpisem ani izolacją systemow ## Walidacja i uruchomiony pilot - Dotychczasowy adapter Taskand MCP: 9/9 testów; kontroler ticket-029: 11/11. -- Pierwszy `make test` przeszedł: konformacja 4/4, kontrakty 30/30, negatywne 17/17, - twin 9, web twin 8, CLI 7, MCP Python 10 i rejestr MCP Node 1. - Końcowy `test-mcp` po rozszerzeniu CLI: Python 11 i Node 1, wszystkie przeszły. - Powtórny pełny test po rejestracji 422 kandydatów ujawnił 2 błędy kontraktów - doctor/prescribe i doctor/heal (28/30): duży JSON jest ucinany przez natychmiastowe - zakończenie procesu. Wymagana osobna naprawa ograniczona do tej regresji. +- Końcowy `make test` z 422 zarejestrowanymi kandydatami: konformacja 4/4, + kontrakty 30/30, negatywne 17/17, twin 9, web twin 8, CLI 7, + MCP Python 11 i rejestr MCP Node 1; wszystkie przeszły. +- `node --test tests/doctor_diagnostics.test.mjs`: 14/14. Nowy test najpierw + odtworzył ucięcie JSON na potoku; po poprawce oba rzeczywiste programy + zachowują wszystkie 1024 ustalenia, ostatni rekord i tryb planowania. +- Integracja jest lokalnym commitem `9384699affb303d1ad31831eeea7181116e34680`. + Zależność ticket-031, commit `8fa9d47b389a9c0ab4b7a12601fad1b8ac23b95b`, + zastępuje asynchroniczny zapis stdout synchronicznym `writeFileSync(1, ...)` + przed istniejącym `process.exit`. Poprawka obejmuje doctor/prescribe i + doctor/heal oraz odświeża wyłącznie ich bindingHash. Dołączono ją lokalnie + do gałęzi ticket-030 przez fast-forward. - `./project/governance-check.sh`: zero błędów i ostrzeżeń. - Testy MCP uruchamiają prawdziwe serwery SDK stdio/HTTP i rzeczywisty rejestr; sprawdzają schematy, zmianę profilu/kontraktu, kandydaturę/dopuszczenie, @@ -214,8 +220,13 @@ kontrolowanej aktualizacji. Pin profilu nie jest podpisem ani izolacją systemow - Import 45 serwerów zakończył się bez niedostępnych integracji i zarejestrował 422 kandydatów w roboczym `mcp/`. - Pilot `http://127.0.0.1:8084`, jednostka użytkownika - `taskand-mcp030-pilot.service`, ma 7 dopuszczonych narzędzi i 415 kandydatów. + `taskand-mcp030-release.service`, ma 7 dopuszczonych narzędzi i 415 kandydatów. Profile, token i surowe odpowiedzi przechowuje prywatny magazyn hosta. + Po wdrożeniu powtórzono wszystkie siedem wywołań i test adaptera. + Health zgłasza `LOCAL_CODE_MATCH`; sprawdzono odciski 447 plików źródłowych + manifestu wydania. Doctor/prescribe zwraca pełny JSON 230708 bajtów, + a doctor/heal 217052 bajtów; oba zawierają 416 rekordów. Heal pozostaje + w trybie planowania bez wykonanych napraw. - Rzeczywiste wywołania URI: odczyt pliku filesystem, odczyt memory, status git, fetch publicznego README, konwersja lokalnego HTML MarkItDown, tillm health i analiza dwóch manifestów diff-dsl. Odpowiedzi sprawdzono @@ -229,11 +240,13 @@ Powtarzalne dowody są w testach repozytorium. Lokalne obserwacje wdrożenia: - `final-import.json`: SHA-256 `f75187b4f538fcff8e529513e0242f03274330286b6aea61fc31438cac3d4b51`. - `gateway-canaries-verified.json`: SHA-256 `d4ab73a6998e2691636e36d72cb848a9434aae605c1218ffd3223e90ece28b93`. +- `deployed-verification.json`: SHA-256 `75cd58a9263facf54284189e44fc9784ccfb388fe1b0272b7208e73f665ff915`. + Magazyn operacyjny: `~/.local/state/taskand/mcp-local-import-20260919/`; instalacje i pilot: `~/.local/share/taskand/mcp-import/`. Te ścieżki pomagają operatorowi lokalnemu; nie zastępują dostępnego w repo kodu -ani niezależnych dowodów publikacji. Rewizja w metadanych wskazuje bazę zmiany; -implementację identyfikuje commit zawierający ten dokument. +ani niezależnych dowodów publikacji. Rewizja w metadanych wskazuje wdrożony commit implementacji. Późniejszy +commit odbioru aktualizuje wyłącznie ten dokument i kryteria ticket-030. ## Ograniczenia odbioru @@ -250,10 +263,14 @@ systemowym. Schemat MCP ani adnotacja readOnly nie przyznają uprawnień. Utrata odpowiedzi może pozostawić efekt i wymaga uzgodnienia stanu przed kolejnym wywołaniem. Hostowe profile mogą zawierać sekrety i nie trafiają do Git. -Integracja nie jest scalona z main. Dotychczasowy PR #38 czeka na chronione +Integracja jest lokalnie wdrożona, lecz nie jest scalona z main ani wypchnięta. +Zachowany PR #38 ma HEAD `803771c0877ed7a2e21bdc049602efd4ad9a1fd8` i czeka na chronione lokalne CI: brak `/run/onedev-docker-gate/taskand-browser.socket`, zależność [onedev-agent #304](https://github.com/subactor/onedev-agent/issues/304). -Nowa implementacja nie obchodzi tej kontroli. Audyt dokumentacji wykazuje +Nowa implementacja nie obchodzi tej kontroli. Po ponownej decyzji użytkownika +zatrzymano dokładną sesję dashboardu i zwolniono jej lease przez CAS. Jej czysty +checkout, w tym ignorowane dane, zachowano w prywatnym archiwum przed zwolnieniem +katalogu; gałąź, PR38 i wdrożenie na 8082 pozostały zachowane. Audyt dokumentacji wykazuje cztery istniejące wcześniej problemy metadanych/placeholderów w innych dokumentach; zapisano je w kolejce PLF-010. Nowy dokument nie dodaje takiego problemu. @@ -262,10 +279,13 @@ zapisano je w kolejce PLF-010. Nowy dokument nie dodaje takiego problemu. Dodatkowe narzędzia dopuszczać indywidualnie po ocenie konkretnego działania; import sam zachowuje stan candidate. Zmiany konfiguracji importować jako nowe -wersje z osobnym snapshotem profili. Pełna publikacja wymaga chronionego +wersje z osobnym snapshotem profili. Wersja uruchomiona na 8084 pochodzi z `8fa9d47`; manifest plików wiąże ją +z commitem. Poprzedni dziennik żądań skopiowano po zatrzymaniu starej jednostki +pilota, zachowując również poprzedni katalog jako punkt odtworzenia. +Pełna publikacja wymaga chronionego przeglądu i działającego lokalnego wykonawcy CI. -Rollback pilota: `systemctl --user stop taskand-mcp030-pilot.service`. +Rollback pilota: `systemctl --user stop taskand-mcp030-release.service`. Zachować katalog pilota, profile i receipts do uzgodnienia efektów; istniejące instancje 8077/8082 pozostają niezależne. Jednostka jest lokalnym pilotem sesji, nie deklaracją wdrożenia produkcyjnego ani konfiguracji startu po restarcie hosta. diff --git a/project/ticket-030/README.md b/project/ticket-030/README.md index f0ded3c..b26b073 100644 --- a/project/ticket-030/README.md +++ b/project/ticket-030/README.md @@ -13,11 +13,13 @@ Prior ticket-029 source, deployed pilot and PR38 remain intact, awaiting its pro ## Acceptance - [x] AC-01: Inventory every local registration; build deterministic, versioned MCP process packages with metadata/schema/source provenance and private host configuration. -- [ ] AC-02: Native registry discovers and invokes admitted MCP package URI; real stdio/HTTP and invalid-input regressions pass. +- [x] AC-02: Native registry discovers and invokes admitted MCP package URI; real stdio/HTTP and invalid-input regressions pass. - [x] AC-03: Verify local deployments, document unavailable servers and credential prerequisites, preserve unknown outcomes and existing services. Canonical result: docs/information/mcp-uri-catalog.md. Local validation: 45 servers / 422 tool packages, all discovered and registered. Seven explicitly admitted native gateway canaries passed; tillm health reports optional client dependencies unavailable. Existing adapter and controller tests pass. Full make test and managed governance pass. Published baseline documents have pre-existing docs findings, tracked separately from the new document. Protected publication remains separate from local delivery. -Validation follow-up: full inventory exposes truncated doctor/prescribe and doctor/heal JSON (28/30 contracts). MCP suite passes; local integration commit is not final acceptance. Preserve this ticket and allocate a bounded follow-up for stdout flushing before claiming full completion. +Resolved validation follow-up: full inventory initially exposed truncated doctor/prescribe and doctor/heal JSON (28/30 contracts). Allocated ticket-031 fixed pipe output and added a regression; final acceptance below supersedes that failed run. + +Final continuation 2026-09-19: user prioritized MCP after stopping dashboard. Exact dashboard CLI was stopped and observed gone; lease cancelled by CAS. Clean dashboard checkout privately archived and released, preserving pushed branch/PR38 and running 8082. Restored source committed as 9384699. Ticket031 doctor pipe-output correction committed as 8fa9d47 and fast-forwarded into this branch. Full make test passes with 422 candidates; all 14 doctor regressions pass. Local immutable release on 8084 uses commit 8fa9d47 and preserves request ledger. No push, new PR or merge of this integration. Shared-path integration with PR38 remains a separate protected publication step. From f3f09c67467cebb9786569ab88db91868c698834 Mon Sep 17 00:00:00 2001 From: Tom Softreck Date: Sat, 19 Sep 2026 21:37:51 +0200 Subject: [PATCH 4/6] fix(gateway): serve pilot panel and keep API requests on its origin --- gateway/__init__.py | 6 ++- project/ticket-032/README.md | 15 ++++++ project/ticket-032/intent.json | 95 +++++++++++++++++++++++++++++++++ tests/runtime_readiness_test.py | 32 +++++++++++ 4 files changed, 147 insertions(+), 1 deletion(-) create mode 100644 project/ticket-032/README.md create mode 100644 project/ticket-032/intent.json diff --git a/gateway/__init__.py b/gateway/__init__.py index d042e45..1d5e185 100644 --- a/gateway/__init__.py +++ b/gateway/__init__.py @@ -46,7 +46,11 @@ def do_OPTIONS(self) -> None: def do_GET(self) -> None: if urlsplit(self.path).path in ('/', '/index.html'): - page = (Path(__file__).resolve().parent.parent / 'index.html').read_bytes() + try: + page = (Path(__file__).resolve().parent.parent / 'index.html').read_bytes() + except OSError: + self._send(503, {'ok': False, 'error': 'PANEL_UNAVAILABLE'}) + return self.send_response(200) self.send_header('Content-Type', 'text/html; charset=utf-8') self.send_header('Cache-Control', 'no-store') diff --git a/project/ticket-032/README.md b/project/ticket-032/README.md new file mode 100644 index 0000000..a1b6b7e --- /dev/null +++ b/project/ticket-032/README.md @@ -0,0 +1,15 @@ +# Ticket 032: Serve MCP pilot panel at gateway root + +- **ID**: ticket-032 +- **Owner**: codex +- **Status**: IN_PROGRESS +- **Workflow state**: PUBLICATION +- **Created**: 2026-09-19 + +SESSION_EXECUTION_AUTHORIZATION: user reports Endpoint not found: GET / at the delivered MCP pilot. PLF-011 records this bounded correction to ticket030 local deployment. No parallel writer; previous lease explicitly released. + +- [ ] AC-01: Root and index aliases return the real panel; browser requests stay on this pilot; unknown paths and unauthenticated API access remain rejected; the corrected 8084 release and native MCP round trip pass. + +Minimal shared behavior is reconciled with preserved PR38 source; its dashboard and 8082 runtime are unchanged. + +Validation before deployment: real HTTP regression failed with 404 before the fix, then all 14 readiness tests and 7 release identity tests passed. Real Chrome preview returned the bundled panel, sent mesh requests only to the selected pilot, preserved 401 without authorization, retained 8090 -> 8077 compatibility and had zero JavaScript errors. Managed governance passed. The deployment acceptance is recorded externally after this exact source commit. diff --git a/project/ticket-032/intent.json b/project/ticket-032/intent.json new file mode 100644 index 0000000..7f881f6 --- /dev/null +++ b/project/ticket-032/intent.json @@ -0,0 +1,95 @@ +{ + "schema": "new-project.intent/v3", + "ticket": "ticket-032", + "summary": "Serve the existing panel from the MCP gateway root and keep browser API calls on the selected pilot", + "workstream": "integration", + "classification": { + "kind": "BUG", + "priority": "P2", + "origin": "requested" + }, + "allowedPaths": [ + "project/ticket-032/**", + "gateway/__init__.py", + "index.html", + "tests/runtime_readiness_test.py" + ], + "forbiddenPaths": [ + "project/ticket-*/user-*.md" + ], + "stacks": [], + "dependsOn": [ + "ticket-030" + ], + "conflictsWith": [], + "integrationTicket": null, + "delivery": { + "acceptedBaseSha": "8804583ef82342ad7870659cf79e9ad8278e2369", + "targetBranch": "main", + "outcome": "Opening the delivered 8084 URL returns the existing Taskand panel; browser API requests target that origin and 8090 compatibility remains.", + "nonGoals": [ + "Dashboard redesign or merging PR38", + "New tool admission or changes to API authorization", + "Changing existing 8077 or 8082 deployments" + ], + "complexity": "M", + "estimatedMinutes": 35, + "budgets": { + "maxImplementationFiles": 3, + "maxAffectedComponents": 3, + "maxPublicInterfaceChanges": 1, + "maxRuntimeDependencies": 0 + }, + "architecture": { + "status": "accepted", + "decision": "Serve only the fixed bundled index.html path for GET / and /index.html. Use the current browser origin except the established standalone 8090 frontend. Reuse minimal root-serving behavior from preserved PR38 with bounded error handling and query-safe logging.", + "components": [ + { + "name": "gateway", + "paths": [ + "gateway/__init__.py" + ] + }, + { + "name": "panel", + "paths": [ + "index.html" + ] + }, + { + "name": "tests", + "paths": [ + "tests/runtime_readiness_test.py" + ] + } + ], + "responsibilityChanges": false, + "interfaceChanges": [ + "GET / and /index.html serve bundled panel" + ], + "dataChanges": [], + "ui": { + "impact": "single-state", + "states": [ + "success" + ], + "evidence": [ + "real HTTP root and browser API-origin smoke checks" + ] + }, + "rollback": "Restart the previous immutable 8084 release, preserving request history." + }, + "runtimeDependencies": [], + "validation": [ + { + "criterion": "AC-01", + "commands": [ + "python tests/runtime_readiness_test.py", + "python tests/health_identity_test.py", + "./project/governance-check.sh" + ], + "evidence": "Actual gateway HTTP root aliases, API isolation and unchanged authorization; browser origin checks and deployed MCP canary." + } + ] + } +} diff --git a/tests/runtime_readiness_test.py b/tests/runtime_readiness_test.py index d5b91eb..93219b2 100644 --- a/tests/runtime_readiness_test.py +++ b/tests/runtime_readiness_test.py @@ -1,5 +1,6 @@ import contextlib import hashlib +import http.client from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer import json import multiprocessing @@ -15,6 +16,7 @@ ROOT = Path(__file__).resolve().parents[1] sys.path.insert(0, str(ROOT)) from app import runtime_readiness as readiness # noqa: E402 +from gateway import GatewayHTTPHandler # noqa: E402 PANEL = b"readiness fixture" COMMIT = "a" * 40 @@ -250,5 +252,35 @@ def test_stream_never_substitutes_expected_sha_for_missing_or_different_health(s self.assertFalse(lines[-1]['preflightPassed']) +class GatewayPanelTests(unittest.TestCase): + def test_real_gateway_serves_panel_without_exposing_other_files_or_api(self): + service = ThreadingHTTPServer(('127.0.0.1', 0), GatewayHTTPHandler) + thread = threading.Thread(target=service.serve_forever, kwargs={'poll_interval': 0.02}, daemon=True) + thread.start() + client = http.client.HTTPConnection('127.0.0.1', service.server_port, timeout=5) + try: + for path in ('/', '/index.html', '/?view=status', '/index.html?view=status'): + client.request('GET', path) + response = client.getresponse() + self.assertEqual(response.status, 200, path) + self.assertEqual(response.getheader('Content-Type'), 'text/html; charset=utf-8') + self.assertEqual(response.getheader('Referrer-Policy'), 'no-referrer') + self.assertEqual(response.read(), (ROOT / 'index.html').read_bytes()) + for path in ('/grants.yaml', '/../grants.yaml', '/missing', '/api/context'): + client.request('GET', path) + response = client.getresponse() + self.assertEqual(response.status, 401 if path == '/api/context' else 404, path) + self.assertFalse(json.loads(response.read())['ok']) + client.request('POST', '/api/proc/call', body='{}', headers={'Content-Type': 'application/json'}) + response = client.getresponse() + self.assertEqual(response.status, 401) + self.assertFalse(json.loads(response.read())['ok']) + finally: + client.close() + service.shutdown() + service.server_close() + thread.join(1) + + if __name__ == "__main__": unittest.main() From 8c4f108dd2ee44fa1fb42d937482bea67a395f7b Mon Sep 17 00:00:00 2001 From: Tom Softreck Date: Sat, 19 Sep 2026 22:23:04 +0200 Subject: [PATCH 5/6] feat(dashboard): connect MCP tools to native URI catalog and execution --- gateway/handlers/chat.py | 25 +++++ gateway/handlers/context.py | 49 ++++++++- gateway/router.py | 6 +- index.html | 186 +++++++++++++++++++++++--------- project/ticket-033/README.md | 23 ++++ project/ticket-033/intent.json | 107 ++++++++++++++++++ tests/mesh_test.py | 113 +++++++++++++++++++ tests/runtime_readiness_test.py | 57 +++++++++- 8 files changed, 511 insertions(+), 55 deletions(-) create mode 100644 project/ticket-033/README.md create mode 100644 project/ticket-033/intent.json diff --git a/gateway/handlers/chat.py b/gateway/handlers/chat.py index f358989..67d11d8 100644 --- a/gateway/handlers/chat.py +++ b/gateway/handlers/chat.py @@ -3,6 +3,31 @@ from gateway.utils import call_process, status_for DEV_CHAT = "proc://taskand.dev/dev/chat/v1" +LLM_URI = 'proc://taskand.dev/dev/llm/v1' + + +def handle_conversation(handler, body): + """A conversation uses a fixed LLM process; it never dispatches tools.""" + if not require_grant(handler, LLM_URI, 'call'): + return + messages = body.get('messages') + if (not isinstance(messages, list) or not 1 <= len(messages) <= 24 + or any(not isinstance(m, dict) or set(m) != {'role', 'content'} + or m['role'] not in {'user', 'assistant'} + or not isinstance(m['content'], str) or not m['content'].strip() + or len(m['content']) > 12000 for m in messages) + or messages[-1]['role'] != 'user' + or sum(len(m['content']) for m in messages) > 48000): + handler._send(400, {'ok': False, 'error': 'CONVERSATION_MESSAGES_INVALID'}) + return + result = call_process(LLM_URI, {'messages': [{'role': 'system', 'content': + 'Odpowiadaj po polsku. To rozmowa i przygotowanie zadań. Nie masz narzędzi ' + 'w tym trybie. Nie twierdź, że wykonałeś operacje ani odczytałeś aktualny stan usług.'}, + *messages], 'max_tokens': 2500, 'reasoning_effort': 'low'}, timeout=100) + if not isinstance(result, dict) or result.get('ok') is not True or not isinstance(result.get('content'), str) or not result['content'].strip(): + handler._send(503, {'ok': False, 'error': 'CONVERSATION_MODEL_UNAVAILABLE'}) + return + handler._send(200, {'ok': True, 'reply': result['content'], 'mode': 'conversation', 'toolsExecuted': False}) def handle_chat(request_handler, body: dict) -> None: diff --git a/gateway/handlers/context.py b/gateway/handlers/context.py index d06798b..56994b2 100644 --- a/gateway/handlers/context.py +++ b/gateway/handlers/context.py @@ -1,10 +1,55 @@ """Context commands and read-only owner-filtered projections. No impersonation.""" from urllib.parse import parse_qs, urlsplit -from gateway.auth import check_auth, check_grant +from gateway.auth import check_auth, check_grant, require_grant from gateway.context import ACTIVE, ContextError, default_store -from gateway.utils import call_process +from gateway.utils import call_process, registry, LLM_KEY PLANNER = 'proc://taskand.dev/planner/plan/v1' +MCP_CATALOG = 'proc://taskand.dev/mcp/catalog/v1' + + +def handle_mcp_catalog(handler, body): + """Authenticated metadata projection, with a read-only dashboard capability. + + This capability grants no registry/core call, admission or tool execution. + Host profiles, commands, environment and credential references stay private. + """ + user = require_grant(handler, MCP_CATALOG, 'read') + if not user: + return + source = registry('list', {}, timeout=30) + entries = source.get('processes') if isinstance(source, dict) else None + if not isinstance(source, dict) or not source.get('ok') or not isinstance(entries, list) or len(entries) > 5000: + handler._send(503, {'ok': False, 'error': 'MCP_CATALOG_UNAVAILABLE'}) + return + tools = [] + for entry in entries: + if not isinstance(entry, dict): + handler._send(503, {'ok': False, 'error': 'MCP_CATALOG_INVALID'}) + return + if entry.get('origin') != 'mcp': + continue + mcp = entry.get('mcp', {}) + uri, status = entry.get('uri'), entry.get('status') + if (not isinstance(mcp, dict) or not all(isinstance(mcp.get(k), str) and mcp[k] for k in ('server', 'tool')) + or not isinstance(uri, str) or not uri.startswith('proc://taskand.dev/mcp-') + or status not in {'candidate', 'active', 'deprecated'} + or not isinstance(entry.get('inputSchema'), dict)): + handler._send(503, {'ok': False, 'error': 'MCP_CATALOG_INVALID'}) + return + tools.append({'uri': uri, 'server': mcp['server'], 'name': mcp['tool'], + 'description': entry.get('desc', ''), 'status': status, + 'inputSchema': entry['inputSchema'], + **({'outputSchema': entry['outputSchema']} if isinstance(entry.get('outputSchema'), dict) else {}), + 'canCall': status == 'active' and check_grant(user, uri, 'call'), + 'canApprove': status != 'active' and check_grant(user, uri, 'admin')}) + tools.sort(key=lambda t: (t['server'], t['name'], t['uri'])) + handler._send(200, {'ok': True, 'tools': tools, + 'counts': {'servers': len({t['server'] for t in tools}), 'tools': len(tools), + 'active': sum(t['status'] == 'active' for t in tools), + 'candidate': sum(t['status'] == 'candidate' for t in tools)}, + 'conversation': {'allowed': check_grant(user, 'proc://taskand.dev/dev/llm/v1', 'call'), + 'configured': bool(LLM_KEY)}}) def identity(handler): diff --git a/gateway/router.py b/gateway/router.py index 75ce555..373643b 100644 --- a/gateway/router.py +++ b/gateway/router.py @@ -2,17 +2,19 @@ from gateway.handlers.health import handle_healthz from gateway.handlers.federation import handle_federation, handle_registry, handle_well_known_catalog from gateway.handlers.proc import handle_proc_call -from gateway.handlers.chat import handle_chat +from gateway.handlers.chat import handle_chat, handle_conversation from gateway.handlers.doctor import handle_doctor from gateway.handlers.planner import handle_planner from gateway.handlers.orchestrator import handle_orchestrator -from gateway.handlers.context import handle_context, handle_state +from gateway.handlers.context import handle_context, handle_state, handle_mcp_catalog from gateway.handlers.mesh import handle_mesh from gateway.handlers.observers import handle_pilot from gateway.handlers.monag_handler import handle_monag_status, handle_monag_resume, handle_notify from urllib.parse import urlsplit ROUTES: Dict[Tuple[str, str], Callable] = { + ('GET', '/api/mcp/catalog'): handle_mcp_catalog, + ('POST', '/api/conversation'): handle_conversation, ('GET', '/api/mesh/state'): handle_mesh, ('POST', '/api/observers/pilot'): handle_pilot, ('GET', '/api/context'): handle_context, diff --git a/index.html b/index.html index f3c4c2c..d070070 100644 --- a/index.html +++ b/index.html @@ -18,6 +18,8 @@ @media(max-width:760px){.visuals{grid-template-columns:1fr}} [hidden]{display:none!important}.topbar{position:sticky;top:0;z-index:10;background:#101820f5;padding:10px 0;border-bottom:1px solid #385064}.topbar nav{display:flex;flex-wrap:wrap;gap:8px}.topbar a{padding:10px 14px;text-decoration:none;border-radius:6px}.topbar a[aria-current=page]{background:#245b57;color:white}.access{margin:12px 0}.access input{max-width:420px}.chat-message{padding:12px 16px;margin:12px 0;border-radius:10px;background:#182633;white-space:pre-wrap;overflow-wrap:anywhere}.chat-message[data-role=user]{border-left:3px solid #91d6ce}.chat-message[data-role=assistant]{border-left:3px solid #a1cce9}#conversationLog{max-height:55vh;overflow:auto;min-height:140px}.chat-actions{display:flex;gap:8px;flex-wrap:wrap} + +.mcp-summary{display:flex;align-items:center;justify-content:space-between;gap:16px;padding:12px 16px;border:1px solid #385064;border-radius:8px}.mcp-filters{display:grid;grid-template-columns:1fr 2fr 1fr;gap:16px}.mcp-workspace{display:grid;grid-template-columns:minmax(0,1fr) minmax(0,1.4fr);gap:24px}#mcpTool{min-height:300px;max-width:100%}#mcpFields small{display:block}#mcpSchema{max-height:300px}#mcpDescription{overflow-wrap:anywhere}h3{font-size:1rem}#mcpHistory button{max-width:100%;overflow-wrap:anywhere;font-size:.85rem}@media(max-width:760px){.mcp-filters,.mcp-workspace{grid-template-columns:1fr}.mcp-summary{align-items:flex-start;flex-direction:column}}

taskand. kontekst · bliźniaki · live