diff --git a/.github/scripts/README.md b/.github/scripts/README.md new file mode 100644 index 0000000..722e8d1 --- /dev/null +++ b/.github/scripts/README.md @@ -0,0 +1,129 @@ +# Dependency update tooling + +Weekly automation that refreshes every pinned dependency in this repository — +container image tags, `uv.lock` files, and GitHub Action SHAs — behind a +supply-chain cooldown and an upstream-provenance check. + +Driven by [`.github/workflows/update.yml`](../workflows/update.yml) every Tuesday +at 03:16 UTC. Run it yourself the same way CI does: + +```bash +bash .github/scripts/update-pins.sh +``` + +It needs `uv` and a GitHub token; without `GITHUB_TOKEN` set it falls back to +`gh auth token`, and without either it runs unauthenticated into GitHub's +60-requests-per-hour limit almost immediately. + +## What it guarantees + +**A 7-day cooldown on every adopted version.** Nothing published in the last +week is adopted, so a compromised upstream release has time to be noticed by +someone else first ([context](https://github.com/aquasecurity/trivy/discussions/10425)). +Candidates are walked newest-first and the first one past the window is taken — +a 3-day-old release is skipped while an 8-day-old one is still picked up, rather +than the whole pin stalling. For Python dependencies the cooldown is declarative +instead: `exclude-newer = "7 days"` in each project's `[tool.uv]`, which the +resolver enforces for `uv lock --upgrade` and for a reader's `uv sync` alike. + +**Provenance for every GitHub-sourced version.** The tag must exist as a ref in +the repository it claims to come from, and must resolve (through an annotated +tag, if there is one) to a commit. Git refs are repo-scoped — a fork's tags never +appear in the parent's refs — which is what the +[imposter-commit attack](https://github.com/aquasecurity/trivy/security/advisories/GHSA-69fq-xp46-6x23) +lacks, since it references a bare SHA with no ref behind it. + +**Hash-pinned Actions, verified twice.** `pinact run -u` re-pins to the newest +release past its own 7-day cooldown; `pinact run --check --verify-comment` +confirms each `# vX.Y.Z` comment still names the pinned SHA; and +`verify_action_pins.py` confirms that SHA is reachable from a branch upstream, +which is the check pinact has no equivalent for. That last one also fails on any +`uses:` that is not SHA-pinned at all, so it doubles as the gate a per-PR +`zizmor unpinned-uses` rule would otherwise provide. + +## Layout + +| Path | Purpose | +| --- | --- | +| `update-pins.sh` | The pin list. One commented block per pin — start here. | +| `update_file.py` | Resolve a version, gate it, rewrite it into files. | +| `verify_action_pins.py` | Confirm pinned Action commits exist upstream. | +| `check_doc_drift.py` | Report docs quoting a superseded version. | +| `supply_chain/` | Cooldown, GitHub verification, Docker Hub tag discovery. | + +`supply_chain/` is a trimmed vendoring of `zenable_monorepo` from +`Zenable-io/next-gen-governance`, which is private while this repository is +public. The security-relevant logic is kept behaviourally close to upstream so a +fix there ports as a readable diff. **If a third repository needs this, extract +these modules into a package all three consume rather than vendoring again.** + +## Adding a pin + +Append a block to `update-pins.sh`. Both regexes are required — there is no +inferred default, because every pin here is a bespoke line in a compose file or +Dockerfile and a guessed pattern is how an update silently becomes a no-op. + +```bash +"${UPDATE_FILE[@]}" \ + --file "${GIT_ROOT}/labs/example/compose.yaml" \ + --source-type github-release \ + --github-owner someone --github-repo something \ + --pin-level patch \ + --no-downgrade \ + --search-pattern 'ghcr\.io/someone/something:v[0-9]+\.[0-9]+\.[0-9]+' \ + --replacement-pattern 'ghcr.io/someone/something:{version}' +``` + +Prefer `--source-type github-release` even when the image lives on quay.io or +ghcr.io, as both Keycloak and agentgateway do here: GitHub releases carry a real +publish date and a verifiable tag, and those registries offer neither. + +Use `--source-type docker-hub` when there is no GitHub release to read, and pass +`--version-line` whenever a major bump would mean a *different artifact* rather +than a newer one — `jaegertracing/all-in-one` is held to `1` because Jaeger v2 +is a different image with a different CLI, and an unrestricted pin would +"update" that lab into something that does not start. + +## Known gaps + +These are deliberate. Each is a decision, not an oversight. + +- **`labs/ema-mcp/run.sh` is digest-pinned, not automated.** + `ceposta/keycloak:id-jag` is a mutable tag on a third party's personal Docker + Hub account, with no version in it and no release feed behind it, so there is + nothing for a cooldown to measure or an update to move it to. It is pinned by + digest so the bytes are at least fixed; refreshing it is a manual decision + about whether the new contents are trustworthy, and the command to resolve a + new digest is in the comment above the pin. +- **`FROM python:3.13-slim` is not automated.** A floating tag: rebuilt whenever + its base OS changes, so the labs already get those patches on every build, and + its `last_updated` is perpetually a day old — which makes a release cooldown + structurally unmeasurable. What the pin actually controls is 3.13 versus 3.14, + a compatibility decision that belongs in a human PR that re-runs the labs. +- **Recorded output is reported, never rewritten.** READMEs and `evidence/` hold + captured terminal transcripts. Regex-editing one to name a version nobody ran + would be fabricating evidence, and `docker compose ps` column alignment would + not survive it. `check_doc_drift.py` annotates them instead, and names which of + the two homes applies — see below. It matches two shapes: a full `repo:tag` + reference, and a bare product name plus version (`kc.sh --version` prints + `Keycloak 26.7.2`, which has no registry path to match on). The second only + applies to plain-semver pins, because a templated tag like python's + `3.13-slim` has no clean version to compare against and the labs legitimately + print a fuller `Python 3.13.12`. +- **`labs/*/README.md` is not editable here.** Each one is generated from that + lab's `.mdx` in `Zenable-io/next-gen-governance` + (`services/ui_frontend/src/lib/labs/content/labs/`) and carries a "Do not edit + by hand" banner. A stale transcript in a README is fixed *there*, then + re-exported with + `node services/ui_frontend/scripts/export-lab-readme.js --all `. + An edit committed here is silently reverted by the next export. `evidence/`, + the rig code, and the compose/Dockerfile pins the automation touches are all + owned by this repository, which is why they are safe to rewrite. +- **Nothing here runs the labs.** These PRs are not proof a bump works. That is + `next-gen-governance`'s `task e2e`. +- **Branch reachability is advisory for tag-sourced pins**, and a hard failure + only for Action SHAs. Release tooling routinely tags a commit that never lands + on a branch — Keycloak's `Set version to 26.7.2` is tagged and then superseded + on `release/26.7` — so failing on it would block correctly-published releases. + The tag-ref check above is what carries the guarantee there; for an Action pin + there is no ref to lean on, so reachability stays mandatory. diff --git a/.github/scripts/check_doc_drift.py b/.github/scripts/check_doc_drift.py new file mode 100644 index 0000000..bffc6fe --- /dev/null +++ b/.github/scripts/check_doc_drift.py @@ -0,0 +1,181 @@ +#!/usr/bin/env python3 +"""Report documentation that quotes a superseded image version. + +Every lab's README carries recorded terminal output, and `evidence/` holds the +captured runs behind it. Both name image versions. When the automation bumps a +pin, that recorded output becomes wrong. + +It is not fixed by a regex. A transcript rewritten to name a version nobody ran +is fabricated evidence, and the column alignment of `docker compose ps` output +would not survive the edit anyway. So this reports the drift and leaves the +files alone: a human regenerates them by running the lab, which is the same +thing that produced them in the first place. + +Two different homes, which is why the report says which one applies. `evidence/` +belongs to this repository and comes from each lab's `scripts/capture-evidence.sh`. +Every `README.md` under `labs/` is GENERATED from that lab's `.mdx` in +Zenable-io/next-gen-governance and carries a "Do not edit by hand" banner saying +so — a fix committed here is silently reverted by the next export. + +Config files are the source of truth. Anything found in a `.md` or under +`evidence/` naming a different version for the same image is reported. +""" + +import re +import subprocess +import sys +from collections import defaultdict +from pathlib import Path + +from supply_chain import annotations + +# `image: repo:tag` in compose, and `FROM repo:tag` in a Dockerfile. The tag is +# captured separately so a digest suffix or a trailing comment does not join it. +_IMAGE_RE = re.compile( + r"(?:image:\s*|FROM\s+)(?P[a-z0-9][a-z0-9._/-]*[a-z0-9]):(?P[\w][\w.-]*)", + re.IGNORECASE, +) + +# The same reference as it appears in prose or recorded output, where it is not +# introduced by an `image:` or `FROM` key. +_MENTION_RE = re.compile(r"(?P[a-z0-9][a-z0-9._/-]*[a-z0-9]):(?P[\w][\w.-]*)") + +# Recorded output often names a product rather than an image: `kc.sh --version` +# prints `Keycloak 26.7.1`, with no registry path and no colon, which the +# reference pattern above cannot see. Matched against the repository's last path +# segment, so `quay.io/keycloak/keycloak` also covers "Keycloak ". +_PRODUCT_RE_TEMPLATE = r"\b{name}[ :v]+(?P\d+\.\d+(?:\.\d+)*)\b" + +# Only plain-semver pins take part in the product-name check. A templated tag +# like python's `3.13-slim` has no clean version to compare against, and the +# labs legitimately print a fuller `Python 3.13.12` for the same pin — which +# would read as drift on every single run. +_PLAIN_VERSION_RE = re.compile(r"^v?\d+\.\d+(?:\.\d+)*$") + +# `*Dockerfile*` rather than `*Dockerfile`, because a lab that builds more than +# one image suffixes them — `Dockerfile.get-started`, `Dockerfile.tickets`. The +# unsuffixed glob silently skipped both, which reads as "no drift" rather than +# as an error. +_CONFIG_GLOBS = ("*compose.yml", "*compose.yaml", "*Dockerfile*") +_DOC_GLOBS = ("*.md", "*.txt", "*.json") + + +def _tracked_files(root: Path, globs: tuple[str, ...]) -> list[Path]: + """Git-tracked files matching any of ``globs``. + + Uses the index rather than a filesystem walk so a stray `.venv/` or a + build artifact can never be mistaken for repository content. + """ + result = subprocess.run( + ["git", "-C", str(root), "ls-files", "-z", *globs], + capture_output=True, + text=True, + check=True, + ) + return [root / name for name in result.stdout.split("\0") if name] + + +def collect_pinned_versions(root: Path) -> dict[str, dict[str, set[Path]]]: + """Map each image repository to the tags the config files pin it at. + + :return: ``{repo: {tag: {file, ...}}}`` + """ + pins: dict[str, dict[str, set[Path]]] = defaultdict(lambda: defaultdict(set)) + for path in _tracked_files(root, _CONFIG_GLOBS): + for match in _IMAGE_RE.finditer(path.read_text(encoding="utf-8")): + pins[match["repo"]][match["tag"]].add(path) + return pins + + +def find_drift(root: Path, pins: dict[str, dict[str, set[Path]]]) -> list[str]: + """Find documentation naming a tag the config files no longer use.""" + findings: list[str] = [] + config_files = {p.resolve() for p in _tracked_files(root, _CONFIG_GLOBS)} + + for path in _tracked_files(root, _DOC_GLOBS): + if path.resolve() in config_files: + continue + try: + content = path.read_text(encoding="utf-8") + except UnicodeDecodeError: + continue + + seen: set[tuple[str, str]] = set() + + def record(repo: str, tag: str, shown: str, *, strip_v: bool = False) -> None: + known_tags = pins.get(repo) + # A bare `word:word` is far too common in prose to treat as an image + # reference, so only repositories the config files actually pin count. + if not known_tags: + return + # The product-name form captures the digits only, so `agentgateway:v1.4.1` + # reaches here as `1.4.1` and would otherwise read as drift against its + # own pin. The reference form keeps the tag verbatim, where a stray `v` + # really is a different tag. + comparable = ( + {t.lstrip("vV") for t in known_tags} if strip_v else known_tags + ) + if (tag.lstrip("vV") if strip_v else tag) in comparable: + return + if (repo, tag) in seen: + return + seen.add((repo, tag)) + current = ", ".join(sorted(known_tags)) + findings.append( + f"{path.relative_to(root)}: names {shown}, but the " + f"configuration now pins {repo}:{current}" + ) + + for match in _MENTION_RE.finditer(content): + record(match["repo"], match["tag"], f"{match['repo']}:{match['tag']}") + + for repo, known_tags in pins.items(): + if not any(_PLAIN_VERSION_RE.match(t) for t in known_tags): + continue + product = repo.rsplit("/", 1)[-1] + product_re = re.compile( + _PRODUCT_RE_TEMPLATE.format(name=re.escape(product)), re.IGNORECASE + ) + for match in product_re.finditer(content): + record(repo, match["tag"], f"{product} {match['tag']}", strip_v=True) + + return findings + + +def main() -> int: + root = Path( + subprocess.run( + ["git", "rev-parse", "--show-toplevel"], + capture_output=True, + text=True, + check=True, + ).stdout.strip() + ) + + findings = find_drift(root, collect_pinned_versions(root)) + if not findings: + print("No documentation drift found.") + return 0 + + print(f"{len(findings)} file reference(s) now disagree with the pinned versions:") + for finding in findings: + print(f" - {finding}") + annotations.warning(finding, title="doc-quotes-superseded-version") + + print( + "\nRegenerate the recorded output rather than editing it by hand. Where it " + "lives depends on the file:\n" + " evidence/** is owned here — re-run the lab's scripts/capture-evidence.sh.\n" + " README.md is GENERATED from the lab's .mdx in Zenable-io/next-gen-governance\n" + " (services/ui_frontend/src/lib/labs/content/labs/). Fix the transcript\n" + " there, then re-export with\n" + " `node services/ui_frontend/scripts/export-lab-readme.js --all `.\n" + " Editing README.md here is undone by the next export." + ) + # Advisory, not a gate. The update PR should still open; a human decides + # whether the transcripts are worth re-recording this week. + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/.github/scripts/pyproject.toml b/.github/scripts/pyproject.toml new file mode 100644 index 0000000..b2c4082 --- /dev/null +++ b/.github/scripts/pyproject.toml @@ -0,0 +1,16 @@ +[project] +name = "labs-update-tooling" +version = "0" +description = "Cooldown-gated, integrity-verified dependency pinning for Zenable Labs" +requires-python = ">=3.11" +dependencies = [ + "requests>=2.32", + "PyYAML>=6.0", +] + +[tool.uv] +package = false +# The same 7-day supply-chain cooldown this tooling enforces on the labs, applied +# to the tooling itself. Tooling that adopts a same-day release while telling the +# repo to wait a week is not a control, it is a suggestion. +exclude-newer = "7 days" diff --git a/.github/scripts/supply_chain/__init__.py b/.github/scripts/supply_chain/__init__.py new file mode 100644 index 0000000..49c5395 --- /dev/null +++ b/.github/scripts/supply_chain/__init__.py @@ -0,0 +1,13 @@ +"""Supply-chain primitives for the Zenable Labs dependency automation. + +A deliberately small subset of ``zenable_monorepo`` from Zenable-io/next-gen-governance, +carrying only what this repository's pins need: a release cooldown, GitHub release +and tag->commit integrity verification, Docker Hub tag discovery, and CI annotations. + +Vendored rather than depended on because next-gen-governance is private and this +repository is public. The security-relevant logic — the cooldown window, the +release -> tag ref -> commit -> reachable-from-branch chain — is kept behaviourally +identical to upstream so a fix there ports here as a readable diff. When a third +repository needs this, extract these modules into a package both can consume +rather than vendoring a second copy. +""" diff --git a/.github/scripts/supply_chain/annotations.py b/.github/scripts/supply_chain/annotations.py new file mode 100644 index 0000000..d28c773 --- /dev/null +++ b/.github/scripts/supply_chain/annotations.py @@ -0,0 +1,71 @@ +"""GitHub Actions annotation utilities for CI workflow commands. + +Emits ``::warning``/``::error``/``::notice`` workflow commands to stdout, and +no-ops outside GitHub Actions so local runs stay readable. + +Trimmed from ``zenable_monorepo.annotations``: this repository never annotates a +column range or opens a ``::group::``, so those are omitted rather than carried +as dead code. + +See https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/workflow-commands-for-github-actions +""" + +import os +from pathlib import Path + + +def _in_github_actions() -> bool: + return os.environ.get("GITHUB_ACTIONS") == "true" + + +def _escape_message(value: str) -> str: + """Percent-encode the characters GitHub reserves in message text.""" + return value.replace("%", "%25").replace("\r", "%0D").replace("\n", "%0A") + + +def _escape_property(value: str) -> str: + """Property values additionally escape the ``:`` and ``,`` delimiters.""" + return _escape_message(value).replace(":", "%3A").replace(",", "%2C") + + +def _annotate( + level: str, + message: str, + *, + file: str | Path | None = None, + title: str | None = None, +) -> str: + """Format and (in CI) print one annotation. Returns the formatted string.""" + parts = [] + if file is not None: + parts.append(f"file={_escape_property(str(file))}") + if title is not None: + parts.append(f"title={_escape_property(title)}") + prop_str = f" {','.join(parts)}" if parts else "" + result = f"::{level}{prop_str}::{_escape_message(message)}" + + if _in_github_actions(): + print(result) + + return result + + +def warning( + message: str, *, file: str | Path | None = None, title: str | None = None +) -> str: + """Emit a ``::warning`` annotation. Returns the formatted string.""" + return _annotate("warning", message, file=file, title=title) + + +def error( + message: str, *, file: str | Path | None = None, title: str | None = None +) -> str: + """Emit an ``::error`` annotation. Returns the formatted string.""" + return _annotate("error", message, file=file, title=title) + + +def notice( + message: str, *, file: str | Path | None = None, title: str | None = None +) -> str: + """Emit a ``::notice`` annotation. Returns the formatted string.""" + return _annotate("notice", message, file=file, title=title) diff --git a/.github/scripts/supply_chain/cooldown.py b/.github/scripts/supply_chain/cooldown.py new file mode 100644 index 0000000..bab1a43 --- /dev/null +++ b/.github/scripts/supply_chain/cooldown.py @@ -0,0 +1,176 @@ +"""Supply-chain cooldown primitives shared across the update tooling. + +Lags dependency adoption behind upstream releases by ``cooldown_days`` so there +is time for a supply chain compromise to be detected before we consume a new +release. See https://github.com/aquasecurity/trivy/discussions/10425 + +Timestamp reliability matters: some sources expose infrastructure-controlled +publish times (GitHub release ``published_at``, Docker Hub ``last_updated``) +that a committer cannot forge, while others only expose VCS commit or tag times +that are forgeable via ``GIT_COMMITTER_DATE``. Callers classify their source via +:class:`TimestampReliability` so the distinction surfaces in the run's +annotations rather than being invisibly assumed away. + +Kept behaviourally identical to ``zenable_monorepo.cooldown`` in +Zenable-io/next-gen-governance so a fix there ports here as a readable diff. +""" + +from datetime import datetime, timedelta, timezone +from pathlib import Path + +from supply_chain import annotations + + +class TimestampReliability: + """Classifies trustworthiness of timestamps used in security-sensitive checks. + + UNRELIABLE: git commit/author dates and tag dates — forgeable via + GIT_COMMITTER_DATE, GIT_AUTHOR_DATE, or history rewrites. + + RELIABLE: registry publish times and GitHub API timestamps — + infrastructure-controlled, not forgeable by a committer. + """ + + RELIABLE = "reliable" + UNRELIABLE = "unreliable" + + +def emit_cooldown_annotation( + file_path: Path, + reliability: str, + timestamp: str, + elapsed_days: int, + cooldown_days: int, + allowed: bool, +) -> None: + """Annotate which kind of timestamp a cooldown decision rested on. + + Surfaces on the run summary, so reliance on forgeable data is visible + rather than buried in a log line nobody reads. + """ + status = "allowed" if allowed else "blocked" + + if reliability == TimestampReliability.UNRELIABLE: + annotations.warning( + f"Cooldown check ({status}) used a forgeable timestamp ({timestamp}, " + f"{elapsed_days}d ago, cooldown={cooldown_days}d). " + f"VCS timestamps are forgeable via GIT_COMMITTER_DATE/GIT_AUTHOR_DATE. " + f"Immutable sources (registry publish time) are preferred.", + file=file_path, + title=f"cooldown-timestamp-{reliability}", + ) + else: + annotations.notice( + f"Cooldown check ({status}) used verified timestamp ({timestamp}, " + f"{elapsed_days}d ago, cooldown={cooldown_days}d).", + file=file_path, + title=f"cooldown-timestamp-{reliability}", + ) + + +# The cooldown buys time for SOMEONE ELSE to notice a compromised upstream, so +# for a repo we publish ourselves it protects nothing and only blocks adopting +# something we just shipped. Waived centrally, never by a per-caller flag: one +# list to review, one place to audit. +# +# Keys are the upstream's stable identity, lowercased — never a versioned label, +# which would silently stop matching on the next release and quietly re-impose +# the cooldown nobody meant to restore. +COOLDOWN_EXEMPT_SOURCES: dict[str, str] = { + "zenable-io/labs": "this repo; a release of ours is not third-party supply chain", + "jonzeolla/lab-resources": "Zenable-owned; we cut the releases we consume", +} + + +def is_cooldown_exempt(source: str | None) -> bool: + """True when ``source`` is centrally allowlisted out of the cooldown.""" + if not source: + return False + return source.strip().lower() in COOLDOWN_EXEMPT_SOURCES + + +def is_within_cooldown( + published_at: datetime | None, cooldown_days: int, *, source: str | None = None +) -> bool: + """Quick cooldown check without logging or annotations. + + Returns True if the version is too recent (still within the cooldown). + """ + if is_cooldown_exempt(source): + return False + if cooldown_days <= 0: + return False + if published_at is None: + return False + return datetime.now(timezone.utc) - published_at < timedelta(days=cooldown_days) + + +def check_cooldown( + published_at: datetime | None, + cooldown_days: int, + log, + *, + file_path: Path | None = None, + source_label: str = "", + source: str | None = None, + reliability: str = TimestampReliability.RELIABLE, +) -> bool: + """Check whether enough time has passed since the upstream release published. + + :param published_at: When the upstream version was published (None to skip). + :param cooldown_days: Minimum age in days of a release before we adopt it. + :param log: Logger instance. + :param file_path: Optional file path for CI annotations. + :param source_label: Human-readable label for logs (e.g. "keycloak/keycloak@26.7.2"). + :param source: Stable upstream identity checked against + :data:`COOLDOWN_EXEMPT_SOURCES`. Omit to apply the cooldown unconditionally. + :param reliability: Whether the timestamp source is forgeable. + :return: True if the cooldown has elapsed (OK to update), False otherwise. + """ + if is_cooldown_exempt(source): + # Annotated, not merely logged: waiving a supply-chain control should be + # visible on the run that did it. + assert source is not None + msg = ( + f"Cooldown waived for {source}: " + f"{COOLDOWN_EXEMPT_SOURCES[source.strip().lower()]}" + ) + log.info(msg) + annotations.notice(msg, file=file_path, title="cooldown-exempt-source") + return True + + if cooldown_days <= 0: + return True + + if published_at is None: + msg = ( + "No publish date available for upstream release, skipping cooldown check. " + "Cooldown provides no protection for this source." + ) + log.warning(msg) + annotations.warning( + msg, file=file_path, title="cooldown-bypassed-no-publish-date" + ) + return True + + elapsed = datetime.now(timezone.utc) - published_at + allowed = elapsed >= timedelta(days=cooldown_days) + + emit_cooldown_annotation( + file_path=file_path or Path("unknown"), + reliability=reliability, + timestamp=published_at.isoformat(), + elapsed_days=elapsed.days, + cooldown_days=cooldown_days, + allowed=allowed, + ) + + if not allowed: + label = f" ({source_label})" if source_label else "" + log.info( + f"Cooldown active: upstream release{label} published {elapsed.days}d ago " + f"(cooldown: {cooldown_days}d). Skipping." + ) + return False + + return True diff --git a/.github/scripts/supply_chain/docker_hub.py b/.github/scripts/supply_chain/docker_hub.py new file mode 100644 index 0000000..ea83baa --- /dev/null +++ b/.github/scripts/supply_chain/docker_hub.py @@ -0,0 +1,209 @@ +"""Docker Hub tag discovery for image pins. + +New code rather than a port. ``zenable_monorepo.registry`` resolves a pin by +asking which semver tag shares a digest with ``latest``, which cannot express +either of this repository's image pins: ``python:3.13-slim`` is a suffixed tag +that ``latest`` never points at, and ``jaegertracing/all-in-one`` must stay on +its 1.x line because 2.x moved to a different image entirely. + +So a pin here is a *tag template* plus an optional version line, and discovery +is a filtered listing rather than a digest comparison. + +Cooldown caveat, deliberately not hidden: ``last_updated`` is when the tag was +last PUSHED, not when the version first appeared, so it only means "this version +is N days old" for a tag the publisher writes once. That holds for a release tag +like ``jaegertracing/all-in-one:1.76.0`` and does NOT hold for a floating tag +like ``python:3.13-slim``, which is rebuilt whenever its base OS changes and is +therefore permanently a day old. + +The failure is safe but useless: every candidate reads as too recent, so the pin +never moves. Do not use this source for a floating tag — take the version from a +source with a real publish date, or pin the digest. The timestamp is +registry-controlled either way, and so unforgeable by a committer, which is why +it is classified RELIABLE. +""" + +import logging +import re +from dataclasses import dataclass +from datetime import datetime, timezone + +from supply_chain.github import sort_key +from supply_chain.retry import raising_session, retry_on_transient + +log = logging.getLogger(__name__) + +_LEVEL_SUFFIXES: dict[str, str] = { + "major": r"\d+", + "minor": r"\d+\.\d+", + "patch": r"\d+\.\d+\.\d+", +} + +_PAGE_SIZE = 100 + +# Docker Hub holds thousands of tags for a popular image. The cap keeps a +# misconfigured filter from paging forever; hitting it raises rather than +# returning a partial listing, because "newest of the first 30 pages" is a +# wrong answer that looks like a right one. +_MAX_PAGES = 30 + +_VERSION_PLACEHOLDER = "{version}" + + +@dataclass(frozen=True) +class TagInfo: + """One Docker Hub tag that matched the pin's template.""" + + version: str + tag: str + last_updated: datetime | None + + +def _version_regex(pin_level: str, version_line: str) -> str: + """Build the version portion of the tag regex. + + ``version_line`` narrows to a release line ("1" or "3.13") and is matched + literally; the remaining segments come from the pin level. + """ + try: + full = _LEVEL_SUFFIXES[pin_level] + except KeyError: + raise ValueError(f"Unsupported pin_level: {pin_level}") from None + + if not version_line: + return full + + line_parts = version_line.split(".") + level_parts = full.split(r"\.") + if len(line_parts) > len(level_parts): + raise ValueError( + f"version_line {version_line!r} is more specific than pin_level " + f"{pin_level!r}; it would match nothing" + ) + for part in line_parts: + if not part.isdigit(): + raise ValueError(f"version_line must be numeric segments, got: {version_line!r}") + + remaining = level_parts[len(line_parts) :] + return r"\.".join([re.escape(p) for p in line_parts] + remaining) + + +def _tag_regex(tag_template: str, pin_level: str, version_line: str) -> re.Pattern[str]: + """Compile the fully-anchored regex a candidate tag must match. + + Anchored for the same reason the GitHub tag pattern is: it is what keeps + ``3.13.1-slim-bookworm`` from satisfying a ``{version}-slim`` minor pin. + """ + if _VERSION_PLACEHOLDER not in tag_template: + raise ValueError(f"tag_template must contain {_VERSION_PLACEHOLDER}: {tag_template!r}") + prefix, _, suffix = tag_template.partition(_VERSION_PLACEHOLDER) + version_part = _version_regex(pin_level, version_line) + return re.compile(rf"^{re.escape(prefix)}({version_part}){re.escape(suffix)}$") + + +def default_tag_filter(tag_template: str, version_line: str) -> str: + """Pick the Docker Hub ``name=`` substring that narrows the listing most. + + The API takes one substring filter, so choose the longest literal in the + template — for ``{version}-slim`` that is ``-slim``, which cuts + ``library/python`` from thousands of tags to hundreds. With no literal to + use, fall back to the version line. + """ + literals = [part for part in tag_template.split(_VERSION_PLACEHOLDER) if part] + if literals: + return max(literals, key=len) + return f"{version_line}." if version_line else "" + + +@retry_on_transient(max_attempts=5) +def _get_page(session, url: str, params: dict | None) -> dict: + return session.get(url, params=params, timeout=30).json() + + +def fetch_tags(repository: str, name_filter: str = "") -> list[dict]: + """List a repository's tags, following pagination. + + :param repository: ``namespace/name``; a bare name is assumed to be official. + :param name_filter: Substring the tag name must contain, applied server-side. + :raises ValueError: If the listing exceeds :data:`_MAX_PAGES`. + """ + if "/" not in repository: + repository = f"library/{repository}" + + session = raising_session() + url: str | None = f"https://hub.docker.com/v2/repositories/{repository}/tags" + params: dict | None = {"page_size": _PAGE_SIZE, "name": name_filter} + + tags: list[dict] = [] + for _ in range(_MAX_PAGES): + payload = _get_page(session, url, params) + tags.extend(payload.get("results", [])) + url = payload.get("next") + # The `next` link already carries the query string. + params = None + if not url: + return tags + + raise ValueError( + f"Tag listing for {repository} (name={name_filter!r}) exceeded {_MAX_PAGES} " + f"pages. Narrow the filter rather than raising the cap: a truncated " + f"listing silently yields the wrong 'newest' tag." + ) + + +def _parse_timestamp(value: str | None) -> datetime | None: + """Parse a Docker Hub ISO-8601 timestamp into an aware UTC datetime.""" + if not value: + return None + try: + return datetime.fromisoformat(value.replace("Z", "+00:00")).astimezone( + timezone.utc + ) + except ValueError: + log.warning("Unparseable Docker Hub timestamp: %s", value) + return None + + +def get_matching_tags( + repository: str, + tag_template: str, + pin_level: str = "minor", + version_line: str = "", + name_filter: str | None = None, +) -> list[TagInfo]: + """Return the tags matching the template and pin level, newest-first. + + :param repository: Docker Hub repository, e.g. ``library/python``. + :param tag_template: Tag shape with a ``{version}`` placeholder, e.g. ``{version}-slim``. + :param pin_level: "major", "minor", or "patch" — how many version segments the tag carries. + :param version_line: Restrict to a release line, e.g. ``1`` or ``3.13``. + :param name_filter: Override the server-side substring filter. + """ + matcher = _tag_regex(tag_template, pin_level, version_line) + if name_filter is None: + name_filter = default_tag_filter(tag_template, version_line) + + log.info( + "Listing %s tags matching %s (name filter: %r)", + repository, + matcher.pattern, + name_filter, + ) + raw_tags = fetch_tags(repository, name_filter) + + matches: list[TagInfo] = [] + for entry in raw_tags: + name = entry.get("name", "") + matched = matcher.match(name) + if not matched: + continue + matches.append( + TagInfo( + version=matched.group(1), + tag=name, + last_updated=_parse_timestamp(entry.get("last_updated")), + ) + ) + + matches.sort(key=lambda t: sort_key(t.version), reverse=True) + return matches diff --git a/.github/scripts/supply_chain/github.py b/.github/scripts/supply_chain/github.py new file mode 100644 index 0000000..55d39d9 --- /dev/null +++ b/.github/scripts/supply_chain/github.py @@ -0,0 +1,312 @@ +"""GitHub release discovery and tag->commit integrity verification. + +Trimmed from ``zenable_monorepo.github``: this repository has no monorepo tag +namespaces and no releaseless repositories to pin, so ``tag_namespace`` and the +git-refs tag source are omitted. The verification chain is carried over +unchanged — it is the part that matters. +""" + +import functools +import logging +import os +import re +from datetime import datetime, timezone + +import requests + +from supply_chain import annotations +from supply_chain.retry import is_transient, raising_session, retry_on_transient + +log = logging.getLogger(__name__) + +_LEVEL_SUFFIXES: dict[str, str] = { + "major": r"\d+", + "minor": r"\d+\.\d+", + "patch": r"\d+\.\d+\.\d+", +} + + +@functools.lru_cache(maxsize=None) +def version_pattern(v_prefix: bool = True, pin_level: str = "minor") -> re.Pattern[str]: + """Compile the fully-anchored tag pattern for a pin level. + + The anchoring is load-bearing, not cosmetic. Upstreams mislabel prereleases: + agentgateway ships ``v1.4.0-beta.1`` with ``prerelease: false``, so the + release metadata alone would let a beta through. ``^v\\d+\\.\\d+\\.\\d+$`` + rejects it on shape regardless of what the publisher claimed. + + :param v_prefix: Whether the tag carries a leading ``v``. + :param pin_level: "major", "minor", or "patch". + """ + try: + suffix = _LEVEL_SUFFIXES[pin_level] + except KeyError: + raise ValueError(f"Unsupported pin_level: {pin_level}") from None + return re.compile(rf"^{'v' if v_prefix else ''}{suffix}$") + + +def sort_key(version: str) -> list: + """Sort a version string numerically per segment, ignoring any ``v`` prefix.""" + return [ + int(part) if part.isdigit() else part + for part in version.lstrip("vV").split(".") + ] + + +def _github_headers() -> dict[str, str]: + """Build GitHub API headers, including the auth token when one is available.""" + headers = {"Accept": "application/vnd.github.v3+json"} + token = os.environ.get("GITHUB_TOKEN") + if token: + headers["Authorization"] = f"Bearer {token}" + return headers + + +def parse_github_timestamp(value: str | None) -> datetime | None: + """Parse a GitHub ISO-8601 timestamp into an aware UTC datetime.""" + if not value: + return None + return datetime.fromisoformat(value.replace("Z", "+00:00")).astimezone(timezone.utc) + + +@retry_on_transient(max_attempts=5) +def get_matching_releases( + owner: str, + repo: str, + pin_level: str = "minor", + v_prefix: bool = True, +) -> list[dict]: + """Return the releases whose tags match ``pin_level``, newest-first. + + Drafts and anything flagged ``prerelease`` are dropped, and the tag pattern + then drops anything that merely looks like a prerelease. Each element is the + full release dict, so the caller gets ``published_at`` without a second call. + """ + tag_matcher = version_pattern(v_prefix, pin_level) + + releases_url = f"https://api.github.com/repos/{owner}/{repo}/releases" + session = raising_session() + response = session.get(releases_url, headers=_github_headers(), timeout=30) + + matching = [] + for release in response.json(): + if release.get("prerelease", False) or release.get("draft", False): + continue + if tag_matcher.match(release.get("tag_name", "")): + matching.append(release) + + matching.sort(key=lambda r: sort_key(r["tag_name"]), reverse=True) + return matching + + +@retry_on_transient(max_attempts=5) +def _get_tag_ref(owner: str, repo: str, tag: str) -> dict: + """Get a tag reference from the repository's git refs. + + Confirms the tag exists as a first-class ref in THIS repo, rather than being + merely reachable by SHA through GitHub's shared object storage, which + includes every fork's objects. + """ + url = f"https://api.github.com/repos/{owner}/{repo}/git/refs/tags/{tag}" + session = raising_session() + response = session.get(url, headers=_github_headers(), timeout=30) + return response.json() + + +@retry_on_transient(max_attempts=5) +def _resolve_ref_to_commit(owner: str, repo: str, ref_data: dict) -> str: + """Resolve a git ref object to its underlying commit SHA. + + Handles both lightweight tags (type "commit") and annotated tags (type + "tag"), which need a further dereference. + """ + obj = ref_data["object"] + if obj["type"] == "commit": + return obj["sha"] + if obj["type"] == "tag": + url = f"https://api.github.com/repos/{owner}/{repo}/git/tags/{obj['sha']}" + session = raising_session() + response = session.get(url, headers=_github_headers(), timeout=30) + tag_obj = response.json() + if tag_obj["object"]["type"] != "commit": + raise ValueError( + f"Annotated tag dereferences to unexpected type: {tag_obj['object']['type']}" + ) + return tag_obj["object"]["sha"] + raise ValueError(f"Unexpected ref object type: {obj['type']}") + + +@retry_on_transient(max_attempts=5) +def _is_ancestor_of_branch( + session: requests.Session, owner: str, repo: str, commit_sha: str, branch_name: str +) -> bool: + """Check whether ``commit_sha`` is an ancestor of (or equal to) the branch. + + Uses the compare API: when the merge base of (commit, branch) equals the + commit itself, the commit is in that branch's history. + """ + compare_url = ( + f"https://api.github.com/repos/{owner}/{repo}/compare/{commit_sha}...{branch_name}" + ) + try: + response = session.get(compare_url, headers=_github_headers(), timeout=30) + response.raise_for_status() + merge_base_sha = response.json().get("merge_base_commit", {}).get("sha", "") + return merge_base_sha == commit_sha + except requests.HTTPError as exc: + if is_transient(exc): + raise + # Debug, not warning: the caller walks every branch, so one line per + # branch would bury the single verdict that matters under sixty copies + # of it. An outright missing commit is caught by _commit_exists before + # the walk starts, and the walk's own failure is annotated by the caller. + log.debug( + "Ancestry check for %s on %s/%s branch %s failed: %s", + commit_sha, + owner, + repo, + branch_name, + exc, + ) + return False + + +@retry_on_transient(max_attempts=5) +def _commit_exists(owner: str, repo: str, commit_sha: str) -> bool: + """Whether ``commit_sha`` resolves at all under ``owner/repo``. + + Note this is NOT a provenance check and cannot replace reachability: because + GitHub serves forks from shared object storage, a commit pushed only to a + fork still resolves under the parent. It exists to fail a nonexistent SHA in + one request instead of one per branch. + """ + url = f"https://api.github.com/repos/{owner}/{repo}/commits/{commit_sha}" + response = requests.get(url, headers=_github_headers(), timeout=30) + # 404 is the ordinary "no such commit". 422 is what GitHub returns for a + # SHA it will not even look up (the all-zero SHA, for one); both mean the + # ref does not resolve here, and neither is worth a stack trace. + if response.status_code in (404, 422): + return False + response.raise_for_status() + return True + + +@retry_on_transient(max_attempts=5) +def _get_default_branch(owner: str, repo: str) -> str: + url = f"https://api.github.com/repos/{owner}/{repo}" + session = raising_session() + response = session.get(url, headers=_github_headers(), timeout=30) + return response.json().get("default_branch", "main") + + +@retry_on_transient(max_attempts=5) +def _get_branches(owner: str, repo: str, per_page: int = 100) -> list[dict]: + """Get all branches for a repository, following pagination.""" + branches: list[dict] = [] + url: str | None = ( + f"https://api.github.com/repos/{owner}/{repo}/branches?per_page={per_page}" + ) + session = raising_session() + headers = _github_headers() + while url: + response = session.get(url, headers=headers, timeout=30) + branches.extend(response.json()) + url = response.links.get("next", {}).get("url") + return branches + + +def verify_commit_on_branch(owner: str, repo: str, commit_sha: str) -> str: + """Verify a commit is reachable from at least one branch in the repository. + + Imposter commits — pushed to a fork but referenced by SHA as if they belonged + to the parent repo — are not reachable from any branch of the parent. This is + the core defense against: + - https://github.com/aquasecurity/trivy/discussions/10425 + - https://github.com/aquasecurity/trivy/security/advisories/GHSA-69fq-xp46-6x23 + + :return: The name of a branch the commit is reachable from. + :raises ValueError: If the commit is unreachable (a potential imposter). + """ + if not _commit_exists(owner, repo, commit_sha): + raise ValueError(f"Commit {commit_sha} does not exist in {owner}/{repo}") + + session = requests.Session() + + # Check the default branch first; it is the overwhelmingly common case and + # usually saves listing every branch. + default_branch = _get_default_branch(owner, repo) + if _is_ancestor_of_branch(session, owner, repo, commit_sha, default_branch): + return default_branch + + # Fall back to every branch, which is what covers release branches. + for branch in _get_branches(owner, repo): + branch_name = branch["name"] + if branch_name == default_branch: + continue + if _is_ancestor_of_branch(session, owner, repo, commit_sha, branch_name): + return branch_name + + raise ValueError( + f"Commit {commit_sha} is not reachable from any branch in {owner}/{repo}. " + f"This may indicate an imposter commit from a fork. " + f"See https://github.com/aquasecurity/trivy/security/advisories/GHSA-69fq-xp46-6x23" + ) + + +def verify_tag_commit_integrity(owner: str, repo: str, tag: str) -> dict: + """Verify that a tag maps to a legitimate commit on the repository. + + 1. Confirm the tag exists as a ref in this repo's git refs + 2. Resolve the tag to its underlying commit SHA (handling annotated tags) + 3. Corroborate by checking the commit is reachable from a named branch + + Step 1 is the load-bearing one, and it is a hard failure. Git refs are + repo-scoped: a fork's tags never appear in the parent's refs, however freely + GitHub's content-addressable storage shares the underlying objects. A tag ref + present here was therefore created by someone with push access to this + repository, which is exactly what the imposter-commit attack lacks — that + attack references a *bare SHA* with no ref behind it. + + Step 3 is advisory here, and that is a deliberate difference from + ``verify_action_pins.py``, where a bare SHA is all there is and reachability + is the only defense available. Release tooling routinely tags a commit that + never lands on a branch: Keycloak's ``Set version to 26.7.2`` is tagged and + then superseded on ``release/26.7`` by the next development-version commit, + so the tag commit legitimately diverges from every branch. Failing on that + would block a correctly-published release, so it warns instead. + + :return: Dict with tag_ref_sha, commit_sha, and reachable_from_branch + (``None`` when no branch reaches the commit). + :raises ValueError: If the tag ref is missing or does not resolve to a commit. + """ + log.info(f"Verifying tag integrity: {owner}/{repo}@{tag}") + + tag_ref = _get_tag_ref(owner, repo, tag) + tag_ref_sha = tag_ref["object"]["sha"] + log.info( + f"Tag ref confirmed: {tag} -> {tag_ref_sha} (type: {tag_ref['object']['type']})" + ) + + commit_sha = _resolve_ref_to_commit(owner, repo, tag_ref) + log.info(f"Resolved to commit: {commit_sha}") + + try: + branch = verify_commit_on_branch(owner, repo, commit_sha) + except ValueError: + branch = None + msg = ( + f"{owner}/{repo}@{tag} resolves to {commit_sha}, which is not reachable " + f"from any branch. The tag ref itself is present in {owner}/{repo}, so " + f"this is most likely a release-tooling commit that was tagged without " + f"being merged back — but confirm the tag before merging." + ) + log.warning(msg) + annotations.warning(msg, title="tag-commit-not-on-branch") + else: + log.info(f"Commit {commit_sha} verified reachable from branch: {branch}") + + return { + "tag_ref_sha": tag_ref_sha, + "commit_sha": commit_sha, + "reachable_from_branch": branch, + } diff --git a/.github/scripts/supply_chain/retry.py b/.github/scripts/supply_chain/retry.py new file mode 100644 index 0000000..16eebd1 --- /dev/null +++ b/.github/scripts/supply_chain/retry.py @@ -0,0 +1,125 @@ +"""Retry helper for the handful of HTTP calls this tooling makes. + +``zenable_monorepo.retries`` upstream covers a much larger surface — arbitrary +exception tuples, per-call retryable predicates, several backoff policies. This +tooling talks to exactly two JSON APIs, so this is deliberately the smaller +thing: retry rate limits and transient 5xx, honour the server's own wait hint, +and let anything else fail immediately. + +A non-transient failure must NOT be retried. Every caller here is a +supply-chain check, and a check that quietly swallows a 404 on a tag ref is +worse than one that stops the run. +""" + +import functools +import logging +import time +from collections.abc import Callable +from datetime import datetime, timezone + +import requests + +log = logging.getLogger(__name__) + +# GitHub signals a primary rate limit with 403 + x-ratelimit-remaining: 0, a +# secondary one with 403/429 + retry-after. Docker Hub uses a plain 429. +_RATE_LIMIT_STATUSES = frozenset({403, 429}) +_TRANSIENT_STATUSES = frozenset({500, 502, 503, 504}) + +# Beyond this, waiting out a rate-limit reset costs more than failing the +# weekly run and picking the updates up next week. +_MAX_SLEEP_SECONDS = 300 + + +def _retry_after_seconds(response: requests.Response) -> float | None: + """Seconds the server asked us to wait, from Retry-After or the reset epoch.""" + retry_after = response.headers.get("retry-after") + if retry_after: + try: + return float(retry_after) + except ValueError: + # The RFC also permits an HTTP-date; neither API sends one, so + # fall through to the reset header rather than parse it. + pass + + if response.headers.get("x-ratelimit-remaining") == "0": + reset = response.headers.get("x-ratelimit-reset") + if reset: + try: + reset_at = datetime.fromtimestamp(float(reset), tz=timezone.utc) + except (ValueError, OSError): + return None + return max(0.0, (reset_at - datetime.now(timezone.utc)).total_seconds()) + + return None + + +def is_transient(exc: BaseException) -> bool: + """True when ``exc`` is worth retrying: a rate limit, a 5xx, or a dropped connection.""" + if isinstance(exc, (requests.ConnectionError, requests.Timeout)): + return True + if not isinstance(exc, requests.HTTPError) or exc.response is None: + return False + status = exc.response.status_code + if status in _TRANSIENT_STATUSES: + return True + if status not in _RATE_LIMIT_STATUSES: + return False + # A 403 is only a rate limit when the server says so. A 403 from a private + # or renamed repository is a real answer and must surface as one. + return ( + exc.response.headers.get("x-ratelimit-remaining") == "0" + or "retry-after" in exc.response.headers + ) + + +def retry_on_transient(max_attempts: int = 5) -> Callable: + """Retry the wrapped call on transient HTTP failures with exponential backoff. + + Honours the server's ``Retry-After``/``x-ratelimit-reset`` hint when it gives + one, since guessing shorter than the reset just burns the remaining budget. + """ + + def decorator(func: Callable) -> Callable: + @functools.wraps(func) + def wrapper(*args, **kwargs): + for attempt in range(1, max_attempts + 1): + try: + return func(*args, **kwargs) + except Exception as exc: + if attempt == max_attempts or not is_transient(exc): + raise + delay = 2.0**attempt + if isinstance(exc, requests.HTTPError) and exc.response is not None: + hinted = _retry_after_seconds(exc.response) + if hinted is not None: + delay = hinted + if delay > _MAX_SLEEP_SECONDS: + log.error( + "%s asked us to wait %.0fs, past the %ds cap; failing", + func.__name__, + delay, + _MAX_SLEEP_SECONDS, + ) + raise + log.warning( + "%s failed (attempt %d/%d): %s; retrying in %.0fs", + func.__name__, + attempt, + max_attempts, + exc, + delay, + ) + time.sleep(delay) + raise AssertionError("unreachable") + + return wrapper + + return decorator + + +def raising_session() -> requests.Session: + """A session that turns any non-2xx response into an HTTPError.""" + session = requests.Session() + session.hooks = {"response": [lambda r, *args, **kwargs: r.raise_for_status()]} + return session diff --git a/.github/scripts/update-pins.sh b/.github/scripts/update-pins.sh new file mode 100644 index 0000000..25f96eb --- /dev/null +++ b/.github/scripts/update-pins.sh @@ -0,0 +1,126 @@ +#!/usr/bin/env bash +# +# Refresh every pinned dependency in this repository. +# +# Each block below is one pin: the upstream it comes from, the files that carry +# it, and the regex that finds it. Explicit and repetitive on purpose — a pin is +# reviewed by reading it, and a table-driven loop would hide the one field that +# ever matters (the search pattern) behind indirection. +# +# Run it locally the same way CI does: +# bash .github/scripts/update-pins.sh +# +# Every version bump is gated on a 7-day cooldown, and GitHub-sourced versions +# additionally have their tag -> commit -> reachable-from-branch chain verified +# before anything is written. See supply_chain/cooldown.py and supply_chain/github.py. + +set -euo pipefail + +GIT_ROOT="$(git rev-parse --show-toplevel)" +SCRIPTS_DIR="${GIT_ROOT}/.github/scripts" + +# --frozen so a run cannot silently resolve the tooling's own dependencies to +# something newer than the committed lock. +UPDATE_FILE=(uv run --directory "${SCRIPTS_DIR}" --frozen python update_file.py) + +# --------------------------------------------------------------------------- +# Container images +# --------------------------------------------------------------------------- + +# Keycloak. The image lives on quay.io, which publishes no usable release feed, +# but the upstream GitHub releases carry the same version numbers and give us a +# real publish date plus the tag/commit integrity check quay cannot. +# Both labs run the same Keycloak, so one lookup updates both files. +"${UPDATE_FILE[@]}" \ + --file "${GIT_ROOT}/labs/a2a/docker-compose.yml" \ + --file "${GIT_ROOT}/labs/agent-identity/docker-compose.yml" \ + --source-type github-release \ + --github-owner keycloak \ + --github-repo keycloak \ + --no-v-prefix \ + --pin-level patch \ + --no-downgrade \ + --search-pattern 'quay\.io/keycloak/keycloak:[0-9]+\.[0-9]+\.[0-9]+' \ + --replacement-pattern 'quay.io/keycloak/keycloak:{version}' + +# agentgateway. Image on ghcr.io, versions from GitHub releases. +# Note the anchored tag pattern is doing real work here: agentgateway ships +# `v1.4.0-beta.1` with `prerelease: false`, so release metadata alone would let a +# beta through. +# +# Held at v1.4.1 on purpose as of 2026-09-03, and this pin is still automated so +# the next run proposes v1.5.0 again. Read that PR before merging it: v1.5.0 +# renames the trace spans this lab teaches. `POST /*` disappears, `delete_session` +# and `get_stream` become `DELETE get-started` / `GET get-started`, and +# `tools/call get-started` splits per tool into `tools/call get-started_add`. +# That invalidates two recorded transcripts, the sentence about a tool call +# naming its target, and the instruction to click a `POST /*` row to find the +# parent span — all of which live in agentgateway-mcp.mdx in +# next-gen-governance. It is a lab-content change needing an author, not a +# version bump, which is why it was not taken here. +"${UPDATE_FILE[@]}" \ + --file "${GIT_ROOT}/labs/agentgateway-mcp/compose.yaml" \ + --source-type github-release \ + --github-owner agentgateway \ + --github-repo agentgateway \ + --pin-level patch \ + --no-downgrade \ + --search-pattern 'ghcr\.io/agentgateway/agentgateway:v[0-9]+\.[0-9]+\.[0-9]+' \ + --replacement-pattern 'ghcr.io/agentgateway/agentgateway:{version}' + +# Jaeger all-in-one, held to the 1.x line. Jaeger v2 is a different image +# (`jaegertracing/jaeger`) with a different CLI, so an unrestricted pin would +# "update" this lab into something that does not start. +"${UPDATE_FILE[@]}" \ + --file "${GIT_ROOT}/labs/agentgateway-mcp/compose.yaml" \ + --source-type docker-hub \ + --image jaegertracing/all-in-one \ + --version-line 1 \ + --pin-level patch \ + --no-downgrade \ + --search-pattern 'jaegertracing/all-in-one:[0-9]+\.[0-9]+\.[0-9]+' \ + --replacement-pattern 'jaegertracing/all-in-one:{version}' + +# NOT automated: `FROM python:3.13-slim`, in every lab Dockerfile. +# +# `3.13-slim` is a floating tag. Docker Hub rebuilds it whenever its base OS gets +# security updates, so the labs already receive those on every build with no pin +# change at all, and the tag's `last_updated` is perpetually a day old — which +# makes a release cooldown structurally unmeasurable for it. (get_matching_tags +# would return candidates that are all "too recent" forever, so this would fail +# closed and silently do nothing, but a pin that can never move should not be +# here pretending to.) +# +# What the pin actually controls is 3.13 versus 3.14, which is a compatibility +# decision for the labs' dependencies, not a supply-chain one. That belongs in a +# human PR that also re-runs the labs, so it is left out on purpose. +# +# The supply chain for this image is the digest, not the tag. If that matters +# more than the READMEs staying readable, pin `python:3.13-slim@sha256:...` and +# revisit. + +# --------------------------------------------------------------------------- +# Python lockfiles +# --------------------------------------------------------------------------- +# +# The cooldown for these is declarative: `exclude-newer = "7 days"` in each +# project's [tool.uv]. `uv lock --upgrade` honours it, so a package published +# yesterday is not resolvable here regardless of what this script does. + +echo "==> Refreshing uv lockfiles" +while IFS= read -r project; do + echo "--> ${project}" + uv lock --directory "${project}" --upgrade +done < <(git -C "${GIT_ROOT}" ls-files '*pyproject.toml' | xargs -n1 dirname | sed "s|^|${GIT_ROOT}/|") + +# --------------------------------------------------------------------------- +# Documentation drift +# --------------------------------------------------------------------------- +# +# Recorded terminal output is evidence, not configuration: rewriting a +# `docker compose ps` transcript to name a version nobody ran would be +# fabricating it. So the transcripts are reported, never edited, and a human +# regenerates them by running the lab. + +echo "==> Checking for documentation quoting superseded versions" +uv run --directory "${SCRIPTS_DIR}" --frozen python check_doc_drift.py diff --git a/.github/scripts/update_file.py b/.github/scripts/update_file.py new file mode 100644 index 0000000..3c320cb --- /dev/null +++ b/.github/scripts/update_file.py @@ -0,0 +1,449 @@ +#!/usr/bin/env python3 +"""Update a pinned version in one or more files, gated on cooldown and integrity. + +Resolves the newest upstream version that has cleared its supply-chain cooldown, +verifies it (for GitHub sources, that the tag maps to a commit reachable from a +branch of the repo it claims to come from), and rewrites the pin via an explicit +search/replacement regex pair. + +Trimmed from ``scripts/update_file.py`` in Zenable-io/next-gen-governance to the +two sources this repository actually pins from. Adding another (PyPI, npm, a +GitHub tag with no release) means a new ``_select_*`` branch and a source type; +the cooldown and downgrade machinery is source-agnostic already. + +Deliberately omitted, with reasons: +- Default patterns per file type. Every pin here is a bespoke line in a + compose file, Dockerfile, or README, so an inferred pattern would be a + guess. Both patterns are required arguments instead. +- ``--include-prerelease``. Nothing here should track a prerelease, and the + anchored tag regex rejects them on shape even when upstream mislabels one. +""" + +import argparse +import logging +import os +import re +import shutil +import subprocess +import sys +from datetime import datetime, timezone +from pathlib import Path + +from supply_chain import docker_hub, github +from supply_chain.cooldown import check_cooldown, is_within_cooldown + +log = logging.getLogger("update_file") + +# --------------------------------------------------------------------------- +# Version comparison for the --no-downgrade gate +# --------------------------------------------------------------------------- +# +# The gate exists so a refresh can never replace a pinned version with something +# strictly older. SemVer 2.0 ordering: numeric parts compared numerically, +# stable > prerelease at the same release level, numeric prerelease identifiers +# below alphanumeric ones. + +_VERSION_RE = re.compile(r"\d+(?:\.\d+){0,3}(?:-[\w.+-]+)?(?:\+[\w.-]+)?") + +# Hex digits are also decimal digits, so a hash pin's SHA would otherwise read as +# a version (`8177...` -> 8177) and wedge the downgrade gate shut. Full-length +# only, so a date-shaped version like `20260803` is never mistaken for one. +_GIT_SHA_RE = re.compile(r"\b(?:[0-9a-f]{40}|[0-9a-f]{64})\b") + + +def _parse_version(version: str) -> tuple: + """Return a sortable tuple for a SemVer-ish version string.""" + stripped = version.lstrip("vV") + main, _, _build = stripped.partition("+") + base, _, pre = main.partition("-") + release_parts: list[int] = [] + for piece in base.split("."): + if not piece.isdigit(): + break + release_parts.append(int(piece)) + # Pad to 3 so 1.0 == 1.0.0 and short versions sort against full ones. + while len(release_parts) < 3: + release_parts.append(0) + if not pre: + # No prerelease -> higher precedence than any prerelease at the same release. + return (tuple(release_parts), 1, ()) + pre_parts: list[tuple] = [] + for piece in pre.split("."): + # Per SemVer, numeric prerelease identifiers rank below alphanumeric ones. + pre_parts.append((0, int(piece)) if piece.isdigit() else (1, piece)) + return (tuple(release_parts), 0, tuple(pre_parts)) + + +def _is_version_upgrade(current: str, candidate: str) -> bool: + """True iff ``candidate`` is strictly newer than ``current``.""" + return _parse_version(current) < _parse_version(candidate) + + +def _extract_current_version(file_path: Path, search_pattern: str) -> str | None: + """Find the currently-pinned version in ``file_path`` using ``search_pattern``. + + Pulls a version-shaped substring out of each match so callers need not author + named capture groups. Returns None when nothing matches. + """ + try: + content = file_path.read_text(encoding="utf-8") + except OSError: + return None + try: + pattern_re = re.compile(search_pattern, flags=re.MULTILINE) + except re.error: + return None + for match in pattern_re.finditer(content): + version_match = _VERSION_RE.search(_GIT_SHA_RE.sub("", match.group(0))) + if version_match: + return version_match.group(0) + return None + + +def _should_skip_due_to_downgrade( + file_paths: list[Path], search_pattern: str, candidate: str +) -> bool: + """True iff any file's current pin is already >= the candidate. + + Fails open — if no current version can be extracted, the replacement + proceeds — so the gate never silently blocks a real update. + """ + for file_path in file_paths: + current = _extract_current_version(file_path, search_pattern) + if current is None: + continue + if not _is_version_upgrade(current, candidate): + log.info( + f"--no-downgrade: {file_path} pins {current}, which is not older " + f"than candidate {candidate}; leaving unchanged." + ) + return True + return False + + +# --------------------------------------------------------------------------- +# Source resolution +# --------------------------------------------------------------------------- + + +def _ensure_github_token() -> None: + """Populate GITHUB_TOKEN from ``gh auth token`` when it isn't already set. + + CI injects the token. Locally the gh CLI usually holds one, and without it + every request is unauthenticated and hits the 60/hour limit immediately. + """ + if os.environ.get("GITHUB_TOKEN"): + return + + gh_path = shutil.which("gh") + if not gh_path: + log.warning( + "GITHUB_TOKEN not set and gh CLI not found; GitHub API calls will be unauthenticated" + ) + return + + try: + result = subprocess.run( + [gh_path, "auth", "token"], capture_output=True, text=True, check=True + ) + except subprocess.CalledProcessError: + log.warning("gh auth token failed; GitHub API calls will be unauthenticated") + return + + token = result.stdout.strip() + if token: + os.environ["GITHUB_TOKEN"] = token + log.info("Resolved GITHUB_TOKEN from gh CLI") + + +def _select_github_release(args, cooldown_days: int) -> str | None: + """Newest GitHub release tag past its cooldown, or None if all are too recent.""" + source = f"{args.github_owner}/{args.github_repo}" + releases = github.get_matching_releases( + owner=args.github_owner, + repo=args.github_repo, + pin_level=args.pin_level, + v_prefix=args.v_prefix, + ) + if not releases: + raise ValueError( + f"No {args.pin_level}-level releases found for {source}" + ) + log.info( + f"Found {len(releases)} candidate release(s), newest: {releases[0]['tag_name']}" + ) + + for release in releases: + version = release["tag_name"] + published_at = github.parse_github_timestamp(release.get("published_at")) + if not is_within_cooldown(published_at, cooldown_days, source=source): + check_cooldown( + published_at, + cooldown_days, + log, + file_path=args.file[0], + source_label=f"{source}@{version}", + source=source, + ) + return version + elapsed = (datetime.now(timezone.utc) - published_at).days if published_at else 0 + log.debug( + f"Skipping {version}: published {elapsed}d ago (cooldown: {cooldown_days}d)" + ) + + log.info( + f"All {len(releases)} release(s) within cooldown ({cooldown_days}d). Skipping." + ) + return None + + +def _select_docker_hub_tag(args, cooldown_days: int) -> str | None: + """Newest Docker Hub tag past its cooldown, or None if all are too recent.""" + source = args.image + candidates = docker_hub.get_matching_tags( + repository=args.image, + tag_template=args.tag_template, + pin_level=args.pin_level, + version_line=args.version_line, + name_filter=args.tag_filter, + ) + if not candidates: + raise ValueError( + f"No tags matching {args.tag_template!r} at {args.pin_level} level " + f"found for image: {args.image}" + ) + log.info(f"Found {len(candidates)} candidate tag(s), newest: {candidates[0].tag}") + + for candidate in candidates: + if not is_within_cooldown(candidate.last_updated, cooldown_days, source=source): + check_cooldown( + candidate.last_updated, + cooldown_days, + log, + file_path=args.file[0], + source_label=f"{source}:{candidate.tag}", + source=source, + ) + return candidate.version + elapsed = ( + (datetime.now(timezone.utc) - candidate.last_updated).days + if candidate.last_updated + else 0 + ) + log.debug( + f"Skipping {candidate.tag}: pushed {elapsed}d ago (cooldown: {cooldown_days}d)" + ) + + log.info( + f"All {len(candidates)} tag(s) within cooldown ({cooldown_days}d). Skipping." + ) + return None + + +# --------------------------------------------------------------------------- +# CLI +# --------------------------------------------------------------------------- + + +def parse_args(argv: list[str] | None = None) -> argparse.Namespace: + parser = argparse.ArgumentParser( + description="Update a pinned version in one or more files from an upstream source" + ) + parser.add_argument( + "--file", + action="append", + required=True, + type=lambda p: Path(p).absolute(), + help=( + "File to update; repeatable. Several files sharing one pin are resolved " + "with a single upstream lookup" + ), + ) + parser.add_argument( + "--source-type", + choices=["github-release", "docker-hub"], + default="github-release", + help="Where the upstream version comes from", + ) + parser.add_argument("--github-owner", help="Required for --source-type github-release") + parser.add_argument("--github-repo", help="Required for --source-type github-release") + parser.add_argument( + "--image", + help="Docker Hub repository, e.g. 'library/python'. Required for --source-type docker-hub", + ) + parser.add_argument( + "--tag-template", + default="{version}", + help="Tag shape with a {version} placeholder, e.g. '{version}-slim' (docker-hub only)", + ) + parser.add_argument( + "--version-line", + default="", + help=( + "Restrict to a release line, e.g. '1' or '3.13'. Use it when a major bump " + "means a different artifact rather than a newer one (docker-hub only)" + ), + ) + parser.add_argument( + "--tag-filter", + default=None, + help=( + "Override the server-side tag substring filter. Defaults to the longest " + "literal in --tag-template (docker-hub only)" + ), + ) + parser.add_argument( + "--search-pattern", + required=True, + help="Regex matching the currently-pinned line", + ) + parser.add_argument( + "--replacement-pattern", + required=True, + help="Replacement text with a {version} placeholder", + ) + parser.add_argument( + "--pin-level", + choices=["major", "minor", "patch"], + default="patch", + help="How many version segments the pin carries", + ) + parser.add_argument( + "--no-v-prefix", + action="store_false", + dest="v_prefix", + default=True, + help="The upstream version has no leading 'v'", + ) + parser.add_argument( + "--strip-v-in-replacement", + action="store_true", + default=False, + help="Drop the 'v' prefix when writing to the file, even though the source has one", + ) + parser.add_argument( + "--cooldown-days", + type=int, + default=7, + help=( + "Minimum age in days of an upstream release before adopting it. " + "Gives a supply chain compromise time to be detected first " + "(https://github.com/aquasecurity/trivy/discussions/10425)" + ), + ) + parser.add_argument( + "--no-downgrade", + action="store_true", + default=False, + help="Skip the replacement when the pinned version is already >= the candidate", + ) + parser.add_argument( + "--log-level", + default=os.environ.get("LABS_LOGLEVEL", "INFO"), + help="Python logging level", + ) + + args = parser.parse_args(argv) + + if args.source_type == "github-release": + missing = [ + flag + for flag, value in ( + ("--github-owner", args.github_owner), + ("--github-repo", args.github_repo), + ) + if not value + ] + if missing: + parser.error( + f"{', '.join(missing)} required when --source-type is github-release" + ) + elif args.source_type == "docker-hub": + if not args.image: + parser.error("--image is required when --source-type is docker-hub") + + if "{version}" not in args.replacement_pattern: + parser.error("--replacement-pattern must reference {version}") + + return args + + +def run(args: argparse.Namespace) -> None: + file_paths: list[Path] = args.file + + missing = [str(p) for p in file_paths if not p.exists()] + if missing: + raise FileNotFoundError(f"These files do not exist: {', '.join(missing)}") + + if args.source_type == "github-release": + _ensure_github_token() + + log.info(f"Fetching versions from {args.source_type}...") + if args.source_type == "github-release": + new_version = _select_github_release(args, args.cooldown_days) + else: + new_version = _select_docker_hub_tag(args, args.cooldown_days) + + if new_version is None: + return + + log.info(f"Selected version: {new_version}") + + if args.no_downgrade and _should_skip_due_to_downgrade( + file_paths, args.search_pattern, new_version + ): + return + + # Verify release integrity before writing anything: the tag must exist as a + # ref in the repo it claims to come from, and the commit it resolves to must + # be reachable from a branch there. An imposter commit pushed to a fork + # satisfies neither. + # https://github.com/aquasecurity/trivy/security/advisories/GHSA-69fq-xp46-6x23 + if args.source_type == "github-release": + result = github.verify_tag_commit_integrity( + owner=args.github_owner, repo=args.github_repo, tag=new_version + ) + log.info( + f"Verified: tag={new_version} commit={result['commit_sha']} " + f"branch={result['reachable_from_branch'] or '(none; see warning)'}" + ) + + replacement_version = new_version + if args.strip_v_in_replacement and replacement_version.startswith("v"): + replacement_version = replacement_version[1:] + log.info( + f"Stripping 'v' prefix for replacement: {new_version} -> {replacement_version}" + ) + + replacement = args.replacement_pattern.format(version=replacement_version) + + for file_path in file_paths: + contents = file_path.read_text(encoding="utf-8") + updated, count = re.subn( + args.search_pattern, replacement, contents, flags=re.MULTILINE + ) + if count == 0: + # A pin that stops matching is how an update silently becomes a + # no-op, so this is loud rather than a debug line. + log.warning( + f"No matches for pattern '{args.search_pattern}' in {file_path}" + ) + continue + if updated == contents: + log.info(f"{file_path} already pinned to {replacement_version}; no change") + continue + file_path.write_text(updated, encoding="utf-8") + log.info(f"Updated {file_path} to {replacement_version} ({count} replacement(s))") + + +def main() -> int: + args = parse_args() + logging.basicConfig( + level=args.log_level.upper(), format="%(levelname)s %(name)s: %(message)s" + ) + run(args) + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/.github/scripts/uv.lock b/.github/scripts/uv.lock new file mode 100644 index 0000000..85f150e --- /dev/null +++ b/.github/scripts/uv.lock @@ -0,0 +1,266 @@ +version = 1 +revision = 3 +requires-python = ">=3.11" + +[options] +exclude-newer = "0001-01-01T00:00:00Z" # This has no effect and is included for backwards compatibility when using relative exclude-newer values. +exclude-newer-span = "P7D" + +[[package]] +name = "certifi" +version = "2026.7.22" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a3/c2/24167ea9858356b47a87a50d39908bfdb72ceeefe0041586e704e5376b3a/certifi-2026.7.22.tar.gz", hash = "sha256:741e2c3b351ddf169a738da9f2c048608ff7f2c5cc02f1ebc6b118bb090d5d55", size = 138112, upload-time = "2026-07-22T03:35:12.644Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0b/a7/71ac2cff56fec219ed242bb11b8efb69fcc4bec75db06fb7bfe35de520e6/certifi-2026.7.22-py3-none-any.whl", hash = "sha256:62f22742b58a1a33014a2b6b706588a8d7e2a88ae7bd1a6ebe8c992928483775", size = 136983, upload-time = "2026-07-22T03:35:11.276Z" }, +] + +[[package]] +name = "charset-normalizer" +version = "3.5.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e5/3f/143b048436775b0f76ac3eec145c019e8173ccc2885c8f20319b996d5e83/charset_normalizer-3.5.1.tar.gz", hash = "sha256:6117b84ea48435e5356dc737f5121485c30920ba43375fa7b434fd753df0eac3", size = 171764, upload-time = "2026-08-15T08:20:44.807Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6a/b6/034f6802e9c3f6418966cfabb7db8c9252cc2429c5098f41cc43af804149/charset_normalizer-3.5.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:eda059b6bc8bc0812d626fd91a7ce01bf583df0a61296eff390fd94141a34e30", size = 363585, upload-time = "2026-08-15T08:16:46.646Z" }, + { url = "https://files.pythonhosted.org/packages/d5/fa/6a7e2a7c4b5451912b8c417732df79574354443592a88d616de03da66ae5/charset_normalizer-3.5.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:aa2bb0b37202dca27175591f761108b5d34096ade1191ffe4808bdf6b1571488", size = 251189, upload-time = "2026-08-15T08:16:48.287Z" }, + { url = "https://files.pythonhosted.org/packages/a4/c8/ab42b07cfd82e919f427fcfaa7c41abae8242833ad1aad66d42bae40b669/charset_normalizer-3.5.1-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0b2b1b3fa5670c127b246df1d0c059defd41f689a868a3b9d79df9b1cac42d22", size = 239724, upload-time = "2026-08-15T08:16:49.67Z" }, + { url = "https://files.pythonhosted.org/packages/e7/80/b9348b5d3041209f98b4cdad7655766369233f1d533f4f4f7558e9717bec/charset_normalizer-3.5.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6e5e4d73d588ca5ed09df1b7dcd1b203d1df3c542e3f50d126c947d432b10731", size = 280078, upload-time = "2026-08-15T08:16:51.228Z" }, + { url = "https://files.pythonhosted.org/packages/82/38/083a24028304bc85bb9e376fed801178423dcbb67495f73b6ea0624e1894/charset_normalizer-3.5.1-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b54e7e13267d49ffbfe68e25b3cbd774dab38fa37238f71265e91b36146eb21c", size = 276650, upload-time = "2026-08-15T08:16:52.625Z" }, + { url = "https://files.pythonhosted.org/packages/0d/35/731ac04aa0a097fc1c97f0994c375bdb230c6c96619db794208fe664e9ce/charset_normalizer-3.5.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c7b742bf31c88566b4bb6335a7f393bb322e580b6bb98df7bd0c25e6e3519ce8", size = 262325, upload-time = "2026-08-15T08:16:54.085Z" }, + { url = "https://files.pythonhosted.org/packages/f5/28/c2028e7021fb89c6e56868ed0e387b8e9aa811abdd2ab3208d6578d2c930/charset_normalizer-3.5.1-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6ba32c4d2abf1d2fe7cf27d280f4cca5664233b0f885549c7761719eb977f486", size = 261140, upload-time = "2026-08-15T08:16:55.604Z" }, + { url = "https://files.pythonhosted.org/packages/28/f0/0c0ceec6d98b7daa62e361e418135d59685811d79ba11529aad5cdf15e84/charset_normalizer-3.5.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0722590aabf9dc6a6c0343d523c05458fa2b5047dbe6302fd526bb570600753f", size = 252791, upload-time = "2026-08-15T08:16:57.103Z" }, + { url = "https://files.pythonhosted.org/packages/f0/3e/48f4cd187b1c33189d86039e9cbe4f92c05454175504b44ff81806d4d1bf/charset_normalizer-3.5.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:aa1099b956fb795e686d073568f6dc002a0bb89765ea6d5b055dd7d9bf1b116c", size = 240730, upload-time = "2026-08-15T08:16:58.418Z" }, + { url = "https://files.pythonhosted.org/packages/42/85/f9e22af69af67c54cce42be9455d9c81294f918b4ccc454db01f66efcac2/charset_normalizer-3.5.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:bd6c173f04743d483881bffa1478d5a4624475b8cd1d2194956a75548e191c18", size = 280791, upload-time = "2026-08-15T08:16:59.918Z" }, + { url = "https://files.pythonhosted.org/packages/fd/4c/9044135f42127630b6fa742feb51256353f6ab87a78f2fdd1de3de955a7f/charset_normalizer-3.5.1-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:f298e218441525d3794428b4c8b8fb8662c6d3ea79925d4807ee6b9a96a3bca5", size = 259598, upload-time = "2026-08-15T08:17:01.421Z" }, + { url = "https://files.pythonhosted.org/packages/ba/ed/1dd7cfebb4e75812934c49ca3b79757d11948053f7937ab7070c151f3c55/charset_normalizer-3.5.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:6e2912d4babbc65196ac13c2f53468dc57fb8b9c25ef913e8c59ddf7c6dc0e1b", size = 278217, upload-time = "2026-08-15T08:17:02.782Z" }, + { url = "https://files.pythonhosted.org/packages/bf/eb/239c84503cc9e3ba6eb34686a24bc66e84f3924efdd7e38e751a19f6bc10/charset_normalizer-3.5.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3d27167433c0d5f18dc850f07d0b3816221984fecdc405d6c157a6f0b8f8e9e6", size = 263417, upload-time = "2026-08-15T08:17:04.216Z" }, + { url = "https://files.pythonhosted.org/packages/37/ab/4e4510e1e288478e2c8333131d1c1382382ba8cd2165053c79e39d1da961/charset_normalizer-3.5.1-cp311-cp311-win32.whl", hash = "sha256:ac00177c4831ffa650f8609e4bdddd5fe09c03b1c0c47acece7e6ea20421598b", size = 181774, upload-time = "2026-08-15T08:17:05.58Z" }, + { url = "https://files.pythonhosted.org/packages/e3/57/32f0ccea59e8612057c61d6fd22ef2cb63cca93c9fe594094919696ac170/charset_normalizer-3.5.1-cp311-cp311-win_amd64.whl", hash = "sha256:f9b1e28d0e8dbfa858abdba91d6b547beaf2df1a59bec6da6faae7b96a4991a9", size = 206653, upload-time = "2026-08-15T08:17:07.075Z" }, + { url = "https://files.pythonhosted.org/packages/17/d4/b65c433fc521e58b5f54293982a5e51c05cb5f2dd3f1c7a6acb65b75324e/charset_normalizer-3.5.1-cp311-cp311-win_arm64.whl", hash = "sha256:ae31a1a1db2ee6cc2942fccaf695c934bc7f3db9f2133a3fef1f367cf1a4ab10", size = 185630, upload-time = "2026-08-15T08:17:08.502Z" }, + { url = "https://files.pythonhosted.org/packages/30/27/78873dc8b6a56357517b74b6bb9568b80450e7bb4f6ef7e3fa9d22aa0bd7/charset_normalizer-3.5.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:5b6d1386bf0096d26d3a863dc0a487a5b4eb9aa93cf5ba69683d29dde6b9d60f", size = 344456, upload-time = "2026-08-15T08:17:10.072Z" }, + { url = "https://files.pythonhosted.org/packages/9a/4c/be49ada26b1f0232d57aa89bbebf997a5cc2332a5616b6eca26ff680044d/charset_normalizer-3.5.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4582c27e8c889d64811987b5967fbd3ae0c823fe1fd933b543d55ac20bb475fa", size = 238530, upload-time = "2026-08-15T08:17:11.563Z" }, + { url = "https://files.pythonhosted.org/packages/76/84/6f1290fa07ae6978d3960caa3eb1b8019bf9284ab7c2297b00c099ef4250/charset_normalizer-3.5.1-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:1d1c7a53a6c2103925cdd6d7229f8c567379f211c869793df679f2e9f738c369", size = 230200, upload-time = "2026-08-15T08:17:12.919Z" }, + { url = "https://files.pythonhosted.org/packages/e7/a0/47b18adeed31c8f16ba9700f32c1b18594cfa09f47eb672a488c273c22bf/charset_normalizer-3.5.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e6621fb2a4988d6e53eedc455e5903e2679f3967b8acb3d639f1b63c14a2e893", size = 262222, upload-time = "2026-08-15T08:17:14.571Z" }, + { url = "https://files.pythonhosted.org/packages/38/fe/341861ac118dae06f3ec0eb487488af52128f2ef2faf0b11003944d22259/charset_normalizer-3.5.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7c0c10730342b0c9b35dd1d619beb8214e520bd96a1f870f452680b238aab3e0", size = 258951, upload-time = "2026-08-15T08:17:16.158Z" }, + { url = "https://files.pythonhosted.org/packages/6f/89/bb5108dc6c3651dca963f2b0a3ba19bbcb370c94e1b6d3e0e844a58e6dca/charset_normalizer-3.5.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b9af956078716df40d985fb0dfeb2c2120c5ca92ba4ff4b388acfd01cdc14d08", size = 248801, upload-time = "2026-08-15T08:17:17.683Z" }, + { url = "https://files.pythonhosted.org/packages/b1/ba/ef83ae3aca816393decfa3530976f38a79812d707b80b580ac33b83f9877/charset_normalizer-3.5.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f9f8405c2c758532c74fed975dbee57be1f31a6e865c031870c79a6ed3212ada", size = 244070, upload-time = "2026-08-15T08:17:19.191Z" }, + { url = "https://files.pythonhosted.org/packages/f6/0b/c5292a2462d69b7378ea89793bbb5b2b6fcf6f7dd6d1667f9619094ad553/charset_normalizer-3.5.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:96fef3e886d6a9874b14f27fc193fbdc69d5d8035783d86aa4e1cea594e695f9", size = 240110, upload-time = "2026-08-15T08:17:20.547Z" }, + { url = "https://files.pythonhosted.org/packages/46/22/111e5be3b740d5c2a5bfcedb3d237b6591e5c2e82ae9d6ffcb121fe0909c/charset_normalizer-3.5.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:5d8531a6569d025f68e2321e7638fb7978f23db58e5f69f56913837aae03816e", size = 232836, upload-time = "2026-08-15T08:17:21.895Z" }, + { url = "https://files.pythonhosted.org/packages/f9/d2/d2aad6fe0dbb44b194bf3becb60f5a0ac48446ade999a47fe7bb41eb09a7/charset_normalizer-3.5.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:aae2ee51122d3ae968a3837d97dc24a0aeebb0dea23694422cd172bd30017cd6", size = 262712, upload-time = "2026-08-15T08:17:23.727Z" }, + { url = "https://files.pythonhosted.org/packages/35/5a/337e4663a5eae6de99db940ee8066d4145caafb61327db62deda15313cce/charset_normalizer-3.5.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:7235dc28fc6dd9d832ac7c7bce95367dedb85929f17368a0c2bee1e080b9acbf", size = 242977, upload-time = "2026-08-15T08:17:25.157Z" }, + { url = "https://files.pythonhosted.org/packages/ca/85/f82f8a92e31c7519410e2e1afdc630f28ec47490ce2c09a11c1a43cbb459/charset_normalizer-3.5.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:4abdc5f9ad448c1ecbfae2974b820535d6bc6e7eef63babbab3d81cf46968c71", size = 260207, upload-time = "2026-08-15T08:17:26.602Z" }, + { url = "https://files.pythonhosted.org/packages/b7/52/643d11ffd60e9ac2fd1fb87e167a19285b9eefeff4a40e63c87cbfbeab36/charset_normalizer-3.5.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ba501e667c17d8411f98e67a022d9604ef179aff0e459b7e292c796837c13573", size = 250562, upload-time = "2026-08-15T08:17:27.971Z" }, + { url = "https://files.pythonhosted.org/packages/62/16/46556278c2168d12df9da7fede5dc6fc70e60301b26a82bbeec238c9cfe3/charset_normalizer-3.5.1-cp312-cp312-win32.whl", hash = "sha256:cfa1c0cc3a8f9f53f1243a5a99ac36fd003880199383b37672e86ddda9cb07e2", size = 178507, upload-time = "2026-08-15T08:17:29.277Z" }, + { url = "https://files.pythonhosted.org/packages/9d/7a/4c6c298171e6b3e745633180ff59350fc0ca0db1ffd28df1e369e0579f71/charset_normalizer-3.5.1-cp312-cp312-win_amd64.whl", hash = "sha256:3617ac3cfd8b9888f145ad89dd6e692285834b0201c6074a5eeaad3fd4d668c2", size = 200551, upload-time = "2026-08-15T08:17:30.668Z" }, + { url = "https://files.pythonhosted.org/packages/cd/d7/eb95a042f0dd22e304b0b6472b154f3546a1a039a9ee89ccb2a7f61591fc/charset_normalizer-3.5.1-cp312-cp312-win_arm64.whl", hash = "sha256:88e85ab89cb822c1e635f51d6d32e488f94e002e70e2f492bdb8b945543f345a", size = 180700, upload-time = "2026-08-15T08:17:32.028Z" }, + { url = "https://files.pythonhosted.org/packages/bc/61/2cb6ad133dbbb449fa2d37ccae973232f4827e799af258d15e589a3d1e9e/charset_normalizer-3.5.1-cp313-cp313-android_24_arm64_v8a.whl", hash = "sha256:4f298bdadb8f0b9e5672877f647d1be9373ef5320c9e2f049795e26cad28b6a9", size = 211584, upload-time = "2026-08-15T08:17:33.597Z" }, + { url = "https://files.pythonhosted.org/packages/18/57/a305c968be1ca13f3dd1b32f445877e97addf55d80b65c7cb35fac82b777/charset_normalizer-3.5.1-cp313-cp313-android_24_x86_64.whl", hash = "sha256:88ca277405c2d3b71c4e1c2ee0e7966e807bcba86a69d11e19ba199d18ae4491", size = 223359, upload-time = "2026-08-15T08:17:35.022Z" }, + { url = "https://files.pythonhosted.org/packages/09/0a/d3646670292ce8d8f8cc11ac067d44885e697a5591f57a9221128da5e7b3/charset_normalizer-3.5.1-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:9362dd90aa7dab48c0054a21187791ccf05473f7dba5d92b8033ae62164675e7", size = 194464, upload-time = "2026-08-15T08:17:36.452Z" }, + { url = "https://files.pythonhosted.org/packages/de/93/d51ec556e01042fed6f993ea859311bc7917b466684182fbbceb6ca24762/charset_normalizer-3.5.1-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:977cdbd483a9cff38179bea4fd754289a6f2195c7abd414aba85410b3e66cc5e", size = 197676, upload-time = "2026-08-15T08:17:37.819Z" }, + { url = "https://files.pythonhosted.org/packages/a4/a0/562247944386f7d4ef94467e84876600cc1e0f1b93239aaa9213d2bc3cbd/charset_normalizer-3.5.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e90251c0c7bdd54a100a0dce3c07b7e637278c93af29dbf78ebb89a58c4bac7d", size = 340473, upload-time = "2026-08-15T08:17:39.303Z" }, + { url = "https://files.pythonhosted.org/packages/31/e7/1d994be1b93d41e9502b8b0460eaa88a1dd8df335df415db87d6c3e91ab2/charset_normalizer-3.5.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:94d78ecec2605a8d0398b0f365d5f12a63248438516f5dac536a5eff7337df4a", size = 240156, upload-time = "2026-08-15T08:17:40.66Z" }, + { url = "https://files.pythonhosted.org/packages/09/53/27923ce5cc6cbccb832037b27dca98882d9c53e9b69e866bbbef4aae7fc8/charset_normalizer-3.5.1-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d59b75732e9b6f27388e10c14b0259cc5f2e48c78627d185e6a177b58ad3cffe", size = 228246, upload-time = "2026-08-15T08:17:42.003Z" }, + { url = "https://files.pythonhosted.org/packages/ce/48/5a97e84d63af1d55c07439cb80e56d99a8efb4295700eb4e18c0d1615d2c/charset_normalizer-3.5.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0d929fc574b4d6fd9e7c0f5c2ede8716a41911923aa7fa5fce38e0818aa4a1ac", size = 263660, upload-time = "2026-08-15T08:17:43.627Z" }, + { url = "https://files.pythonhosted.org/packages/7a/c2/071575791dcc88316c0a9a65ce38897a82e4cfe4a325f0f7fe1b1ac47bcf/charset_normalizer-3.5.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:394fea06235c8543390050ed5f529187074b029fb027213f6c46ac11ab5d950e", size = 260354, upload-time = "2026-08-15T08:17:45.094Z" }, + { url = "https://files.pythonhosted.org/packages/fb/af/63240b0c0248c075c2535a1f1bd992821d8251b9f173abc13329661d09e4/charset_normalizer-3.5.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:62b55f6722735a6c472f88361cde6640608773d9443cebdbb51abf436a1fcdd3", size = 250638, upload-time = "2026-08-15T08:17:46.496Z" }, + { url = "https://files.pythonhosted.org/packages/4d/66/70dfad64f15be09c15ccfee81330a7e515895dbe296dd23114e9a231268a/charset_normalizer-3.5.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fa48b1b63d639f9483e0633e092f5851e2348c352f1f9bb6c8182f87884ef876", size = 244583, upload-time = "2026-08-15T08:17:47.963Z" }, + { url = "https://files.pythonhosted.org/packages/c0/24/ef36367d38b9ddd4bccbf72888c342e8de1f5ae506fa0b2dcf970e2732a1/charset_normalizer-3.5.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c71fb0d56c920c269cd3e2e3fe7c610e3f1fdb21a6ce60efa6430ff63676cea6", size = 242038, upload-time = "2026-08-15T08:17:49.481Z" }, + { url = "https://files.pythonhosted.org/packages/db/ab/55e683ba0fff2e43adafc10daa3001eac90fdaa419a97227d5a7067eedde/charset_normalizer-3.5.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:485a0d363cafefcd2538a73c7c838daa2035f09b2c9f9b5e3133f80c6aeb84c2", size = 233677, upload-time = "2026-08-15T08:17:50.845Z" }, + { url = "https://files.pythonhosted.org/packages/bd/67/0f40eaf8d1b6e7cf15e82382a2965efaca787fc1c2794b7021d37aaf5036/charset_normalizer-3.5.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:5c0ea61a470e070686aa30892fed79e297d2c8d0ab46b8bcdf027d38c51da591", size = 264491, upload-time = "2026-08-15T08:17:52.61Z" }, + { url = "https://files.pythonhosted.org/packages/5c/64/12b4c2a11ee8df4fcc518c78b0d93e3a92bd3d5253d1617ce74ff0e8c7ef/charset_normalizer-3.5.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:90b7481fb62fbe172c558bc6fd1c4c98d82004a54a7551f20e11ac9bf0b8708c", size = 245196, upload-time = "2026-08-15T08:17:54.023Z" }, + { url = "https://files.pythonhosted.org/packages/37/2e/651d910af6d0fba325eee1cda37ec5443462ed25360e666c144166eb6091/charset_normalizer-3.5.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:35fe081843b35aad20ffeccec3eeffbe637b15d14f3fb22cc1b59cd8ec17e93c", size = 261660, upload-time = "2026-08-15T08:17:55.491Z" }, + { url = "https://files.pythonhosted.org/packages/90/c6/b09e05e6db7f64338e0dc067c79577b1138da86c1e38369096851d96be88/charset_normalizer-3.5.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fd0350afdc3aabd5576f60ea109228bd5538139713c7b094c5cd27c73a98bc6f", size = 252618, upload-time = "2026-08-15T08:17:57.025Z" }, + { url = "https://files.pythonhosted.org/packages/76/4e/362d4f9fdcdf5556fb2aa3ce7d4a58ebce03ed1ff03aa1d9aca8d02f13f3/charset_normalizer-3.5.1-cp313-cp313-pyemscripten_2025_0_wasm32.whl", hash = "sha256:9d9a0dc7cbe9bec24c3f767c9122c41fe5a1bc43f47cd099d00d393e09769de4", size = 140362, upload-time = "2026-08-15T08:17:58.425Z" }, + { url = "https://files.pythonhosted.org/packages/b4/d4/703be739b26acce318bd29eb3b25b7209e1b1f527f9eae3d1f1f01fdde2b/charset_normalizer-3.5.1-cp313-cp313-win32.whl", hash = "sha256:d63600d620ad0064c3a748b950ac5ea38a80190e5498532efefa4b7b3f1da1f3", size = 177755, upload-time = "2026-08-15T08:18:00.037Z" }, + { url = "https://files.pythonhosted.org/packages/8a/33/56d97ade41c8db611e727168c52ae46c9224c362ec28d4b65d7e9869e8da/charset_normalizer-3.5.1-cp313-cp313-win_amd64.whl", hash = "sha256:aea996a6aba25260827c9ea511d1addfde2da9eb686ac961838509086188b7e6", size = 199295, upload-time = "2026-08-15T08:18:01.506Z" }, + { url = "https://files.pythonhosted.org/packages/5b/75/5b20dd1e6573a01a08158fe104104fa2c8abf941745596954185726cd46c/charset_normalizer-3.5.1-cp313-cp313-win_arm64.whl", hash = "sha256:fd0a274c0e5f9a21565cd9d3dd749b61f96b7aa1e20a93aa1ba4029518f2e5c0", size = 179856, upload-time = "2026-08-15T08:18:02.929Z" }, + { url = "https://files.pythonhosted.org/packages/29/cd/2b812ce5e888f1ce69a5350281e58aab07ae64a958ecae8912f30865718e/charset_normalizer-3.5.1-cp314-cp314-android_24_arm64_v8a.whl", hash = "sha256:774d157f112367ff4abd29019f38f023c24e00e56edc7829c20e358a5a913ad8", size = 212318, upload-time = "2026-08-15T08:18:04.403Z" }, + { url = "https://files.pythonhosted.org/packages/9e/4a/a6ee107430768a5334e6d63f31f148a04a1a491ef161a1ac9415a73f2fa8/charset_normalizer-3.5.1-cp314-cp314-android_24_x86_64.whl", hash = "sha256:26422d45fd13551cf564c58932f7d72b4f58b93b0fcf18c35ba6be12b46bb102", size = 224897, upload-time = "2026-08-15T08:18:05.997Z" }, + { url = "https://files.pythonhosted.org/packages/c3/d9/35ae3f64f29d0179c35c3baefe575904df2913dde519129c7f75995a2b1d/charset_normalizer-3.5.1-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:09a7bba9f739468c8e78c36a75c33768e53cb1959fc638f510454c14683f00d5", size = 194848, upload-time = "2026-08-15T08:18:07.397Z" }, + { url = "https://files.pythonhosted.org/packages/74/76/f2fc7380f056cc273a53af37f50d08ad54b2c59f61078f31432edcf1c2bd/charset_normalizer-3.5.1-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:4c9548dc78002099910abaebc0a72ac58b7d30931869e0351c09b507dff4ece3", size = 198163, upload-time = "2026-08-15T08:18:08.989Z" }, + { url = "https://files.pythonhosted.org/packages/e9/40/095ce62fa078483cccc1fa2b36e6bc9580b85422a20ee9f925341c50e44f/charset_normalizer-3.5.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:c428c6c31eb5f4277d7f8eccaf767fbd548ddd5ce3c8b4f4cbbfab3d96b5904c", size = 341823, upload-time = "2026-08-15T08:18:10.458Z" }, + { url = "https://files.pythonhosted.org/packages/f1/5a/0e58b1c04a1596e0256f407274a92d5fb2ee21324409d1fab1da48a65b5b/charset_normalizer-3.5.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2f06b7eae9dbe77fe1d644ca244dad508de8d302870a43f3c559b521270938a0", size = 242458, upload-time = "2026-08-15T08:18:11.989Z" }, + { url = "https://files.pythonhosted.org/packages/22/95/b4618ce912e6db0b1aae89ba788e38e8a7eba0f3025cc66e8c0699f977b2/charset_normalizer-3.5.1-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6b7430cf5728e68f6c462254009a6ef4086e1bea43cf2f57aa9c55fb4f50ff96", size = 226717, upload-time = "2026-08-15T08:18:13.401Z" }, + { url = "https://files.pythonhosted.org/packages/8a/76/c681192bbda3d55356db5dadd64381d5202b37c6b598fcda5282e88b5d3d/charset_normalizer-3.5.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ab743e9bc90c1f73552ec33e10e3331315acd2c397b36065b591b0181de533cc", size = 266111, upload-time = "2026-08-15T08:18:14.961Z" }, + { url = "https://files.pythonhosted.org/packages/88/be/55127bfca72c0cff6c022488d140d7c5b04c771e3b72e9bdb4836d54979d/charset_normalizer-3.5.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f6f7deae3feb4edfa2efaf7c574fe88cbf055038a6abdb40188e4fff66d5699f", size = 263128, upload-time = "2026-08-15T08:18:16.515Z" }, + { url = "https://files.pythonhosted.org/packages/e0/91/39c3af510b0aa32bbda03374259200f28430febfd1bf5e511fe765282ce5/charset_normalizer-3.5.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:15f024313246a4ed976c60f440bb8d257815513a681d212ff74fd46f7d715a90", size = 251240, upload-time = "2026-08-15T08:18:18.127Z" }, + { url = "https://files.pythonhosted.org/packages/1c/a5/cbe418bbc6ecdfc3e05a0116002897c4b403a5e838d697e64c78e9f0190d/charset_normalizer-3.5.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:823f82903d189af463d7df250ef1f7f696f3cee08cc8d91deb565e8d425f6506", size = 245282, upload-time = "2026-08-15T08:18:19.625Z" }, + { url = "https://files.pythonhosted.org/packages/cc/a4/689bb42e8e7cd492f3cb64907c6bc00ad247ec9a3628cd3f8eed126e8ae1/charset_normalizer-3.5.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:01e93745f7f219b703b60ba7afead36cfc4242782be5af484673fc500df12da5", size = 244597, upload-time = "2026-08-15T08:18:21.121Z" }, + { url = "https://files.pythonhosted.org/packages/c1/ce/9962938e179cf9f699d3f1e7b3114b5d7642dee6a893745229f9dd04f274/charset_normalizer-3.5.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:329fc3ccb63ad22d867d84c2adea759a64079a37ba4a343433b02c7a2816871e", size = 231376, upload-time = "2026-08-15T08:18:22.57Z" }, + { url = "https://files.pythonhosted.org/packages/85/54/46000450ada53bd9eac5429a2c8c54cd2d9b39c0c255f229aea9af0948a5/charset_normalizer-3.5.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:bb57753e36e4855b8ca375069482250a6246372331a3e4f3407eaebb007443f5", size = 266715, upload-time = "2026-08-15T08:18:24.235Z" }, + { url = "https://files.pythonhosted.org/packages/3d/bb/618749d70f792b44252a777bf89bfb86823b9bbc1ea13fe8ce759b07f38a/charset_normalizer-3.5.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:fce8cbd4997efeb450bd298b54f755dcdff18d496f7a5ddbb4867c6d7c88fdc3", size = 245848, upload-time = "2026-08-15T08:18:25.726Z" }, + { url = "https://files.pythonhosted.org/packages/7e/3f/ffb64458527c7668031d5eb095d978de561958dc9f5b53f8e488a533e603/charset_normalizer-3.5.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:6c9cdde8becb25a7fde49924511aa2644d6f8081cc8df8e9452724303348d8e3", size = 264521, upload-time = "2026-08-15T08:18:27.193Z" }, + { url = "https://files.pythonhosted.org/packages/4f/ab/74a55fd803916a35ac461daf002708191aac19b546b80dc8cabfedc63d98/charset_normalizer-3.5.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:9ac4444d8d4fd4c4bd08bf451ed3167aa9e7ec6cdb41b648794f1d1103652e36", size = 253054, upload-time = "2026-08-15T08:18:28.568Z" }, + { url = "https://files.pythonhosted.org/packages/a0/2a/6a9034b7d3c60b17499afb482df5878bf9fa20b50cc3887d5ef017a833db/charset_normalizer-3.5.1-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:f03ac127268b43ef4fe9e6ab6794a6794b49485a0cc0c1db79876d2f33f75bc7", size = 140580, upload-time = "2026-08-15T08:18:30.214Z" }, + { url = "https://files.pythonhosted.org/packages/f3/46/1d362e1a00d035d66b9869e1281eee115907f7e390a16a07824ab5737360/charset_normalizer-3.5.1-cp314-cp314-win32.whl", hash = "sha256:1f5883d77fd409a261abb5dc8ccbe335720d798b1de4abb3b1d47ccbbc76b53b", size = 180325, upload-time = "2026-08-15T08:18:31.877Z" }, + { url = "https://files.pythonhosted.org/packages/7a/7c/4938c329b6a9d446f6a59aa2092ff7118f274209b5ed0e26893d1d30a63c/charset_normalizer-3.5.1-cp314-cp314-win_amd64.whl", hash = "sha256:c658c50ac0c98cd755a2dd50b7977d3bca7df401dcc47fbdfa87db53ef7d4e8b", size = 204175, upload-time = "2026-08-15T08:18:33.466Z" }, + { url = "https://files.pythonhosted.org/packages/ac/33/eeb384dbd8dec570661354592f4f2e1b2fcc92585624d146a000caf53841/charset_normalizer-3.5.1-cp314-cp314-win_arm64.whl", hash = "sha256:4bea7f8ebe90bbd7f0e4a2de42ca6924ba23e3e76418c408ff82f1d46fabd687", size = 184123, upload-time = "2026-08-15T08:18:34.913Z" }, + { url = "https://files.pythonhosted.org/packages/1c/6c/c73fa9d5a85f6ab05395de61c5f6984e0a9ff40bb5ff888d46dff02526c6/charset_normalizer-3.5.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:fbc597639158fd7c14d55e808718848319540f51b0e6746e3eefa59723a4a348", size = 381682, upload-time = "2026-08-15T08:18:36.349Z" }, + { url = "https://files.pythonhosted.org/packages/30/c7/63565f860921457feba93bae6c86fb7746deb4cffeed2f375cb845318146/charset_normalizer-3.5.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e71c909f353863b2b89c83de2ebed71ea6d0df8a6ef65a128193c5e650766bef", size = 240826, upload-time = "2026-08-15T08:18:37.887Z" }, + { url = "https://files.pythonhosted.org/packages/06/ae/7ae8807410dfa33f8e6f1715740adeaafa8a816cc4cb33508f54b1f7c896/charset_normalizer-3.5.1-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:7ac76cf9afd34929d76eb7fcb63be476a4853d8a96f0dcf2d0db68a0cbdf9885", size = 227861, upload-time = "2026-08-15T08:18:39.315Z" }, + { url = "https://files.pythonhosted.org/packages/e9/a3/887c1642f0da26000b0e0652d91071113c0e72cea33952e225cf589f49a9/charset_normalizer-3.5.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a3a370082ce34d0612f421e15fe011c53bb1feff21a26d06ad4fb244dab5a375", size = 260758, upload-time = "2026-08-15T08:18:40.88Z" }, + { url = "https://files.pythonhosted.org/packages/3e/11/e6f5b9a3d0e55b0ef7505cd3765cdd48f22db89994c947b316f52f801fd8/charset_normalizer-3.5.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:256dd4d85d9e4dc595e2bc983c980e73f62ddeb3165c58b4c3dfe78c5c8548c1", size = 259950, upload-time = "2026-08-15T08:18:42.351Z" }, + { url = "https://files.pythonhosted.org/packages/1b/ee/e4e10a94d51cd1ee638aa7e00b65399e6b2a4e8376ab6d2eac9f95586671/charset_normalizer-3.5.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:58d4aa13a59c969dbfdf9e6a9560e242cbfd9e8a8f50c2747714df1a423adf65", size = 249329, upload-time = "2026-08-15T08:18:43.914Z" }, + { url = "https://files.pythonhosted.org/packages/c4/25/d5f4198819e6059735a84e8d0bfb72dc33976da67b97adcd3fb5a5e07ec6/charset_normalizer-3.5.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0c6dfb5ca6723eeed15aa8e564a014d69fcb8812f94eef11fe3631e0508199f5", size = 243137, upload-time = "2026-08-15T08:18:45.368Z" }, + { url = "https://files.pythonhosted.org/packages/a5/e9/e925ca7569cf9fb9701fd82503fee73eea5268fdb856bdd64947092d3daa/charset_normalizer-3.5.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c010f5581d9c612804cc59fcf7b524b707fbcb72828551237ab545bb5c7034af", size = 242820, upload-time = "2026-08-15T08:18:46.842Z" }, + { url = "https://files.pythonhosted.org/packages/34/17/672c251a888ed2aebcdd2fe830ad0104e25ff83c43f5c4f9c15e9fc6853c/charset_normalizer-3.5.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:52ec005752a56ae79547a05c0139ca2501a0c866390b6115008456b9f0e7cde1", size = 230504, upload-time = "2026-08-15T08:18:48.353Z" }, + { url = "https://files.pythonhosted.org/packages/3f/fc/f6a85abebd42ce4da2f1db0aa56cc6a0df1995e318b3875d14401b8381d1/charset_normalizer-3.5.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:2bced4061f000f7187254a02ad3433ae17eaf991747ceea2f478422590a5bba9", size = 263087, upload-time = "2026-08-15T08:18:49.859Z" }, + { url = "https://files.pythonhosted.org/packages/98/66/7c42677e739ba66746b297e2046918d793078094dc239e1e72768cffccc6/charset_normalizer-3.5.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:9eea3ab2597a5e65fe65296e2d6a84570845a6b55532d90333d740d48bbc850a", size = 243269, upload-time = "2026-08-15T08:18:51.601Z" }, + { url = "https://files.pythonhosted.org/packages/de/d8/a50b79237f417af10f8c2a501ce8d1ca87829a22e69117891ca4ba20a69e/charset_normalizer-3.5.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:496846868fea80e479324862fa877f02411f2fd0f83b79ccee2607aa68b2a032", size = 258766, upload-time = "2026-08-15T08:18:53.23Z" }, + { url = "https://files.pythonhosted.org/packages/2e/1d/0fc91aeaeb3c83b748f532399ce67cf84604b48297405d740000f7a9e786/charset_normalizer-3.5.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:85d5855daafc240cc045c026d7a15fd198a09b0fc8ff6f5ecbb5297b509cb11e", size = 250814, upload-time = "2026-08-15T08:18:54.768Z" }, + { url = "https://files.pythonhosted.org/packages/ae/10/3d8c777cf9024615295aa1b808324ad5b4a77855869c00824bad74ffaf8a/charset_normalizer-3.5.1-cp314-cp314t-win32.whl", hash = "sha256:58d3e12c88e0950bca850ae1f7c256055c097639c2edb9eb123af9807d8b15e4", size = 191074, upload-time = "2026-08-15T08:18:56.305Z" }, + { url = "https://files.pythonhosted.org/packages/4d/81/ae557d3c44d1a1d688696d60563413a0866a91b7ebc50f20df838be3d8c8/charset_normalizer-3.5.1-cp314-cp314t-win_amd64.whl", hash = "sha256:acaf604462bf330b0d07e7a07c1d6e4adac79e5fb13e9c5140590542cafacc00", size = 216476, upload-time = "2026-08-15T08:18:57.889Z" }, + { url = "https://files.pythonhosted.org/packages/27/e9/61c01fb8b804692569c036b3fc50495814502dcf13a60649c6055390b02c/charset_normalizer-3.5.1-cp314-cp314t-win_arm64.whl", hash = "sha256:fdb8a068947befafba9952162645dc2fecaeb400e64584829ed5e9b2fbe21a7f", size = 194115, upload-time = "2026-08-15T08:18:59.418Z" }, + { url = "https://files.pythonhosted.org/packages/4a/4e/8544831ef59d8f27ce92c80871380fdacc8076a8a56ed62f82e54f991333/charset_normalizer-3.5.1-cp315-cp315-macosx_10_15_universal2.whl", hash = "sha256:9085f87b0e38a2b92b8923059b4e8789fe40d9279712d15dcc670048d77079af", size = 342048, upload-time = "2026-08-15T08:19:01.054Z" }, + { url = "https://files.pythonhosted.org/packages/7f/a6/e3b46852424246065355644f4fb6dbccc0239a42a2eee27ecfc8957f0bcd/charset_normalizer-3.5.1-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2679de311c7946dde5d3b6f44941844133ff5c7cb86099c0061ab1e8901c20a8", size = 242997, upload-time = "2026-08-15T08:19:02.492Z" }, + { url = "https://files.pythonhosted.org/packages/03/3b/0cc9a26777334ab2f2e3089b948bbf4e4fe72ea70b897715ef6415043ec8/charset_normalizer-3.5.1-cp315-cp315-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:baf3775a2635e5a11fbd5e4e64ee69c7e86875d224a5c72aca4c141064589a90", size = 237014, upload-time = "2026-08-15T08:19:03.943Z" }, + { url = "https://files.pythonhosted.org/packages/8c/c2/027335f0aa337a2a2e121bac1ad88c4f02ba6053ea0926802784f3db11af/charset_normalizer-3.5.1-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8ac8c94b6539074e0f40899301273ac8402b9b3e01c7b7ba269ff30340aaaf20", size = 266174, upload-time = "2026-08-15T08:19:05.598Z" }, + { url = "https://files.pythonhosted.org/packages/86/d3/e367787febe4e74769dec0f406f2c3c8d1b955fce5aee1fd0f94e8367a45/charset_normalizer-3.5.1-cp315-cp315-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8fe532b3c966d1fb794e0698e4589d0444017ae77fc0b31edea13c0e35bcc449", size = 263361, upload-time = "2026-08-15T08:19:07.251Z" }, + { url = "https://files.pythonhosted.org/packages/af/3d/391b193eb9f3e84b02f9314088c386debdc0debee843535aaea2e2c6715d/charset_normalizer-3.5.1-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5c84bec0ab5ae0c64bfe73a7d2adcb5ce73b467523fc27fd6a28ab2aa6cbe35a", size = 252143, upload-time = "2026-08-15T08:19:08.816Z" }, + { url = "https://files.pythonhosted.org/packages/2e/57/de221f1745a90d418199761967e2776bfe2c275a1194220985e8c1d37833/charset_normalizer-3.5.1-cp315-cp315-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:854066be00447fa8de2ccbbe893e2ffc4b123ef16d897af794c1e18bd4a714b0", size = 252086, upload-time = "2026-08-15T08:19:10.255Z" }, + { url = "https://files.pythonhosted.org/packages/c8/e3/d119f86a01f9331e8186175f24873b1d74a7ee9e2e4b4d68f9947dae5afd/charset_normalizer-3.5.1-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:21b82d8082f6f5e7f456ef0bd16323d08de1266efbfeb476e64b2a91d1471a4e", size = 245231, upload-time = "2026-08-15T08:19:11.807Z" }, + { url = "https://files.pythonhosted.org/packages/26/de/d8e48c135ae480879539cdb179c8d3b50c7879497d75dd899b5763b69cee/charset_normalizer-3.5.1-cp315-cp315-musllinux_1_2_armv7l.whl", hash = "sha256:838648accb3a7fd9803fd45c87bce8509648eb0c11bc34e216141300977244f2", size = 241546, upload-time = "2026-08-15T08:19:13.416Z" }, + { url = "https://files.pythonhosted.org/packages/67/c4/217755fd1abc50d326c252922cd642002758095a81ff45010337b8b3ef65/charset_normalizer-3.5.1-cp315-cp315-musllinux_1_2_ppc64le.whl", hash = "sha256:195ce897c6153c0700078142cf8efe3e6454ca4cf4357499e4078dfd83396626", size = 267033, upload-time = "2026-08-15T08:19:14.981Z" }, + { url = "https://files.pythonhosted.org/packages/b8/d7/34d8e404e358d2adcc5a228c2134643af00104c8fb0bf525f3688d756f05/charset_normalizer-3.5.1-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:978eab16f55b4ab2c2a745be9a0a840bf8f09a7f227d9c76eb30214d078865a5", size = 252045, upload-time = "2026-08-15T08:19:16.618Z" }, + { url = "https://files.pythonhosted.org/packages/5e/fa/40414471acf0aa0692ca77305aa00e434fcd8288f0941c93c30e9a5f8f2f/charset_normalizer-3.5.1-cp315-cp315-musllinux_1_2_s390x.whl", hash = "sha256:cc0329df4caaceb950d2f580b5ac716a377f7059624a0bafaeaf8a218c6ed774", size = 264866, upload-time = "2026-08-15T08:19:18.101Z" }, + { url = "https://files.pythonhosted.org/packages/32/90/fcc850bae791abd2e0c041847f13e270aa08692a79f3e00de6d2dce1cb50/charset_normalizer-3.5.1-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:687c9ca3035544b113bea2055e180af96fb63c0c476e22a9180f51925186e7b7", size = 253932, upload-time = "2026-08-15T08:19:19.734Z" }, + { url = "https://files.pythonhosted.org/packages/af/af/53afe99068b3c10b4cbae592a52ef72a7c92c0188440e83ee3a078fd8f75/charset_normalizer-3.5.1-cp315-cp315-win32.whl", hash = "sha256:706bfd38730a5ac7a365793269a00f4e988178cec121391f4248d84ad8c972e9", size = 180320, upload-time = "2026-08-15T08:19:21.37Z" }, + { url = "https://files.pythonhosted.org/packages/c9/bc/f46a132041b29e4a8779ed712d3df1bf112e94ca8de58b66d7ec2c0cf8b9/charset_normalizer-3.5.1-cp315-cp315-win_amd64.whl", hash = "sha256:92caef967d287a407085d61176fce4012b1dd62daed4eb6d5ceb26d3d2538712", size = 204174, upload-time = "2026-08-15T08:19:23.088Z" }, + { url = "https://files.pythonhosted.org/packages/a1/5d/9ed554480eda8e447b673648628fdc29574d23dbad01fe11837adedd1cae/charset_normalizer-3.5.1-cp315-cp315-win_arm64.whl", hash = "sha256:5fc45d653ea8c9a20479167e11d4a0f8cb2fa3470737ab6f9c827532313187b7", size = 184126, upload-time = "2026-08-15T08:19:24.471Z" }, + { url = "https://files.pythonhosted.org/packages/3b/32/9b8929bf384061ee1fe5d9c27c6f9776d3d824039ad4e14c88ec00c7808e/charset_normalizer-3.5.1-cp315-cp315t-macosx_10_15_universal2.whl", hash = "sha256:59171c6e45bf07d0d5cab3b0bf81d945035530f6873398b3b531c31184d46663", size = 381441, upload-time = "2026-08-15T08:19:26.038Z" }, + { url = "https://files.pythonhosted.org/packages/96/10/e9aa7923d3ddac652c99a1c5f7be494e737e151566a44abe018daf757f2c/charset_normalizer-3.5.1-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9dbdd9205662134957cf0c324f639bdc5031c0ca056e2369e238db75187c0f11", size = 241742, upload-time = "2026-08-15T08:19:27.532Z" }, + { url = "https://files.pythonhosted.org/packages/28/53/a2d249ebddf47b889a100c0bdcb61a2f9dbb8bc24ef325cc062e4f476877/charset_normalizer-3.5.1-cp315-cp315t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e4b018dc5a0eee4676e38fe84a47a427816c590b93b55d9025274ec4d6ffc2dc", size = 235298, upload-time = "2026-08-15T08:19:29.274Z" }, + { url = "https://files.pythonhosted.org/packages/7d/07/469f78af590f7d5cd48e20d8dbfa3d66deeff9ba37768c04d886b5afd45c/charset_normalizer-3.5.1-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ced3fdd71aaa83ce593746c2edb42b7a59cb4c19c8b5c407781c72e493aae55a", size = 262500, upload-time = "2026-08-15T08:19:30.955Z" }, + { url = "https://files.pythonhosted.org/packages/55/66/3bb56a47f7dcba014055b1a1d33c6f08bbe9c1e74dba154cfa25f90ae885/charset_normalizer-3.5.1-cp315-cp315t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:19a3dd5aa73cef1c99687c4fc57db016a9c17104ae1185da88ba566a5d3bebe4", size = 258888, upload-time = "2026-08-15T08:19:32.458Z" }, + { url = "https://files.pythonhosted.org/packages/ff/c1/2adc2800903fb013210349313b710a5376856578d9e33e6b9a1d8b36714a/charset_normalizer-3.5.1-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cc5d36d96478aa9c60654bd932525bf32964c62a7281eafdf16d85003a8d6004", size = 250243, upload-time = "2026-08-15T08:19:33.94Z" }, + { url = "https://files.pythonhosted.org/packages/95/b5/a18d0dd1157ab655cc2cb14a545f4a4784bbad70ab3502412e36097502d9/charset_normalizer-3.5.1-cp315-cp315t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:04368edf83514385ffc3e1cfd4546e595f4f1272dd23ba437a93a9cc3741d47b", size = 249871, upload-time = "2026-08-15T08:19:35.413Z" }, + { url = "https://files.pythonhosted.org/packages/ad/c3/525f508cd1e58d0450ac55ed40ac75bc3a97482c59def5278456a5fbf03c/charset_normalizer-3.5.1-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:9b5db6052055d34d41230fb78d7c439c23dc536a9896f6cb039e8dd92cfc1263", size = 243580, upload-time = "2026-08-15T08:19:36.886Z" }, + { url = "https://files.pythonhosted.org/packages/7c/c1/49a91fe7e97c8140094ca5c64161ab623a70d9f636bf834eace14048acb5/charset_normalizer-3.5.1-cp315-cp315t-musllinux_1_2_armv7l.whl", hash = "sha256:252d099029bcbea642f2a06c4ed5046bdf8b5a8150b64afa5e027e88b106e5ee", size = 239807, upload-time = "2026-08-15T08:19:38.392Z" }, + { url = "https://files.pythonhosted.org/packages/d3/58/56a48c296601274c4689b864a8e2dfb209b81dfcb39472753ce95eea662b/charset_normalizer-3.5.1-cp315-cp315t-musllinux_1_2_ppc64le.whl", hash = "sha256:6199d5606e2bbf2b096cf64d03f8b6790c91081d5ac866b8e7bb6422738cc60c", size = 264083, upload-time = "2026-08-15T08:19:39.856Z" }, + { url = "https://files.pythonhosted.org/packages/10/4c/dc48409274a1817ff349711d26c62aa0c597df865d4d69ef79160c859193/charset_normalizer-3.5.1-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:77efcff2b23071c349402ac1066667a3d011f62398d81408c9b88ad991747c9e", size = 250317, upload-time = "2026-08-15T08:19:41.53Z" }, + { url = "https://files.pythonhosted.org/packages/81/58/d325912115caec62d6bdd77bbab5e0b7da5d234a9f20affdffcbcb530d0b/charset_normalizer-3.5.1-cp315-cp315t-musllinux_1_2_s390x.whl", hash = "sha256:a5cbd90ecf0fc62e64726917ad083b73001f0563657a87ec3c0b504e277dc90d", size = 258173, upload-time = "2026-08-15T08:19:43.07Z" }, + { url = "https://files.pythonhosted.org/packages/34/f7/b13b1ccae2c8ec63980d13be1890eb73f8aeabbfce02a24aabc0908788f5/charset_normalizer-3.5.1-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:4d26f14f041e83dd8edfd61f4cd4fa7285d31798b5bf1f28e70c367ba6c41d61", size = 251960, upload-time = "2026-08-15T08:19:44.587Z" }, + { url = "https://files.pythonhosted.org/packages/1e/25/ed3f9919c5aef8cc818be1f972f565f7610d7b2076b8ebb98839516ffc3c/charset_normalizer-3.5.1-cp315-cp315t-win32.whl", hash = "sha256:ac13b004224fb341e1e25a1ed5e19d32f57cdb2a403e01f003b46f051a550f6f", size = 191186, upload-time = "2026-08-15T08:19:46.293Z" }, + { url = "https://files.pythonhosted.org/packages/69/d5/43c2b3e9d8267092b913eb8b0603f0f71993c395632886bd37a7223f96cf/charset_normalizer-3.5.1-cp315-cp315t-win_amd64.whl", hash = "sha256:35aea775dc2bd5f54cd84a1cd2696cc3207c479cb9cf0bd346f0d343e4300ddb", size = 215947, upload-time = "2026-08-15T08:19:47.853Z" }, + { url = "https://files.pythonhosted.org/packages/a8/76/9aad3e9c8865e5e0efa9a7f6f81c37a67635a985145ecd44528a81e088ee/charset_normalizer-3.5.1-cp315-cp315t-win_arm64.whl", hash = "sha256:fb78f6e7fcd8ad785d28cd577168bc1aaee827b25bb8755638f694794ea98f0a", size = 193909, upload-time = "2026-08-15T08:19:49.383Z" }, + { url = "https://files.pythonhosted.org/packages/5b/97/fb4e82231aba271ffd775a1b4993b0defc4e3059f286ae41d9433409fe85/charset_normalizer-3.5.1-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:41876ee62a3dddf48ff1121ad8f0798032aa03f2fd35f21f34a4cab14f18d8d2", size = 331467, upload-time = "2026-08-15T08:19:50.959Z" }, + { url = "https://files.pythonhosted.org/packages/9f/2f/fe3f187327aac18e2d54e9d2b08e15d27bf9b642d9e51c219f130fc34d1a/charset_normalizer-3.5.1-cp37-abi3-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:a6dac12ff6b846103483683f60c5f8fee205121adc58ffd87e90a90a3af69e99", size = 253057, upload-time = "2026-08-15T08:19:52.654Z" }, + { url = "https://files.pythonhosted.org/packages/d7/c7/9e48cee5c161fe24da823b61bf381921d77cb994a0a4de148e95018c1984/charset_normalizer-3.5.1-cp37-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cee5dd7c6fb5dd52a0fe2a740f9bc6e3593f5f8b1788bde49de02086f30182b2", size = 240930, upload-time = "2026-08-15T08:19:54.163Z" }, + { url = "https://files.pythonhosted.org/packages/49/e0/716601f3cc69be7b198951150c75ead1ece33c3c8036ff6ffa46029659a0/charset_normalizer-3.5.1-cp37-abi3-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:343fb4f2821043bd87095f7b08a1a181febc8e36ac64212143bbfd0a0e1bc235", size = 230822, upload-time = "2026-08-15T08:19:55.807Z" }, + { url = "https://files.pythonhosted.org/packages/d3/05/71bfc5caa0abcc45aea1f6a4d50ac68e59605ddc7666fe8494f4cd229665/charset_normalizer-3.5.1-cp37-abi3-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ae4a097991662cd4fff0ddc74e0fe7874f82e00042fa0ea00855645ed0c79598", size = 260037, upload-time = "2026-08-15T08:19:57.312Z" }, + { url = "https://files.pythonhosted.org/packages/c3/92/de7e32ed05341e7a9c4c877c318418197b7f2d66a3b68d561bf2ac57ca3e/charset_normalizer-3.5.1-cp37-abi3-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4b599739b93b2cbeded49645ae3c8d1405c29ddfbceac1545c87a3f9580a9e96", size = 255097, upload-time = "2026-08-15T08:19:59.056Z" }, + { url = "https://files.pythonhosted.org/packages/f5/7b/ade0a122600319dfa0b1000ab0f9731c94a817904cf3c5de408c73a4ede7/charset_normalizer-3.5.1-cp37-abi3-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b39b69b347e5e47a3b5b8cfc005c68c1ba347474e3960236c4944a8ecd174962", size = 250166, upload-time = "2026-08-15T08:20:00.612Z" }, + { url = "https://files.pythonhosted.org/packages/75/9c/019fbb9f4834491a160951349b1a3714439376f66e5f7cf18b4f18f0c7aa/charset_normalizer-3.5.1-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:a2028475ba855475b8b4d3cfeb4994269c967aea8b9892dfba907f4263a863a3", size = 241821, upload-time = "2026-08-15T08:20:02.321Z" }, + { url = "https://files.pythonhosted.org/packages/2b/b8/11d4840bfc99330cc7fbcc2681ee5a044553a6e77655508d8f9b2bff7b34/charset_normalizer-3.5.1-cp37-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:36047af20e17097c3bb9476c2b7655f2f7aa51322c0ba58c07695bedf755a950", size = 232529, upload-time = "2026-08-15T08:20:04.008Z" }, + { url = "https://files.pythonhosted.org/packages/18/96/2b3a21492d9f65171ac75d872f5018260013d00bfa0ff70ec9f179148cbd/charset_normalizer-3.5.1-cp37-abi3-musllinux_1_2_ppc64le.whl", hash = "sha256:4c4fb141a727957c93edfe5c32a26ceb6b5f6461d67146e2d39f51e16170bea8", size = 260348, upload-time = "2026-08-15T08:20:05.877Z" }, + { url = "https://files.pythonhosted.org/packages/d6/aa/a69a2028e8bd052476c245460ab19d7de595de084dd968f2d75cd50c3e25/charset_normalizer-3.5.1-cp37-abi3-musllinux_1_2_riscv64.whl", hash = "sha256:2f293479cce755c75f1697e87c409b7ae4c555c7dfecb6e988ad13abba943031", size = 247234, upload-time = "2026-08-15T08:20:07.487Z" }, + { url = "https://files.pythonhosted.org/packages/35/8a/3d130aeabcaf3d2466af76b7b141c08d9e89c9016ab4b7cdd0f7dc2d1c62/charset_normalizer-3.5.1-cp37-abi3-musllinux_1_2_s390x.whl", hash = "sha256:3588e376b3ea2eea84976f67273d679f229e24c66dce7b82ae45aef04ff6e072", size = 256917, upload-time = "2026-08-15T08:20:09.142Z" }, + { url = "https://files.pythonhosted.org/packages/80/c2/a7379b840292d0c1ab9fbd17d1f3967aa81794dc95bc74be8999d7fedcf7/charset_normalizer-3.5.1-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:e199fb99720074809a7720f1c0b4d919eea8b87e88713e0f8f602f7bef543d9d", size = 254846, upload-time = "2026-08-15T08:20:10.727Z" }, + { url = "https://files.pythonhosted.org/packages/01/65/d43b714731bb2f40d4053dfa00ecfc1c5a301f8e3316c5db3a09af59fe94/charset_normalizer-3.5.1-cp37-abi3-win32.whl", hash = "sha256:dd732602a7009217f658d5863d12d79d373a4de0eebc111094bcdd3bb8e0a6cc", size = 174216, upload-time = "2026-08-15T08:20:12.334Z" }, + { url = "https://files.pythonhosted.org/packages/35/4f/b911ed898b26a09789eba9c9200c999aff6c61b4bafaf4838e56d1a1e1a3/charset_normalizer-3.5.1-cp37-abi3-win_amd64.whl", hash = "sha256:70055ff39b97c99e7ae40ea3e393fb62aa2e44dbd9b29f8d14f42fb0025c3959", size = 199764, upload-time = "2026-08-15T08:20:13.908Z" }, + { url = "https://files.pythonhosted.org/packages/f0/a7/920baf467bfd9bf689f3b318340f37aee4572a71f162bd8db51da55ba4fa/charset_normalizer-3.5.1-cp37-abi3-win_arm64.whl", hash = "sha256:87e4f41d375c0b9be2fb5251aee4b8a689169e134535aed81bf085c3b647451e", size = 287318, upload-time = "2026-08-15T08:20:15.551Z" }, + { url = "https://files.pythonhosted.org/packages/cc/61/d01fc49b8dea277640b55a9e15960dbca9fdc8c9fde18e572d39c59f4019/charset_normalizer-3.5.1-py3-none-any.whl", hash = "sha256:6df0ec430f9a831772c23ca5a224cba36517a58a84bb32c32bb59a9fa67c47f6", size = 68658, upload-time = "2026-08-15T08:20:43.306Z" }, +] + +[[package]] +name = "idna" +version = "3.19" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5f/f7/abb373e5757eaec4b922b92f97ec8d6d7e057cf06778247604fbc4e7c3f3/idna-3.19.tar.gz", hash = "sha256:5e0811a4383b21dc5838069f801c4fb62113b7447663d2530d2bd6e77b49bf15", size = 215237, upload-time = "2026-08-18T05:14:24.27Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/57/b0/0e52c878c53f245edd3a11020f20979b3f490f245af532c7cae3027754b5/idna-3.19-py3-none-any.whl", hash = "sha256:815e7be7a7806d54abb586dc943addc79e8b2ee16915059658cbeff4b1b43bf4", size = 68550, upload-time = "2026-08-18T05:14:22.343Z" }, +] + +[[package]] +name = "labs-update-tooling" +version = "0" +source = { virtual = "." } +dependencies = [ + { name = "pyyaml" }, + { name = "requests" }, +] + +[package.metadata] +requires-dist = [ + { name = "pyyaml", specifier = ">=6.0" }, + { name = "requests", specifier = ">=2.32" }, +] + +[[package]] +name = "pyyaml" +version = "6.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6d/16/a95b6757765b7b031c9374925bb718d55e0a9ba8a1b6a12d25962ea44347/pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e", size = 185826, upload-time = "2025-09-25T21:31:58.655Z" }, + { url = "https://files.pythonhosted.org/packages/16/19/13de8e4377ed53079ee996e1ab0a9c33ec2faf808a4647b7b4c0d46dd239/pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824", size = 175577, upload-time = "2025-09-25T21:32:00.088Z" }, + { url = "https://files.pythonhosted.org/packages/0c/62/d2eb46264d4b157dae1275b573017abec435397aa59cbcdab6fc978a8af4/pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c", size = 775556, upload-time = "2025-09-25T21:32:01.31Z" }, + { url = "https://files.pythonhosted.org/packages/10/cb/16c3f2cf3266edd25aaa00d6c4350381c8b012ed6f5276675b9eba8d9ff4/pyyaml-6.0.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00", size = 882114, upload-time = "2025-09-25T21:32:03.376Z" }, + { url = "https://files.pythonhosted.org/packages/71/60/917329f640924b18ff085ab889a11c763e0b573da888e8404ff486657602/pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d", size = 806638, upload-time = "2025-09-25T21:32:04.553Z" }, + { url = "https://files.pythonhosted.org/packages/dd/6f/529b0f316a9fd167281a6c3826b5583e6192dba792dd55e3203d3f8e655a/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a", size = 767463, upload-time = "2025-09-25T21:32:06.152Z" }, + { url = "https://files.pythonhosted.org/packages/f2/6a/b627b4e0c1dd03718543519ffb2f1deea4a1e6d42fbab8021936a4d22589/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4", size = 794986, upload-time = "2025-09-25T21:32:07.367Z" }, + { url = "https://files.pythonhosted.org/packages/45/91/47a6e1c42d9ee337c4839208f30d9f09caa9f720ec7582917b264defc875/pyyaml-6.0.3-cp311-cp311-win32.whl", hash = "sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b", size = 142543, upload-time = "2025-09-25T21:32:08.95Z" }, + { url = "https://files.pythonhosted.org/packages/da/e3/ea007450a105ae919a72393cb06f122f288ef60bba2dc64b26e2646fa315/pyyaml-6.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf", size = 158763, upload-time = "2025-09-25T21:32:09.96Z" }, + { url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063, upload-time = "2025-09-25T21:32:11.445Z" }, + { url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973, upload-time = "2025-09-25T21:32:12.492Z" }, + { url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" }, + { url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011, upload-time = "2025-09-25T21:32:15.21Z" }, + { url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870, upload-time = "2025-09-25T21:32:16.431Z" }, + { url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089, upload-time = "2025-09-25T21:32:17.56Z" }, + { url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181, upload-time = "2025-09-25T21:32:18.834Z" }, + { url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658, upload-time = "2025-09-25T21:32:20.209Z" }, + { url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003, upload-time = "2025-09-25T21:32:21.167Z" }, + { url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344, upload-time = "2025-09-25T21:32:22.617Z" }, + { url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" }, + { url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" }, + { url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" }, + { url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" }, + { url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" }, + { url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" }, + { url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" }, + { url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" }, + { url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" }, + { url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" }, + { url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" }, + { url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" }, + { url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" }, + { url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" }, + { url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" }, + { url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" }, + { url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" }, + { url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" }, + { url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" }, + { url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" }, + { url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" }, + { url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" }, + { url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" }, + { url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" }, + { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" }, +] + +[[package]] +name = "requests" +version = "2.34.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ac/c3/e2a2b89f2d3e2179abd6d00ebd70bff6273f37fb3e0cc209f48b39d00cbf/requests-2.34.2.tar.gz", hash = "sha256:f288924cae4e29463698d6d60bc6a4da69c89185ad1e0bcc4104f584e960b9ed", size = 142856, upload-time = "2026-05-14T19:25:27.735Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/f4/c67b0b3f1b9245e8d266f0f112c500d50e5b4e83cb6f3b71b6528104182a/requests-2.34.2-py3-none-any.whl", hash = "sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0", size = 73075, upload-time = "2026-05-14T19:25:26.443Z" }, +] + +[[package]] +name = "urllib3" +version = "2.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/53/0c/06f8b233b8fd13b9e5ee11424ef85419ba0d8ba0b3138bf360be2ff56953/urllib3-2.7.0.tar.gz", hash = "sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c", size = 433602, upload-time = "2026-05-07T16:13:18.596Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl", hash = "sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897", size = 131087, upload-time = "2026-05-07T16:13:17.151Z" }, +] diff --git a/.github/scripts/verify_action_pins.py b/.github/scripts/verify_action_pins.py new file mode 100644 index 0000000..34caef8 --- /dev/null +++ b/.github/scripts/verify_action_pins.py @@ -0,0 +1,170 @@ +#!/usr/bin/env python3 +"""Reachability verification for hash-pinned GitHub Actions. + +pinact's ``--verify-comment`` proves a pin's ``# vX.Y.Z`` comment still resolves +to the SHA next to it. It cannot prove that SHA belongs to the repository it is +attributed to: a tag created against a commit pushed to a *fork* resolves just +as cleanly, which is the imposter-commit half of +https://github.com/aquasecurity/trivy/security/advisories/GHSA-69fq-xp46-6x23 + +Reachability from a named branch is what separates the two, and it is the one +check pinact has no equivalent for. The pin-actions job runs this after pinning, +so a poisoned pin fails the job rather than landing in the weekly PR. + +The min-age cooldown does not cover this. pinact compares the commit's +*committer date*, which is attacker-controlled — backdating a commit walks +straight through a 7-day window. + +This also stands in for a per-PR ``zizmor unpinned-uses`` gate: a ``uses:`` that +is not SHA-pinned is reported as a failure here rather than skipped, so the +weekly run catches anything reintroduced by hand. +""" + +import argparse +import logging +import sys +from collections.abc import Iterator +from pathlib import Path + +import yaml + +from supply_chain import annotations +from supply_chain.github import verify_commit_on_branch + +log = logging.getLogger("verify_action_pins") + +_SHA_LENGTH = 40 +_SHA_CHARS = frozenset("0123456789abcdef") + +# `uses:` values naming something other than an upstream GitHub repository: +# a local action, a local reusable workflow, or a container image. +_NON_UPSTREAM_PREFIXES = ("./", "../", "docker://") + + +def _iter_uses(node: object) -> Iterator[str]: + """Yield every ``uses:`` value in a parsed workflow or action document. + + Walks the whole document rather than the shapes we know today + (``jobs.*.steps[]``, ``runs.steps[]``, ``jobs.*.uses``) so a shape we have + not thought of cannot quietly escape verification. + """ + if isinstance(node, dict): + for key, value in node.items(): + if key == "uses" and isinstance(value, str): + yield value + else: + yield from _iter_uses(value) + elif isinstance(node, list): + for item in node: + yield from _iter_uses(item) + + +def _is_sha(ref: str) -> bool: + return len(ref) == _SHA_LENGTH and set(ref) <= _SHA_CHARS + + +def collect_files(paths: list[Path]) -> list[Path]: + """Expand the requested paths into the YAML documents to inspect.""" + files: set[Path] = set() + for path in paths: + if path.is_dir(): + files.update(path.rglob("*.yml")) + files.update(path.rglob("*.yaml")) + elif path.is_file(): + files.add(path) + return sorted(files) + + +def find_pinned_actions( + files: list[Path], +) -> tuple[dict[tuple[str, str, str], set[str]], list[str]]: + """Map every pinned upstream action to the files that reference it. + + Parsing is YAML-aware rather than line-based so quoting, indentation and + block style cannot change the answer. + + :return: ``{(owner, repo, sha): {file, ...}}`` plus a list of upstream refs + that are not SHA-pinned. Those are reported rather than skipped so this + check is never silently partial. + """ + pins: dict[tuple[str, str, str], set[str]] = {} + unpinned: list[str] = [] + + for file_path in files: + document = yaml.safe_load(file_path.read_text(encoding="utf-8")) + for uses in _iter_uses(document): + if uses.startswith(_NON_UPSTREAM_PREFIXES): + continue + name, _, ref = uses.partition("@") + if not ref: + continue + parts = name.split("/") + if len(parts) < 2: + continue + owner, repo = parts[0], parts[1] + if not _is_sha(ref): + unpinned.append(f"{file_path}: {name}@{ref} is not pinned to a commit") + continue + pins.setdefault((owner, repo, ref), set()).add(str(file_path)) + + return pins, unpinned + + +def verify_reachable(pins: dict[tuple[str, str, str], set[str]]) -> list[str]: + """Confirm each pinned commit is reachable from a branch in its repository. + + Deduplicated by commit: one SHA referenced from many call sites resolves + identically for all of them, so it is verified once to stay inside the + hourly GitHub API budget. + """ + failures: list[str] = [] + + for (owner, repo, sha), locations in sorted(pins.items()): + try: + branch = verify_commit_on_branch(owner, repo, sha) + except Exception as exc: + failures.append( + f"{owner}/{repo}@{sha} is not reachable from any branch: {exc} " + f"Referenced by: {', '.join(sorted(locations))}" + ) + continue + log.info("%s/%s@%s reachable from %s", owner, repo, sha, branch) + + return failures + + +def verify_action_pins(paths: list[Path]) -> int: + """Verify every pinned action under ``paths``. Returns a process exit code.""" + files = collect_files(paths) + if not files: + log.error("No workflow or action files found under: %s", paths) + return 1 + log.info("Inspecting %d file(s) for pinned actions", len(files)) + + pins, unpinned = find_pinned_actions(files) + log.info("Found %d distinct pinned commit(s)", len(pins)) + + failures = unpinned + verify_reachable(pins) + if failures: + for failure in failures: + log.error(failure) + annotations.error(failure, title="action-pin-not-reachable") + return 1 + + log.info("Every pinned action is reachable from an upstream branch") + return 0 + + +if __name__ == "__main__": + logging.basicConfig(level=logging.INFO, format="%(levelname)s %(name)s: %(message)s") + parser = argparse.ArgumentParser( + description="Verify hash-pinned GitHub Actions resolve to commits reachable upstream" + ) + parser.add_argument( + "--path", + type=Path, + action="append", + required=True, + help="File or directory to inspect; repeatable. Directories are searched recursively", + ) + sys.exit(verify_action_pins(parser.parse_args().path)) diff --git a/.github/workflows/update.yml b/.github/workflows/update.yml new file mode 100644 index 0000000..d898065 --- /dev/null +++ b/.github/workflows/update.yml @@ -0,0 +1,173 @@ +--- +name: "Updates" + +on: + schedule: + # Every Tuesday at 03:16 UTC (Monday 11:16 PM EDT / 10:16 PM EST) + - cron: '16 3 * * 2' + workflow_dispatch: + +defaults: + run: + shell: 'bash --noprofile --norc -Eeuo pipefail {0}' + +permissions: {} + +jobs: + update-dependencies: + name: Update dependencies + runs-on: ubuntu-24.04 + permissions: + # Required to push the update branch + contents: write + # Required to open the pull request + pull-requests: write + steps: + - name: Mint a Zenable Automation app token + id: app-token + uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0 + with: + client-id: ${{ vars.ZENABLE_AUTOMATION_CLIENT_ID }} + private-key: ${{ secrets.RELEASE_APP_PRIVATE_KEY }} + permission-contents: write + permission-pull-requests: write + - name: Checkout the repository + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + token: ${{ steps.app-token.outputs.token }} + - name: Install uv + uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1 + with: + # The tooling's own dependencies resolve from its committed lock. + enable-cache: true + - name: Update every pinned dependency + env: + # The default token is enough: every GitHub call this makes is an + # unauthenticated-capable read, and it is used only to stay off the + # 60-requests-per-hour anonymous limit. + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: bash .github/scripts/update-pins.sh + - name: Create or update a pull request + uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8.1.1 + with: + title: 'chore(labs): automated dependency update' + branch: automated-update/dependencies + body: | + # Automated updates from the [Update workflow](https://github.com/Zenable-io/labs/actions/workflows/update.yml) + + Container image tags and `uv.lock` files across every lab. + + Every version here cleared a 7-day cooldown before being adopted, so a + compromised upstream release had a week to surface first. Versions + sourced from GitHub releases were additionally verified end to end: + the tag exists as a ref in the repository it claims to come from, and + the commit it resolves to is reachable from a branch there — which an + imposter commit pushed to a fork is not. + + **Before merging:** check the run's annotations for + `doc-quotes-superseded-version`. Recorded terminal output in the + READMEs and `evidence/` is never rewritten by this automation, so an + image bump leaves those transcripts stale until someone re-runs the + lab and regenerates them. + commit-message: 'chore(labs): automated dependency update' + committer: 'Zenable Automation ' + author: 'Zenable Automation ' + labels: |- + Automatic update + batch-update + token: ${{ steps.app-token.outputs.token }} + delete-branch: true + signoff: true + + # Separate from update-dependencies because this is the ONLY job whose token + # may rewrite .github/workflows/**. Keeping it alone means + # `permission-workflows: write` is requested in exactly one place in this repo, + # and every workflow-file edit the automation makes lands in its own PR. + update-github-actions: + name: Update GitHub Actions + runs-on: ubuntu-24.04 + permissions: + contents: write + pull-requests: write + steps: + - name: Mint a Zenable Automation app token + id: app-token + uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0 + with: + client-id: ${{ vars.ZENABLE_AUTOMATION_CLIENT_ID }} + private-key: ${{ secrets.RELEASE_APP_PRIVATE_KEY }} + permission-contents: write + permission-pull-requests: write + # The one job that needs it: pinact rewrites `uses:` lines in + # .github/workflows/**, and GitHub rejects a push of those files from a + # token without this permission. + permission-workflows: write + - name: Checkout the repository + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + token: ${{ steps.app-token.outputs.token }} + - name: Install uv + uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1 + - name: Install pinact + env: + # Verified against the release's published checksums rather than + # trusted on download. A tool whose whole job is supply-chain pinning + # has no business installing itself unverified. + PINACT_VERSION: v4.1.1 + run: | + os="$(uname -s | tr '[:upper:]' '[:lower:]')" + arch="$(uname -m | sed -e 's/x86_64/amd64/' -e 's/aarch64/arm64/')" + archive="pinact_${os}_${arch}.tar.gz" + base="https://github.com/suzuki-shunsuke/pinact/releases/download/${PINACT_VERSION}" + workdir="$(mktemp -d)" + trap 'rm -rf "${workdir}"' EXIT + curl -sSfL -o "${workdir}/${archive}" "${base}/${archive}" + curl -sSfL -o "${workdir}/checksums.txt" \ + "${base}/pinact_${PINACT_VERSION#v}_checksums.txt" + (cd "${workdir}" && grep " ${archive}\$" checksums.txt | sha256sum -c -) + sudo tar xz -C /usr/local/bin -f "${workdir}/${archive}" pinact + - name: Hash-pin and verify the GitHub Actions + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + # `release/vN` is a branch, not a tag: pinning it would freeze a moving + # ref at whatever its head was that week, with no version comment to + # update from. Converting it to the newest stable tag gives it the same + # cooldown and update path as every other action. + pinact run -u --branch-to-tag '^release/v[0-9]+$' + # Re-resolve each `# vX.Y.Z` comment and confirm it still names the + # pinned SHA. Deliberately a second invocation in check mode: under + # --fix a mismatch REWRITES the comment to match the SHA, so a + # retargeted tag would be silently relabelled instead of failing. + pinact run --check --verify-comment + # pinact stops at "the comment matches the SHA"; it never asks whether + # the SHA belongs to that repo. A tag cut against a fork commit + # satisfies the check above and fails this one. + uv run --directory .github/scripts --frozen python verify_action_pins.py \ + --path "${GITHUB_WORKSPACE}/.github/workflows" + - name: Create or update a pull request + uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8.1.1 + with: + title: 'chore(actions): automated update' + branch: automated-update/github-actions + body: | + # Automated updates from the [Update workflow](https://github.com/Zenable-io/labs/actions/workflows/update.yml) + + Re-pins every GitHub Action to the newest release that has cleared its + 7-day cooldown. + + Each pin was then verified twice: `pinact run --check --verify-comment` + confirms the tag named in each version comment still resolves to the + pinned commit, and `verify_action_pins.py` confirms that commit is + reachable from a branch in the upstream repository — which an imposter + commit pushed to a fork is not. That second check also fails on any + `uses:` that is not SHA-pinned at all. + commit-message: 'chore(actions): automated update' + committer: 'Zenable Automation ' + author: 'Zenable Automation ' + labels: |- + Automatic update + batch-update + token: ${{ steps.app-token.outputs.token }} + delete-branch: true + signoff: true diff --git a/.pinact.yaml b/.pinact.yaml new file mode 100644 index 0000000..6a374d4 --- /dev/null +++ b/.pinact.yaml @@ -0,0 +1,25 @@ +# yaml-language-server: $schema=https://raw.githubusercontent.com/suzuki-shunsuke/pinact/main/json-schema/pinact.json +--- +version: 3 + +# Go's path/filepath#Glob, so no `**`. Both shapes are listed because a hash pin +# is only worth anything if it covers every `uses:` in the repo — an unpinned +# composite action is as exploitable as an unpinned workflow. +files: + - pattern: .github/workflows/*.yml + - pattern: .github/workflows/*.yaml + - pattern: .github/actions/*/action.yml + - pattern: .github/actions/*/action.yaml + +# The same 7-day supply-chain cooldown update_file.py applies to every other pin +# in this repo, so a compromised release has a week to surface before we adopt +# it. `value` gates which tag `pinact run -u` adopts. +# +# `always` stays false: it would add a GetCommit call per pinned action on every +# run, and it would buy little — the cooldown compares the commit's committer +# date, which is attacker-controlled, so it is a staleness guard rather than a +# tamper guard. The pin-actions job follows the update with +# `pinact run --check --verify-comment` and verify_action_pins.py, which are. +min_age: + value: 7 + always: false diff --git a/labs/a2a/agents/pyproject.toml b/labs/a2a/agents/pyproject.toml index 7ac47d9..62d5a02 100644 --- a/labs/a2a/agents/pyproject.toml +++ b/labs/a2a/agents/pyproject.toml @@ -12,3 +12,7 @@ dependencies = [ [tool.uv] package = false +# Adopt only packages published more than a week ago, so a compromised +# release has time to surface before a lab installs it. Enforced by the +# resolver, so `uv lock --upgrade` and a reader's `uv sync` obey it alike. +exclude-newer = "7 days" diff --git a/labs/a2a/agents/uv.lock b/labs/a2a/agents/uv.lock index b79f8ab..2df5171 100644 --- a/labs/a2a/agents/uv.lock +++ b/labs/a2a/agents/uv.lock @@ -6,6 +6,10 @@ resolution-markers = [ "python_full_version < '3.14'", ] +[options] +exclude-newer = "0001-01-01T00:00:00Z" # This has no effect and is included for backwards compatibility when using relative exclude-newer values. +exclude-newer-span = "P7D" + [[package]] name = "a2a-sdk" version = "1.1.2" @@ -345,79 +349,67 @@ wheels = [ [[package]] name = "click" -version = "8.4.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "colorama", marker = "sys_platform == 'win32'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/76/d4/81420972a676e8ffea40450d8c8c92943e7218a78fe9b64359836cc9876b/click-8.4.2.tar.gz", hash = "sha256:9a6cea6e60b17ebe0a44c5cc636d94f09bd66142c1cd7d8b4cd731c4917a15f6", size = 338000, upload-time = "2026-06-24T17:45:15.148Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/fb/e2/79c688af8b210d232694e31e59da9f6ec747bae31c3f5946e4e9b98860d5/click-8.4.2-py3-none-any.whl", hash = "sha256:e6f9f66136c816745b9d65817da91d61d957fb16e02e4dcd0552553c5a197b76", size = 119243, upload-time = "2026-06-24T17:45:13.73Z" }, -] - -[[package]] -name = "colorama" -version = "0.4.6" +version = "8.5.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +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/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, + { url = "https://files.pythonhosted.org/packages/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.0" +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/de/41/6cbdcf9142d00fe82836fbb51e503e58088575cf7a0fe1dbff6695bf0840/cryptography-50.0.0.tar.gz", hash = "sha256:eeac2acb5a20ed25e0ad6d1df9891a520b78b404266b6d11778f25d5d691a6c9", size = 880201, upload-time = "2026-07-31T14:25:10.11Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c5/5c/59086b4aac5e879d38ddbcf74e4be7ade89cebc3eb199a55da998c3bb46a/cryptography-50.0.0-cp311-abi3-macosx_11_0_arm64.whl", hash = "sha256:031e2d5dd4bb9caa3ca9c82e5a197fd8ae680232cee62603d1a813f3f07e3d03", size = 4001252, upload-time = "2026-07-31T14:23:33.331Z" }, - { url = "https://files.pythonhosted.org/packages/57/ef/8f2df13c7216bcad3e1c74e07f6e193d93e998e114f524a53877c9af27ad/cryptography-50.0.0-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fd9192b7b70c573d7f214eb1ae35e00d359f6f5e4b27c7e21e30de1fc6204645", size = 4719554, upload-time = "2026-07-31T14:23:35.611Z" }, - { url = "https://files.pythonhosted.org/packages/d9/41/029086c34d91052fc3b88bcc8056f709a7c915c7a23b235a54eb800b1c97/cryptography-50.0.0-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:06a32a980526a6ab9a4b9bf8f7385800791e2bb960903cb6b530e4817509a3b7", size = 4702130, upload-time = "2026-07-31T14:23:37.635Z" }, - { url = "https://files.pythonhosted.org/packages/7d/ff/b6ce0954962e7f7b969f850a883744197bb3910bdfd7b6da162eab7d9f68/cryptography-50.0.0-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:a1b30560f2acc95aa8b2e06e716a13dbfc97314747b80d9707e307f77b40d6b3", size = 4725244, upload-time = "2026-07-31T14:23:39.471Z" }, - { url = "https://files.pythonhosted.org/packages/06/1e/63a1027cb7fec360a182208e1b7767d5aa1fe57be3d6aa856e69a321edc0/cryptography-50.0.0-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:8d89f3976b10b4ce31118de72329025f70d2c6ead14a8217c5514dd2c6d5a78f", size = 5342265, upload-time = "2026-07-31T14:23:41.286Z" }, - { url = "https://files.pythonhosted.org/packages/6b/72/a1116d683a6d7ece94590013882515de087edf9ef0e6292aae615a44df73/cryptography-50.0.0-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:b42a28c1844fd9de8f3f7d540e36b66f3a9c83fceac7170ebc7a6a19edd9dcae", size = 4734609, upload-time = "2026-07-31T14:23:43.139Z" }, - { url = "https://files.pythonhosted.org/packages/15/37/36a9c479bbe49acea2636c7fd3360d20f7b7e079c300352011c44850b181/cryptography-50.0.0-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:900131fafd8aead39ac7dd3a7e833be754c17a95cfd91221636949fe4eb0aa8a", size = 4356517, upload-time = "2026-07-31T14:23:44.939Z" }, - { url = "https://files.pythonhosted.org/packages/32/98/8a151d64367204cbc63ec65d37502f1d9c53cf4bfc6ec3c532614dbec60d/cryptography-50.0.0-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:07949c449a1abcf60d1ee6e88956d89404c7df3c8258f46589e912988e551987", size = 4724529, upload-time = "2026-07-31T14:23:46.93Z" }, - { url = "https://files.pythonhosted.org/packages/22/f6/ec13b470172126464a86bf54d2294a46d29837fc51ba3e45d4047946fb5e/cryptography-50.0.0-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:f89831ef99dd7dd169ab06d63a831adb9e20a87aac6d380266bbda5823349169", size = 5299852, upload-time = "2026-07-31T14:23:48.851Z" }, - { url = "https://files.pythonhosted.org/packages/da/3a/f05e32c99d440c9bb891ea0e36c9091891e36be5a9a87ab2ee6ea20729f6/cryptography-50.0.0-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:82148ec5bddac30b51a5b3c1945075f896fa022cb93f8e4a01e9f6ee95292c5f", size = 4734462, upload-time = "2026-07-31T14:23:50.861Z" }, - { url = "https://files.pythonhosted.org/packages/ca/dc/bd72b26be8953f80625f63151efd38eee71c76ca6cf591c08ff34615a79e/cryptography-50.0.0-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:1489e263a8048bb8b6a8bac662eb2d402ea5d2b7b4699b72f385f1e2772db105", size = 4852708, upload-time = "2026-07-31T14:23:52.715Z" }, - { url = "https://files.pythonhosted.org/packages/27/20/c930314a2ab476d15dec966ec87e2e9637bb02b06106b12c0396c57bb603/cryptography-50.0.0-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:7cec5b856506da6defb290f30c9ee687d5f5e8cb0bd3f6459dde43b0b4fa40ef", size = 5004179, upload-time = "2026-07-31T14:23:54.887Z" }, - { url = "https://files.pythonhosted.org/packages/32/2e/c9db68a0c4bfa28e310707527c0ee3a2bd254104d2e02e68f368e197aa4c/cryptography-50.0.0-cp311-abi3-win_amd64.whl", hash = "sha256:bd1c592e4d5974f0d08d4888e432157adba757c66da0246918e43677fafa2d30", size = 3840395, upload-time = "2026-07-31T14:23:56.677Z" }, - { url = "https://files.pythonhosted.org/packages/c3/fb/951032a3bf22a5697c83183fb6294a4843772947a70e616c57b3ff5f522e/cryptography-50.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:49e7d93abdbd2990caced757e5fade25302f719c3c8fb6e6fff2dde98999fc41", size = 3989258, upload-time = "2026-07-31T14:23:58.881Z" }, - { url = "https://files.pythonhosted.org/packages/d4/67/91eb047e69c5e845f2f14b8a2e4a1aab0f283cb885531e9e22c8adb176bc/cryptography-50.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:19736989797678c6af1e55cd49055cdbcb55d8f6b5583ac5335f933aba9101dc", size = 4700648, upload-time = "2026-07-31T14:24:00.702Z" }, - { url = "https://files.pythonhosted.org/packages/30/82/85f0f7425c856b9f96459411eb12e74ef72df9caf6f8f15bf23a33ff131f/cryptography-50.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:80b63928fa35083b33966ce1efb70e5b9607181e49dcd1c22c8c005e319f667f", size = 4682442, upload-time = "2026-07-31T14:24:02.538Z" }, - { url = "https://files.pythonhosted.org/packages/1a/28/b555a365adff1cca2fbe7b9e487d68a40de6bc67ff2cb587473eb43de0e7/cryptography-50.0.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:d58c3db7cd6eed54e6c06744db55456b65ebd7492ddeae9c1e93cfca7aa857d3", size = 4707596, upload-time = "2026-07-31T14:24:04.394Z" }, - { url = "https://files.pythonhosted.org/packages/72/d8/f52538140cc719df62a01cf87d1c7142318d235817109d6f4054d7c352d6/cryptography-50.0.0-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:df2a58a472f332225671c35b0a830208b86d004f82baa8530fa3782c85646533", size = 5314552, upload-time = "2026-07-31T14:24:06.31Z" }, - { url = "https://files.pythonhosted.org/packages/38/14/6120e5bd7c5aa022ad15424ba4d5c5269d0d9448ed4d55e492ea91e3c1c4/cryptography-50.0.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:11b74db56cdbe3cdee6e3f6982ecb70334fa10dce99ed58bf7894aaaa3b2a037", size = 4717113, upload-time = "2026-07-31T14:24:08.349Z" }, - { url = "https://files.pythonhosted.org/packages/fa/71/190bf38c3ee2e0f8efc9860ae100c9df4169742eef274b91e7aa1cb133b9/cryptography-50.0.0-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:f59e38625469987d7ef6d495323c55e7db6c212eaf6112267e0d3b565a2e9c9f", size = 4338580, upload-time = "2026-07-31T14:24:10.227Z" }, - { url = "https://files.pythonhosted.org/packages/3a/63/504ccfbbe61fd8aa983f7f146399cdf034c72c2fc55f5b2dfdcdcdb20c99/cryptography-50.0.0-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:ecfed7367f965a0328cfbdd70da860f15441f002f613185668c6e6ebf5a0ac11", size = 4707038, upload-time = "2026-07-31T14:24:12.169Z" }, - { url = "https://files.pythonhosted.org/packages/01/77/2cf79bbfc4d12ca106437a6e170d6aaa01a373e93093118aaaef0e801bd4/cryptography-50.0.0-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:9aa87839c383bdbab6ef865787a1fb877af8dd03464c4400322726feaaadfc6d", size = 5273110, upload-time = "2026-07-31T14:24:14.38Z" }, - { url = "https://files.pythonhosted.org/packages/e5/45/8aae2972c520145377ea3559a605a899bebe227bf070b33cdb445929a9b9/cryptography-50.0.0-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:6ba6a53445bd3cfa809ef3ef5f1589aa6ba08784a1d962bf47d0940e871dab1c", size = 4716439, upload-time = "2026-07-31T14:24:16.415Z" }, - { url = "https://files.pythonhosted.org/packages/7b/20/4fe50b619a48c2525cc46e2dbc1ac490708d704be5d467bdaac6dc955682/cryptography-50.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:3f5735ffe4996d28b809371756219f5354864902a3b9e7c0b9ee87041209fc9c", size = 4837383, upload-time = "2026-07-31T14:24:18.553Z" }, - { url = "https://files.pythonhosted.org/packages/92/91/3a31366e183343d3703f8995c095f5734676bd6938118047e50fcf279eb4/cryptography-50.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:1b4a266766514614f8aa60416e71f2fc6e575d36e7bdc90f644fadb2f4b75b95", size = 4985772, upload-time = "2026-07-31T14:24:20.385Z" }, - { url = "https://files.pythonhosted.org/packages/74/9a/02ffe35b2853d121689871eb5dce862092562b3a1ed5cc98f1aaed441506/cryptography-50.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:12b9c6996425c76ea6c457ace4f3073e715b8c545add07cd1a8f3a4f90691269", size = 3816291, upload-time = "2026-07-31T14:24:22.125Z" }, - { url = "https://files.pythonhosted.org/packages/03/37/73d005be173aff344af30e9fd2a576575cb2391a7101d9cd3842e1fa8cce/cryptography-50.0.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:ccdc4a71a4dabae05de219404f9f4abc38e3b58422177ff93d0da05967dafa07", size = 4036009, upload-time = "2026-07-31T14:24:24.122Z" }, - { url = "https://files.pythonhosted.org/packages/ff/c6/7a6202a534e32103a285b7834a120869557fe198d51d7cfe59754c8bda9c/cryptography-50.0.0-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:910e1d2668e7de9648f2bcee30e180db2a6b15c30f887d7c4c93ddf96e3992e3", size = 4745252, upload-time = "2026-07-31T14:24:26.118Z" }, - { url = "https://files.pythonhosted.org/packages/85/4f/0fa8c2f4428198f15d9ff8d63400e27afbf94ce833f6108da1eb3753f945/cryptography-50.0.0-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a91296cb61e8df6f86d0c19cc4068228da256bf59bf86049fbd821084565327f", size = 4728939, upload-time = "2026-07-31T14:24:27.994Z" }, - { url = "https://files.pythonhosted.org/packages/d1/63/54dd723490ba2dc09b299682c10b38db38f159728bcaae8c591b8af2f22d/cryptography-50.0.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:e722f16708d854fe924790e051061f6704a472c3bac347b6fd88033ea8dd0dc5", size = 4748483, upload-time = "2026-07-31T14:24:30.254Z" }, - { url = "https://files.pythonhosted.org/packages/1d/dd/7c77d26285cc7f6991efce64a0f5b4f9383bfa5dd8c5033003eaf7db4cdb/cryptography-50.0.0-cp39-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:d764dcf130c428ef66786f866dd750f53182bc608813489915e9fc106bb0c82f", size = 5367599, upload-time = "2026-07-31T14:24:32.457Z" }, - { url = "https://files.pythonhosted.org/packages/46/c9/f60aed34c013f317f92817b6c171c2d22a78270fa41109bd4b08af26b194/cryptography-50.0.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:105110f43a471dbd0060b9c9516cb8a6a79233631a04cc2ba16f28323ac6e025", size = 4762647, upload-time = "2026-07-31T14:24:34.599Z" }, - { url = "https://files.pythonhosted.org/packages/be/f3/f9a0173b139372c3a48ed98154b45cc6b9de17c789d5ab552e621c293609/cryptography-50.0.0-cp39-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:828743d939e9629bc267b8e2d08d8bb67cd4319c771a33d4b18b22dd8fb7440a", size = 4385197, upload-time = "2026-07-31T14:24:36.647Z" }, - { url = "https://files.pythonhosted.org/packages/d8/36/83bb81f6e569bc38e1e4a7bc80f29b46bb9601920bc455fc8e888f5d5742/cryptography-50.0.0-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:2a8183b489dc1f7f80f135780fadc1108f14b31b8a40411c7a5b17425f65f28b", size = 4748095, upload-time = "2026-07-31T14:24:39.493Z" }, - { url = "https://files.pythonhosted.org/packages/6b/16/d3008eff98c764979865834c3d386d4fd041b5f52e7f34fc29ac1a5eb515/cryptography-50.0.0-cp39-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:6e7d61120573a7f2cd94cc095f9e81f6967c61ccdf194285aa143ecec8e0b708", size = 5325948, upload-time = "2026-07-31T14:24:41.556Z" }, - { url = "https://files.pythonhosted.org/packages/9c/f8/d97f9603efda3888187bfdb893f26c41be4735c10631d05d284ee6b047c4/cryptography-50.0.0-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:37fdb0d0111f1e2ff07139dfb79f1b49531f8e213c46f1163dd7642979b58c47", size = 4762400, upload-time = "2026-07-31T14:24:43.636Z" }, - { url = "https://files.pythonhosted.org/packages/64/a2/4615c8f7d81a00b1d6e6afe19f694e1543582349fb5f4076f6cb5dc36485/cryptography-50.0.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:c87f62a3d3b9888ed0fdde100ec06aa61ca9cd44bad9057d1dff9a516b5f5bb9", size = 4878208, upload-time = "2026-07-31T14:24:45.522Z" }, - { url = "https://files.pythonhosted.org/packages/d2/1a/efcfb02f91407149a0dacffffab791f7e19bf6385f63b3666dc8b5e5c9c8/cryptography-50.0.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:65c2c3add92b45fd0709db8594536aea39c2a67af0e27ffcf049c498501140b7", size = 5037050, upload-time = "2026-07-31T14:24:47.697Z" }, - { url = "https://files.pythonhosted.org/packages/57/30/4a22984d4f1bdfb8c054f07a92bc176b97a3134cc1d6c4b3bffb1f3688b4/cryptography-50.0.0-cp39-abi3-win_amd64.whl", hash = "sha256:d24fead1d4d076e1bfb006dcec392074a3cd8d7b4fc8a595aa64073b2b7a96ba", size = 3874135, upload-time = "2026-07-31T14:24:50.085Z" }, - { url = "https://files.pythonhosted.org/packages/9d/3e/e54cde8c01631a5a8226ccd617eab9e57fd5cfdad90f1a9e6bb570794631/cryptography-50.0.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:5e34edd123674534acd70147f0ca331eaa2c74e6325fb2028c886aa26ba0b68c", size = 3963170, upload-time = "2026-07-31T14:24:51.968Z" }, - { url = "https://files.pythonhosted.org/packages/01/b6/0b9e125e90f3d2dcf599a218a899cda7326a3158cfa258723f0b398b08f6/cryptography-50.0.0-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:8eb5e1172eb569ea8a872796576e6a67c276351728b6455d5beb01242b027c6a", size = 4692441, upload-time = "2026-07-31T14:24:53.743Z" }, - { url = "https://files.pythonhosted.org/packages/53/c9/a5151588710785a96d7bc4de27d4cd62f263bbbcb203cfe29df537eb6505/cryptography-50.0.0-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:910d11e1a385c654bf738bf3e6b8e6ed5de0f5610fcae2be9e5b398d8081d20e", size = 4699810, upload-time = "2026-07-31T14:24:55.746Z" }, - { url = "https://files.pythonhosted.org/packages/c7/1a/15b92b25eb6ce3089cd49377ae990a0f3ad485a510f968aed1f19dbdcdf2/cryptography-50.0.0-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:62598a8a57f815db4c6259a4e97d857dab56697e7de8e8ab02352ab74da1995d", size = 4691924, upload-time = "2026-07-31T14:24:58.082Z" }, - { url = "https://files.pythonhosted.org/packages/62/15/219075012ab13e8905f3cd572204f4acb4b111df787104346b9bc0cea789/cryptography-50.0.0-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:07479a1cb08219ab719147e742e76090c9c773321959bb94946fffdd397a6437", size = 4699593, upload-time = "2026-07-31T14:24:59.951Z" }, - { url = "https://files.pythonhosted.org/packages/8e/b5/c2c5fce26f0ee40d21bafe7f191d29a34b35a65ac4fe8a1191d1983612e9/cryptography-50.0.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:c99c003e088647b8a5b7c145d6f78c335f6348332b62e142d411c4b63d1460b9", size = 3813796, upload-time = "2026-07-31T14:25:02.298Z" }, +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]] @@ -451,27 +443,27 @@ wheels = [ [[package]] name = "google-auth" -version = "2.56.3" +version = "2.57.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cryptography" }, { name = "pyasn1-modules" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/db/4c/fa42116a48bab3f7a143cf5042ecff7df9c8b73f8a376203cd534d1dc966/google_auth-2.56.3.tar.gz", hash = "sha256:40e229fc901f0a305b553050e5fce562d509bee0435be053abfa91582b51b90c", size = 367110, upload-time = "2026-08-06T06:24:01.36Z" } +sdist = { url = "https://files.pythonhosted.org/packages/41/64/55f316b729f92a552d26e00aa3b1542b2e149d0a5efe2842afff0cac7af7/google_auth-2.57.0.tar.gz", hash = "sha256:9b4f96d6a1feb5f7201231f47cfb3de08d8f176f8a61f9e461555116e95a8789", size = 370794, upload-time = "2026-08-25T19:18:26.419Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/bc/b3/6117b2f24065cd7e2c4f140e9a193e215f089ca8ba314cf91eb9d0b7fe0a/google_auth-2.56.3-py3-none-any.whl", hash = "sha256:8ec438808f813ad034535000261eed1067475d229d05bbf4216e78c3f2362e53", size = 259116, upload-time = "2026-08-06T06:22:51.788Z" }, + { url = "https://files.pythonhosted.org/packages/00/f3/8508a702c094af5f6e89773f4dfdeee74913df0f41a02c21b5e7dc3d75cd/google_auth-2.57.0-py3-none-any.whl", hash = "sha256:180dafe015cfb62193bea26b677500fab5b9fd51a1e825ebf3ad9b182047ae59", size = 259728, upload-time = "2026-08-24T21:55:08.449Z" }, ] [[package]] name = "googleapis-common-protos" -version = "1.75.1" +version = "1.75.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "protobuf" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/72/73/74bcab964c9a7a61f2bb71e8179b0f13e6fa98f7ce00fd168aab291e4a2e/googleapis_common_protos-1.75.1.tar.gz", hash = "sha256:d3042c6c5a2d4e67113104d6b6818b59b6bd92a197f2a91508e801fe815cf071", size = 150967, upload-time = "2026-08-06T06:24:51.972Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c0/90/fb8f1c84537fbf210c1f53a53ae473a805f6599c5a40b93c1bbadd211f7a/googleapis_common_protos-1.75.2.tar.gz", hash = "sha256:8829a3d1e4508c5b7b9a6b9525f7fccff611f8531644579a76466c29295d4bb2", size = 154083, upload-time = "2026-08-25T19:19:13.028Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/9a/51/186c02b8549b69ccda44429cf6ff5081e4b61a602ddfe6a8020d1be31d1b/googleapis_common_protos-1.75.1-py3-none-any.whl", hash = "sha256:28a1934bcd33b9c9da66ac301a0a4227e3367f095a17d0375cb98f0a09d93b79", size = 300626, upload-time = "2026-08-06T06:23:46.696Z" }, + { url = "https://files.pythonhosted.org/packages/47/5b/1c9e55363c3b1890a98cae813de5b4ea327845756cd8fb7ee690140c7eac/googleapis_common_protos-1.75.2-py3-none-any.whl", hash = "sha256:6b83302f554ea93a0f48409c7fc2050f954bcbcddb7e3a9c76d4a823cb22920e", size = 307002, upload-time = "2026-08-25T19:18:08.927Z" }, ] [[package]] @@ -513,11 +505,11 @@ wheels = [ [[package]] name = "idna" -version = "3.18" +version = "3.19" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/cd/63/9496c57188a2ee585e0f1db071d75089a11e98aa86eb99d9d7618fc1edce/idna-3.18.tar.gz", hash = "sha256:ffb385a7e039654cef1ab9ef32c6fafe283c0c0467bba1d9029738ce4a14a848", size = 196711, upload-time = "2026-06-02T14:34:07.794Z" } +sdist = { url = "https://files.pythonhosted.org/packages/5f/f7/abb373e5757eaec4b922b92f97ec8d6d7e057cf06778247604fbc4e7c3f3/idna-3.19.tar.gz", hash = "sha256:5e0811a4383b21dc5838069f801c4fb62113b7447663d2530d2bd6e77b49bf15", size = 215237, upload-time = "2026-08-18T05:14:24.27Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/1e/5e/d4e9f1a599fb8e573b7b87160658329fbf28d19eac2718f51fc3def3aa5a/idna-3.18-py3-none-any.whl", hash = "sha256:7f952cbe720b688055e3f87de14f5c3e5fdaa8bc3928985c4077ca689de849a2", size = 65455, upload-time = "2026-06-02T14:34:06.319Z" }, + { url = "https://files.pythonhosted.org/packages/57/b0/0e52c878c53f245edd3a11020f20979b3f490f245af532c7cae3027754b5/idna-3.19-py3-none-any.whl", hash = "sha256:815e7be7a7806d54abb586dc943addc79e8b2ee16915059658cbeff4b1b43bf4", size = 68550, upload-time = "2026-08-18T05:14:22.343Z" }, ] [[package]] @@ -540,14 +532,14 @@ wheels = [ [[package]] name = "proto-plus" -version = "1.28.3" +version = "1.28.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "protobuf" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/26/6a/056256feb4bd000869aba5c16cf2aa911572ca2a2feb185f86e457b5171e/proto_plus-1.28.3.tar.gz", hash = "sha256:5f91b30dafa6bb38d432c5557a6ee1d35ffd40b4b1e0e3ca27260448560b91d9", size = 58051, upload-time = "2026-08-06T06:24:55.581Z" } +sdist = { url = "https://files.pythonhosted.org/packages/40/a6/4fbadcc2044034449b3f8f0ce82dcf3005d53f37c136642103fd4836a31c/proto_plus-1.28.4.tar.gz", hash = "sha256:5ff7ecad828e032a491fcb86947801768e32237f99dd049b649965b892ae9a63", size = 58679, upload-time = "2026-08-25T19:19:15.102Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/61/3a/cfee3c50294f55a2f0f9575052dec2c2a48891ad4b1c2a133b05a87026cd/proto_plus-1.28.3-py3-none-any.whl", hash = "sha256:dc76880b8ee951cca002098574376cf71e055f9f16d9ba6570fb8a06f726d281", size = 50795, upload-time = "2026-08-06T06:23:50.653Z" }, + { url = "https://files.pythonhosted.org/packages/41/5d/0f04b85dafdc3250ced7f2592efc17dce7f40712e941e9632202481e600d/proto_plus-1.28.4-py3-none-any.whl", hash = "sha256:4b01341272f8a348db3f003b6143109f83ab43091019d5181b3fcdf500ab32aa", size = 50797, upload-time = "2026-08-25T19:18:12.338Z" }, ] [[package]] @@ -808,15 +800,15 @@ wheels = [ [[package]] name = "uvicorn" -version = "0.52.3" +version = "0.52.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "click" }, { name = "h11" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/2e/28/64ca011edf31c715b4fad359c587ea52391aaffa125065695590241ff617/uvicorn-0.52.3.tar.gz", hash = "sha256:18857b9e6579300be55c91c0a1cfd37d9a2cf0cabea33b88275f199eb73b8b58", size = 100621, upload-time = "2026-08-13T16:50:02.899Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f2/0f/3f86e61397dd33bf2ccf28188c40db6a740658aeebbbf6e7dbc101a1f487/uvicorn-0.52.4.tar.gz", hash = "sha256:73acfee47a0b133c5de13d219492d62d8a31e935f4fe6e41a232451a15379f86", size = 100627, upload-time = "2026-08-19T06:27:41.821Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/dc/2b/ebd108734a8204c6b4b93c681c9a38c5273b3ccd5d129fee4ffc1d97772c/uvicorn-0.52.3-py3-none-any.whl", hash = "sha256:116af2710dbf47c80f463cd20ee4884b6662f4c9f227d797ddc7279d2fcc2c7c", size = 79859, upload-time = "2026-08-13T16:50:01.323Z" }, + { url = "https://files.pythonhosted.org/packages/f1/79/4a20b54ab0491485ccd8c077db2d39187c7f12b3e15485d38a7be37c81b4/uvicorn-0.52.4-py3-none-any.whl", hash = "sha256:f86e41a149d7d05a9969337e3946a9c171c06a5d42680896daaba624aeac8da1", size = 79871, upload-time = "2026-08-19T06:27:40.36Z" }, ] [[package]] diff --git a/labs/a2a/docker-compose.yml b/labs/a2a/docker-compose.yml index 2bde2f3..6234fbc 100644 --- a/labs/a2a/docker-compose.yml +++ b/labs/a2a/docker-compose.yml @@ -4,7 +4,7 @@ # TLS checks that make a production Keycloak safe to expose. services: keycloak: - image: quay.io/keycloak/keycloak:26.7.1 + image: quay.io/keycloak/keycloak:26.7.2 container_name: a2a-keycloak command: ["start-dev", "--http-port=8080"] environment: diff --git a/labs/a2a/evidence/card-forecast-signed.json b/labs/a2a/evidence/card-forecast-signed.json index 342512e..a33ed0d 100644 --- a/labs/a2a/evidence/card-forecast-signed.json +++ b/labs/a2a/evidence/card-forecast-signed.json @@ -73,7 +73,7 @@ "signatures": [ { "protected": "eyJhbGciOiJSUzI1NiIsImtpZCI6ImZvcmVjYXN0LWNhcmQta2V5LTEiLCJ0eXAiOiJKV1QifQ", - "signature": "W0JL__8NOKsDw9Km7NkzLvGJOFvntiQ1RfQECRR8IxF5DKECQRK5XYpSP0oeo3_Tjsh5pifFNzPNIG7-Q6tCTbVYlc4NygoqXqxPXXSSaT4PdlUEitik9ZfcQtk3QBvWSoNsyNfYXPBVHW-4IQI4oLCRQhlJ-VEKI8xD23w0HfC38lxsIrIOBZ4nDtKlaKtV_f4cKw7eXaNp2XugZ9A_4ZzbzVvX36Lv_wT3GjOKIx8MHd8XyYFmtYlSiUOIOb0bRivEuDQTc8s_GVGxBNxhHH3KDEDBLVwqt0rF3l7jMdT0mXqAwPDzIfC7HEKzBwgQ3Me341dH1lAjP8ZxSipGwA" + "signature": "iGgJw56ikjjHwaMzSxmpZw-XvbIfmRfYePQI-NzAFKqzGFH555UktjmyWBg5xsMxDezzDE8Xg6XK-776ecj409XVc-W0xUgjNH8SCVnJ5g_Rj0TPvmMDaOB0nJaERAgJV0suuyeLBNSFuRPgQwTN_MwuYbcWyN3gmJr1DbcCOKwXH5WbpVpJVnvhFgOfpQNeOBjiRTdSCGEYxC7qpk3iQ2y829OFl4VgRwFymVgCRmYfqH012Nq2cO33Ta1ogysxClvQdfZufKB-bH6GX13iQsRVHqXkeaOdll_MgjkyEMexZvPQanCkYAc4q2JQ88-tx21do8FHnRGx6WkPy64KuQ" } ] } diff --git a/labs/a2a/evidence/keycloak-discovery.json b/labs/a2a/evidence/keycloak-discovery.json index 41dc1a1..fd622ba 100644 --- a/labs/a2a/evidence/keycloak-discovery.json +++ b/labs/a2a/evidence/keycloak-discovery.json @@ -250,20 +250,20 @@ "claims_parameter_supported": true, "scopes_supported": [ "openid", - "service_account", - "forecast:write", - "roles", - "web-origins", - "acr", - "forecast:read", "phone", + "organization", + "forecast:read", + "web-origins", + "service_account", + "email", "address", - "profile", - "basic", "microprofile-jwt", - "email", - "organization", + "roles", "offline_access", + "forecast:write", + "profile", + "acr", + "basic", "trip:plan" ], "request_parameter_supported": true, diff --git a/labs/a2a/evidence/keycloak-jwks.json b/labs/a2a/evidence/keycloak-jwks.json index 6703bcd..28e436d 100644 --- a/labs/a2a/evidence/keycloak-jwks.json +++ b/labs/a2a/evidence/keycloak-jwks.json @@ -1,29 +1,29 @@ { "keys": [ { - "kid": "ehX5X2-wLlEYUIHa83abi-Dnu_GTPMx9gm5Y0UZ90dA", + "kid": "ocoqrf5fuzZ1sjbC5PtdOVuKqURLTTAuob5BvI-Owtw", "kty": "RSA", "alg": "RSA-OAEP", "use": "enc", "x5c": [ - "MIICpzCCAY8CBgGgCyxvijANBgkqhkiG9w0BAQsFADAXMRUwEwYDVQQDDAxhMmEtd29ya3Nob3AwHhcNMjYwODE2MTUyMjM2WhcNMzYwODE2MTUyNDE2WjAXMRUwEwYDVQQDDAxhMmEtd29ya3Nob3AwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCxbXY7nREE688AiWmaFi1Qw9exy/xanoU0XCjWGdLeM0is4pT6J+0s+G0NilBariz+FOZpX0+/RCMh/ewQ09jeEpWt3mTjUEjwxnU3pKajiRZjh5F8VrBgjMEmMH2OPeKyjRiHB4rTGMQBipl1K1UN1vryigDDPXsJNyRZWhlFRKsixShshP/al1Ci1cTqntdhYP5/nrdXzIC5SR28b8WNVpW1zCaCJZsMW+uwrGvqhajYrKFDDUyBOZoWKJ15Ekc8OPe/Ss2dM4wOz3sYJQxpJ2q/MJIrQXBtI0LEbT0woRoSZjKHMqz7oA2toLkAQV8UVtWPLeb197cj77nk5vfLAgMBAAEwDQYJKoZIhvcNAQELBQADggEBAGc5m0taGGm4tH+8qL3sFF3/b4yyxMcb0+isOjNorVXNg2VDUgTPgyoio3bTfDRw4z6jEUhTT4U3TXX4ZUabQS12+pojG2djE6lMLVjpIdWwcSGRhyXESsDroWF3x7E3F6okxGhjXQO6+JXl7zbHCY+/ZaYnrmdbEpgkTZlKpWZzNCD8cCeUen48Mri3euFNBlWmKC/nl9XE2arJqoXqzY159hm3H82eGrHtRT+K6kU09AhsMy8/3qn2B0vWNhq/Lq70fu0jbVB+4gQwuiMXNFAHSpBDJyHqB6tX38aM8rUnOgvaMJAISty2g5jYXxJICBBBeWBy/BWauKUf7UYu1lk=" + "MIICpzCCAY8CBgGgZwa8oDANBgkqhkiG9w0BAQsFADAXMRUwEwYDVQQDDAxhMmEtd29ya3Nob3AwHhcNMjYwOTAzMTEyNjI5WhcNMzYwOTAzMTEyODA5WjAXMRUwEwYDVQQDDAxhMmEtd29ya3Nob3AwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCj3v9YTwLIJxMklDNOZ/3GjqSWcBlLiTKfA/tmzcdBSrc+m9+73QgRarHsgr7X0F+Psd/BhPFm6mu8qpgKeZH7luN8IH7L5yqVKqAx+rNGPkaTtSpFI4HEZPy8APij2XZSNMcAgh2MAOSxj55rSvZ1QlwUbljpVD+Ri41rbih57MztvKJXYpeBu2bu9U25euM5V2e6zX24s5DzKrb678PI1vUPq8RzAGs4+EggfpWAaYu4xVr2fZjB+iYYIpy8AwlHOlDRlXLmkjm5wpg6iehHt12WlrmjwSLo5eYcqftDOjFyJiq74argxbHHDeoVGRL16oC6ksfFU1HN4GTjWUtPAgMBAAEwDQYJKoZIhvcNAQELBQADggEBAF3GBgLTSN+ztBkjgAjxXSAsgEs0p0JHX7P5csxuVGx54mcW/D1vhCvzhXKoeFdSiHUSSJ56Xyo+3SIDXoBWTTnWDhHrUWGLjfWHwdVVzIkDfQHzGSDEYAMESRUmWLEgIQvjCndex3oVXFF5Wsmh68eaaWxWAW+/NlW3VG/L2p3MNR+kTujD5An8H+cp8L/I6WH43zS9DdehpS1Ku0nHwWWcWpPmJKd3wwO2orFsFCG/w9FI3a2wJAsneY0Uz9amR8Mexo+R7R2qBKoL7fhcT4/8rNb8UaTfIRf9mbfofbQLNk2UDbZn71cE9J0EAP9V8W0/MJKEK3SxEAB35CYEUlw=" ], - "x5t": "DQHpsjbDQ-XfERN5RI42od-yUy8", - "x5t#S256": "3oOee_hQk_RXUnhf9yV2-43XCK7_-zdbDK6bmnAtdWE", - "n": "sW12O50RBOvPAIlpmhYtUMPXscv8Wp6FNFwo1hnS3jNIrOKU-iftLPhtDYpQWq4s_hTmaV9Pv0QjIf3sENPY3hKVrd5k41BI8MZ1N6Smo4kWY4eRfFawYIzBJjB9jj3iso0YhweK0xjEAYqZdStVDdb68ooAwz17CTckWVoZRUSrIsUobIT_2pdQotXE6p7XYWD-f563V8yAuUkdvG_FjVaVtcwmgiWbDFvrsKxr6oWo2KyhQw1MgTmaFiideRJHPDj3v0rNnTOMDs97GCUMaSdqvzCSK0FwbSNCxG09MKEaEmYyhzKs-6ANraC5AEFfFFbVjy3m9fe3I--55Ob3yw", + "x5t": "Hjpr1SIziYlcXaao3oYAgO9MwxY", + "x5t#S256": "RwLBkmApe1ScOjl3ZcCJSJQ_KmtVhPIqkkW3KXWKiCA", + "n": "o97_WE8CyCcTJJQzTmf9xo6klnAZS4kynwP7Zs3HQUq3Ppvfu90IEWqx7IK-19Bfj7HfwYTxZuprvKqYCnmR-5bjfCB-y-cqlSqgMfqzRj5Gk7UqRSOBxGT8vAD4o9l2UjTHAIIdjADksY-ea0r2dUJcFG5Y6VQ_kYuNa24oeezM7byiV2KXgbtm7vVNuXrjOVdnus19uLOQ8yq2-u_DyNb1D6vEcwBrOPhIIH6VgGmLuMVa9n2YwfomGCKcvAMJRzpQ0ZVy5pI5ucKYOonoR7ddlpa5o8Ei6OXmHKn7QzoxciYqu-Gq4MWxxw3qFRkS9eqAupLHxVNRzeBk41lLTw", "e": "AQAB" }, { - "kid": "Gi_gy-v-VfnR4SDyoRFZ0G6EOFUoAYiuiD4mXypaKkw", + "kid": "k2Z0L7M-657oP-A-KW3xi3ccZTT3jZ0iHjPOAfa3cPE", "kty": "RSA", "alg": "RS256", "use": "sig", "x5c": [ - "MIICpzCCAY8CBgGgCyxvDjANBgkqhkiG9w0BAQsFADAXMRUwEwYDVQQDDAxhMmEtd29ya3Nob3AwHhcNMjYwODE2MTUyMjM2WhcNMzYwODE2MTUyNDE2WjAXMRUwEwYDVQQDDAxhMmEtd29ya3Nob3AwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDqQgvgLDaMsq+ZVMT6ZkqD7dtxuaK0CChHokuOIw7BBSyuWoMOHsQNLWQcU7zJ7kfMRxL1Xi7gnGyrQRjTUaej9qKg6ZytSD+D01Gsw5bt9etNIG99S9Du974pzvquNosfAyN8gB8rvGXPvnPVe/elxNZOfiVo9xnqNuyAlmJX62N7JCg1MfWLye9BQpWzeEQ4M+ZigDjsio/mKuEXTVTmyq72rrFm+gQhPlZ4CRZYQNgDWeMO8RUH9E2OKgMaqeno+NwdUf25M8ZLyTYlQ7LSimyrwo+03vxiznTpI98fsBoTBOnmFg5KVrSbxOolFZilz0YfhyqTYihs99mym97XAgMBAAEwDQYJKoZIhvcNAQELBQADggEBAAbKtETLtfZpKUgOEp0ejSSr4JTdsY7XdtD4HWEZn75tNfm7S92HakZOv8d47pmgJTm+jZfScBX3Coawvr5t4Dcx9eqqUtP+ThbfYXah/58plrmFmfZe/UBONYzrCI9xvYxmu9Owq2f6lj8pR1Oe0FpkKjnt2q6S7UMBDCRPY5wzmIxHuICYt4iPYTru4/gUEU/XdkLsqfCF7mLGctovS2U2tQdoEmKgGtp8v4uNWjK04rnnRyeQM6+XF8GKsfMPT9YYaX7j+nn67n65GFpSmer6rX871bAiLKaZIMWlQ8/m69DOtcCp+kO7vQOfhdzP6eg4JwOewJMbNjpL9vWXhsU=" + "MIICpzCCAY8CBgGgZwa8ajANBgkqhkiG9w0BAQsFADAXMRUwEwYDVQQDDAxhMmEtd29ya3Nob3AwHhcNMjYwOTAzMTEyNjI5WhcNMzYwOTAzMTEyODA5WjAXMRUwEwYDVQQDDAxhMmEtd29ya3Nob3AwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC8mfAoIVxeGRZ7B3bJ34TOqKbM3yDSlv281R2HnwSCu5lnRS7+enoLXzi5QA8AvRqtLudhC2xMKSE5V1SM3VwaDhzHgVw7BUGShM+K7P5+md5Ck663+IvDHyLI0Pc5pfMPv9tEn8aFlNSTHsWxDzwUGtARYf6eMbrnxTVEDgoZIukbYXqMhFiRMPVxRltBSoj3A88QDq+U4wDfhv/5jLWp1+2haU7vS12G8xMD3Bwqgp3mNzfZysizZBIrVUPWHIB2YxfokG3qgw1eCV5RS+I/NRxwf024BUNaYrtCEkbZn54qQgu+ToD52GysuPgoGsaWo2R0d3Dkumu7ckwewIazAgMBAAEwDQYJKoZIhvcNAQELBQADggEBADanMXlP+VPUm8APWbtOKfD3JIL6qyxUft5EuD1zYzt+/y3h7UV+aRvzOFIMqEEXHULAFX6CU2isl2Z3ZQ1oHPypk1wtX0byswNXDWdTgXzGnr8jTIBro2dF91+zJ/4mryYJRHcUw/tdJqidVivPLBzjxjDd355lEQhUvdQpmR761IKkQLaq68V34DuKgxXmj4FIAg+vkL0b8OCcRf5MrTnjVGkIWKm9yiwXpsPD5qYTP6GvO/htUt6/QRBZ3i0iLhqgDEYW3ulS/zAIyXB7VX+y/YQLXR0FzX7MDcr9n8LqSZV8wI2LDZuN4Nym/Yuc1TAJhUk3aHpd1QKdF8oU3Sg=" ], - "x5t": "QDCKj996VPVocflby0nS2Ug2n9U", - "x5t#S256": "ZhhPzyXa2twTLxUNvX1El_zCmx5puEQhj4hEvqMtbMM", - "n": "6kIL4Cw2jLKvmVTE-mZKg-3bcbmitAgoR6JLjiMOwQUsrlqDDh7EDS1kHFO8ye5HzEcS9V4u4Jxsq0EY01Gno_aioOmcrUg_g9NRrMOW7fXrTSBvfUvQ7ve-Kc76rjaLHwMjfIAfK7xlz75z1Xv3pcTWTn4laPcZ6jbsgJZiV-tjeyQoNTH1i8nvQUKVs3hEODPmYoA47IqP5irhF01U5squ9q6xZvoEIT5WeAkWWEDYA1njDvEVB_RNjioDGqnp6PjcHVH9uTPGS8k2JUOy0opsq8KPtN78Ys506SPfH7AaEwTp5hYOSla0m8TqJRWYpc9GH4cqk2IobPfZspve1w", + "x5t": "Pu4hl7qqzllW1Dk9DuUXLP2K-TU", + "x5t#S256": "Qh1sM9YufvsJWVhcidgj7UGgb250i_nPuctkJMy6xm8", + "n": "vJnwKCFcXhkWewd2yd-EzqimzN8g0pb9vNUdh58EgruZZ0Uu_np6C184uUAPAL0arS7nYQtsTCkhOVdUjN1cGg4cx4FcOwVBkoTPiuz-fpneQpOut_iLwx8iyND3OaXzD7_bRJ_GhZTUkx7FsQ88FBrQEWH-njG658U1RA4KGSLpG2F6jIRYkTD1cUZbQUqI9wPPEA6vlOMA34b_-Yy1qdftoWlO70tdhvMTA9wcKoKd5jc32crIs2QSK1VD1hyAdmMX6JBt6oMNXgleUUviPzUccH9NuAVDWmK7QhJG2Z-eKkILvk6A-dhsrLj4KBrGlqNkdHdw5Lpru3JMHsCGsw", "e": "AQAB" } ] diff --git a/labs/a2a/evidence/negative-tests.txt b/labs/a2a/evidence/negative-tests.txt index ae7fda6..521ec40 100644 --- a/labs/a2a/evidence/negative-tests.txt +++ b/labs/a2a/evidence/negative-tests.txt @@ -18,4 +18,4 @@ iss=rogue-realm HTTP 401 {"error":"invalid_token","error_des not-a-jwt HTTP 401 {"error":"invalid_token","error_description":"Token failed verification"} === Control: the request that SHOULD work === -aud+scope correct HTTP 200 {"result":{"message":{"messageId":"73a156f3-8039-44ee-960c-dfac7662de1d","role":"ROLE_AGENT","parts":[{"text":"Lisbon: C +aud+scope correct HTTP 200 {"result":{"message":{"messageId":"a228c8af-fb0d-4927-a1e9-d53f1cb33c2c","role":"ROLE_AGENT","parts":[{"text":"Lisbon: C diff --git a/labs/a2a/evidence/rogue-card.txt b/labs/a2a/evidence/rogue-card.txt index b33be7e..8f0110b 100644 --- a/labs/a2a/evidence/rogue-card.txt +++ b/labs/a2a/evidence/rogue-card.txt @@ -4,7 +4,7 @@ client saw: Sunny, 25C ==================================================================== ROGUE AGENT RECEIVED A REQUEST stolen bearer token (1339 chars): eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6... - body: {"method":"SendMessage","params":{"message":{"messageId":"73f0caa3-6d41-4aae-81fc-149eeece9d23","role":"ROLE_AGENT","parts":[{"text":"forecast for Lisbon"}]},"configuration":{}},"id":"d6ad3a15-46b6-4b + body: {"method":"SendMessage","params":{"message":{"messageId":"0d368c61-3ba0-4aa1-a820-85cc4efb791c","role":"ROLE_AGENT","parts":[{"text":"forecast for Lisbon"}]},"configuration":{}},"id":"b4f69f4a-596e-45 ==================================================================== ### DEFENSE: same call, signature verification on diff --git a/labs/a2a/evidence/tokens-decoded.txt b/labs/a2a/evidence/tokens-decoded.txt index 7efd81a..d2ef43d 100644 --- a/labs/a2a/evidence/tokens-decoded.txt +++ b/labs/a2a/evidence/tokens-decoded.txt @@ -6,9 +6,9 @@ "account" ], "azp": "agent-a-forecast", - "scope": "email trip:plan profile", + "scope": "profile email trip:plan", "typ": "Bearer", - "exp": 1786894194 + "exp": 1788435332 } --- agent-b-planner { @@ -18,9 +18,9 @@ "account" ], "azp": "agent-b-planner", - "scope": "email forecast:read profile", + "scope": "profile forecast:read email", "typ": "Bearer", - "exp": 1786894194 + "exp": 1788435332 } --- workshop-cli { @@ -30,9 +30,9 @@ "account" ], "azp": "workshop-cli", - "scope": "email trip:plan profile", + "scope": "profile email trip:plan", "typ": "Bearer", - "exp": 1786894195 + "exp": 1788435332 } --- agent-c-stranger { @@ -42,7 +42,7 @@ "account" ], "azp": "agent-c-stranger", - "scope": "forecast:write email profile", + "scope": "forecast:write profile email", "typ": "Bearer", - "exp": 1786894195 + "exp": 1788435332 } diff --git a/labs/a2a/evidence/versions.txt b/labs/a2a/evidence/versions.txt index 7a3722a..f44167d 100644 --- a/labs/a2a/evidence/versions.txt +++ b/labs/a2a/evidence/versions.txt @@ -1,6 +1,6 @@ -captured: 2026-08-16T15:24:54Z +captured: 2026-09-03T11:30:31Z a2a-sdk 1.1.2 protocol 1.0 card path /.well-known/agent-card.json -quay.io/keycloak/keycloak:26.7.1 -Python 3.12.11 +quay.io/keycloak/keycloak:26.7.2 +Python 3.13.12 diff --git a/labs/acp-goose/README.md b/labs/acp-goose/README.md index a15219e..cf302da 100644 --- a/labs/acp-goose/README.md +++ b/labs/acp-goose/README.md @@ -330,46 +330,71 @@ You should get a `write_file` call carrying `notes.txt` and `hello`. That JSON i > [!WARNING] > `qwen3:0.6b` still fires the tool but garbles the schema, and an agent wired to a model like that fails in ways that look like protocol bugs. Tool-calling fidelity is a model property; check it before you blame the wire. -Notice the `"think": false` in that request. `qwen3:1.7b` is a reasoning model: left alone it writes out its thinking before it answers, and on the sandbox's two CPU cores that takes minutes even for a trivial reply. We could set the flag because we wrote the HTTP call ourselves. goose doesn't currently have a way to send it, so before we hand the model to goose we turn thinking off one level down, in the Ollama inference runtime. - -An Ollama model carries a prompt template, and qwen3's already knows how to skip thinking; that switch just only fires when the caller asks for it. Copy the model's own definition, flip the switch on permanently, and build it under a new name: +Notice the `"think": false` in the previous request. `qwen3:1.7b` is a reasoning model: left alone it writes out its thinking before it answers, and on the sandbox's two CPU cores that costs about twenty seconds for a one-word reply and several minutes for a turn that calls tools. We could set the flag because we wrote the HTTP call ourselves. goose has no way to send it, so before we hand the model to goose we build a copy of the model with the switch baked into its template: ```bash ollama show --modelfile qwen3:1.7b > Modelfile.nothink sed -i 's|^FROM /.*|FROM qwen3:1.7b|' Modelfile.nothink sed -i 's|{{- if and $.IsThinkSet (eq $i $lastUserIdx) }}|{{- if (eq $i $lastUserIdx) }}|' Modelfile.nothink sed -i 's|{{- if $.Think -}}|{{- if false -}}|' Modelfile.nothink +sed -i 's|{{ if and $.IsThinkSet (not $.Think) -}}|{{ if true -}}|' Modelfile.nothink +grep -q '{{ if true -}}' Modelfile.nothink ollama create qwen3-nothink -f Modelfile.nothink ``` -The first `sed` points the new model at the tag instead of a blob path on disk. The other two make the template take the no-thinking branch for every request, whatever the caller asked for. Because it reuses weights already on disk there's no download, and it finishes in about a second. - -Check that it answers without thinking: +Because it reuses weights already on disk there's no download, and it finishes in about a second. Check that it answers without thinking: ```bash -ollama run qwen3-nothink "say ok" +ollama run qwen3-nothink "Reply with only the word: pong" ``` ```console -Okay, I'm ready to help you with whatever you need. Let me know how I can assist you today! +pong ``` -No `Thinking...` block, and seconds rather than minutes. goose reads its provider from the environment, and a new shell knows none of this: +No `Thinking...` block, and the wait is loading the weights rather than generating; plain `qwen3:1.7b` spends about twenty seconds thinking first. + +
+Qwen3 is a thinking model, but we turned thinking off. Open this panel to go down the rabbit hole of how, and why + +Why: a 1.7B model on two CPU cores produces a handful of tokens a second, and thinking mode spends hundreds of them before the first word of the answer. On the sandbox, a turn that calls two tools takes around a minute with thinking off and five to seven minutes with it on. + +The [Qwen3 model card](https://huggingface.co/Qwen/Qwen3-1.7B#switching-between-thinking-and-non-thinking-mode) documents two ways to switch it off. A soft switch, by including `/no_think` in the prompt, which the model treats as a request. And a hard switch, by setting `enable_thinking=False` in the chat template, which starts the reply with an empty `` block so there's nowhere left to think. Ollama's copy of that template carries both, and flips both when a caller sends `think: false` on a request, which is what our hand-written call did. goose can't, so we bake both switches into a copy of the model instead. + +`ollama show --modelfile` prints the model's definition, template included, and each `sed` changes one line of it: + +- `FROM /usr/share/ollama/.ollama/models/blobs/sha256-…` becomes `FROM qwen3:1.7b`. The printed definition points at a blob on disk; pointing at the tag reuses the same weights without copying them. +- `{{- if and $.IsThinkSet (eq $i $lastUserIdx) }}` becomes `{{- if (eq $i $lastUserIdx) }}`. The template appended a thinking switch word to your message only when the caller had set `think`; now it appends one on every request. +- `{{- if $.Think -}}` becomes `{{- if false -}}`. That word was ` /think` or ` /no_think` depending on the request; now it's always ` /no_think`, the soft switch. +- `{{ if and $.IsThinkSet (not $.Think) -}}` becomes `{{ if true -}}`. The empty `` block opened the reply only when a caller sent `think: false`; now it opens every reply, the hard switch. +- `grep -q '{{ if true -}}'` fails the block if that last edit didn't land. `sed` exits 0 whether or not it matched anything, so without this a changed upstream template would build a model that thinks under a name that says it doesn't. + +The request no longer has a say. Send `think: true` to `qwen3-nothink` and you still get no thinking, because there's no branch left for the value to reach. + +> [!NOTE] +> A model this small still reasons in the open when a prompt gives it room to wonder what you meant. Ask it to "say ok" and you'll get a paragraph of deliberation with no think block around it. Direct prompts get direct answers. + +
+ +goose reads its provider from a config file, and a new shell knows none of this. Rather than editing the one in your home directory, the lab ships a goose profile and one environment variable tells goose to read it: + +```yaml +GOOSE_PROVIDER: ollama +GOOSE_MODEL: qwen3-nothink +OLLAMA_HOST: http://localhost:11434 +GOOSE_CONTEXT_LIMIT: 32768 +``` ```bash -export GOOSE_PROVIDER=ollama -export OLLAMA_HOST=http://localhost:11434 -export GOOSE_MODEL=qwen3-nothink -export OLLAMA_CONTEXT_LENGTH=32768 +cd ~/zenable-labs/labs/acp-goose && export GOOSE_PATH_ROOT="$PWD/goose" ``` -> [!TIP] -> `OLLAMA_CONTEXT_LENGTH` matters. Ollama defaults to a 4096-token context, a tool-calling agent blows past that on tool definitions alone, and goose then appears to ignore its own instructions because the context was silently truncated. +`GOOSE_CONTEXT_LIMIT` is what goose sends Ollama as the context window. Ollama's own default is 4096 tokens, a tool-calling agent spends that on tool definitions alone, and the failure looks like a model ignoring instructions when really the prompt was silently truncated. `GOOSE_PATH_ROOT` also moves goose's sessions and logs under `goose/` in this directory, so nothing in your own goose setup is read or written. This profile leaves goose's own extensions on: the file writes it asks our client for come from its `developer` extension. Either way, we end up in the same place: our proxy in front of real goose, denying writes while the model works. ```bash -cd ~/zenable-labs/labs/acp-goose && export PATH="$HOME/.local/bin:$PATH" +cd ~/zenable-labs/labs/acp-goose && export PATH="$HOME/.local/bin:$PATH" && export GOOSE_PATH_ROOT="$PWD/goose" python3 permissive_client.py -- \ python3 acp_policy_proxy.py --deny fs/write_text_file --audit /tmp/acp-real.jsonl -- \ goose acp @@ -388,7 +413,7 @@ If you took Adventure B and want the models gone: ```bash command -v ollama >/dev/null 2>&1 && ollama rm qwen3-nothink qwen3:1.7b || true -rm -f Modelfile.nothink +rm -rf Modelfile.nothink goose/data goose/state ``` Everything else we made lives in three places: temp files, the goose binary, and the cloned rig. Step out of the rig directory first (removing the directory you're standing in leaves your shell in a deleted location), then remove them all: diff --git a/labs/agent-identity/agent/pyproject.toml b/labs/agent-identity/agent/pyproject.toml index 3ea65ca..70fb2b2 100644 --- a/labs/agent-identity/agent/pyproject.toml +++ b/labs/agent-identity/agent/pyproject.toml @@ -15,3 +15,7 @@ dependencies = [ [tool.uv] package = false +# Adopt only packages published more than a week ago, so a compromised +# release has time to surface before a lab installs it. Enforced by the +# resolver, so `uv lock --upgrade` and a reader's `uv sync` obey it alike. +exclude-newer = "7 days" diff --git a/labs/agent-identity/agent/uv.lock b/labs/agent-identity/agent/uv.lock index 46bc484..991386b 100644 --- a/labs/agent-identity/agent/uv.lock +++ b/labs/agent-identity/agent/uv.lock @@ -2,6 +2,10 @@ version = 1 revision = 3 requires-python = ">=3.11" +[options] +exclude-newer = "0001-01-01T00:00:00Z" # This has no effect and is included for backwards compatibility when using relative exclude-newer values. +exclude-newer-span = "P7D" + [[package]] name = "agent-identity-workshop" version = "0" @@ -149,79 +153,67 @@ wheels = [ [[package]] name = "click" -version = "8.4.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "colorama", marker = "sys_platform == 'win32'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/76/d4/81420972a676e8ffea40450d8c8c92943e7218a78fe9b64359836cc9876b/click-8.4.2.tar.gz", hash = "sha256:9a6cea6e60b17ebe0a44c5cc636d94f09bd66142c1cd7d8b4cd731c4917a15f6", size = 338000, upload-time = "2026-06-24T17:45:15.148Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/fb/e2/79c688af8b210d232694e31e59da9f6ec747bae31c3f5946e4e9b98860d5/click-8.4.2-py3-none-any.whl", hash = "sha256:e6f9f66136c816745b9d65817da91d61d957fb16e02e4dcd0552553c5a197b76", size = 119243, upload-time = "2026-06-24T17:45:13.73Z" }, -] - -[[package]] -name = "colorama" -version = "0.4.6" +version = "8.5.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +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/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, + { url = "https://files.pythonhosted.org/packages/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.0" +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/de/41/6cbdcf9142d00fe82836fbb51e503e58088575cf7a0fe1dbff6695bf0840/cryptography-50.0.0.tar.gz", hash = "sha256:eeac2acb5a20ed25e0ad6d1df9891a520b78b404266b6d11778f25d5d691a6c9", size = 880201, upload-time = "2026-07-31T14:25:10.11Z" } +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/c5/5c/59086b4aac5e879d38ddbcf74e4be7ade89cebc3eb199a55da998c3bb46a/cryptography-50.0.0-cp311-abi3-macosx_11_0_arm64.whl", hash = "sha256:031e2d5dd4bb9caa3ca9c82e5a197fd8ae680232cee62603d1a813f3f07e3d03", size = 4001252, upload-time = "2026-07-31T14:23:33.331Z" }, - { url = "https://files.pythonhosted.org/packages/57/ef/8f2df13c7216bcad3e1c74e07f6e193d93e998e114f524a53877c9af27ad/cryptography-50.0.0-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fd9192b7b70c573d7f214eb1ae35e00d359f6f5e4b27c7e21e30de1fc6204645", size = 4719554, upload-time = "2026-07-31T14:23:35.611Z" }, - { url = "https://files.pythonhosted.org/packages/d9/41/029086c34d91052fc3b88bcc8056f709a7c915c7a23b235a54eb800b1c97/cryptography-50.0.0-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:06a32a980526a6ab9a4b9bf8f7385800791e2bb960903cb6b530e4817509a3b7", size = 4702130, upload-time = "2026-07-31T14:23:37.635Z" }, - { url = "https://files.pythonhosted.org/packages/7d/ff/b6ce0954962e7f7b969f850a883744197bb3910bdfd7b6da162eab7d9f68/cryptography-50.0.0-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:a1b30560f2acc95aa8b2e06e716a13dbfc97314747b80d9707e307f77b40d6b3", size = 4725244, upload-time = "2026-07-31T14:23:39.471Z" }, - { url = "https://files.pythonhosted.org/packages/06/1e/63a1027cb7fec360a182208e1b7767d5aa1fe57be3d6aa856e69a321edc0/cryptography-50.0.0-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:8d89f3976b10b4ce31118de72329025f70d2c6ead14a8217c5514dd2c6d5a78f", size = 5342265, upload-time = "2026-07-31T14:23:41.286Z" }, - { url = "https://files.pythonhosted.org/packages/6b/72/a1116d683a6d7ece94590013882515de087edf9ef0e6292aae615a44df73/cryptography-50.0.0-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:b42a28c1844fd9de8f3f7d540e36b66f3a9c83fceac7170ebc7a6a19edd9dcae", size = 4734609, upload-time = "2026-07-31T14:23:43.139Z" }, - { url = "https://files.pythonhosted.org/packages/15/37/36a9c479bbe49acea2636c7fd3360d20f7b7e079c300352011c44850b181/cryptography-50.0.0-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:900131fafd8aead39ac7dd3a7e833be754c17a95cfd91221636949fe4eb0aa8a", size = 4356517, upload-time = "2026-07-31T14:23:44.939Z" }, - { url = "https://files.pythonhosted.org/packages/32/98/8a151d64367204cbc63ec65d37502f1d9c53cf4bfc6ec3c532614dbec60d/cryptography-50.0.0-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:07949c449a1abcf60d1ee6e88956d89404c7df3c8258f46589e912988e551987", size = 4724529, upload-time = "2026-07-31T14:23:46.93Z" }, - { url = "https://files.pythonhosted.org/packages/22/f6/ec13b470172126464a86bf54d2294a46d29837fc51ba3e45d4047946fb5e/cryptography-50.0.0-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:f89831ef99dd7dd169ab06d63a831adb9e20a87aac6d380266bbda5823349169", size = 5299852, upload-time = "2026-07-31T14:23:48.851Z" }, - { url = "https://files.pythonhosted.org/packages/da/3a/f05e32c99d440c9bb891ea0e36c9091891e36be5a9a87ab2ee6ea20729f6/cryptography-50.0.0-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:82148ec5bddac30b51a5b3c1945075f896fa022cb93f8e4a01e9f6ee95292c5f", size = 4734462, upload-time = "2026-07-31T14:23:50.861Z" }, - { url = "https://files.pythonhosted.org/packages/ca/dc/bd72b26be8953f80625f63151efd38eee71c76ca6cf591c08ff34615a79e/cryptography-50.0.0-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:1489e263a8048bb8b6a8bac662eb2d402ea5d2b7b4699b72f385f1e2772db105", size = 4852708, upload-time = "2026-07-31T14:23:52.715Z" }, - { url = "https://files.pythonhosted.org/packages/27/20/c930314a2ab476d15dec966ec87e2e9637bb02b06106b12c0396c57bb603/cryptography-50.0.0-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:7cec5b856506da6defb290f30c9ee687d5f5e8cb0bd3f6459dde43b0b4fa40ef", size = 5004179, upload-time = "2026-07-31T14:23:54.887Z" }, - { url = "https://files.pythonhosted.org/packages/32/2e/c9db68a0c4bfa28e310707527c0ee3a2bd254104d2e02e68f368e197aa4c/cryptography-50.0.0-cp311-abi3-win_amd64.whl", hash = "sha256:bd1c592e4d5974f0d08d4888e432157adba757c66da0246918e43677fafa2d30", size = 3840395, upload-time = "2026-07-31T14:23:56.677Z" }, - { url = "https://files.pythonhosted.org/packages/c3/fb/951032a3bf22a5697c83183fb6294a4843772947a70e616c57b3ff5f522e/cryptography-50.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:49e7d93abdbd2990caced757e5fade25302f719c3c8fb6e6fff2dde98999fc41", size = 3989258, upload-time = "2026-07-31T14:23:58.881Z" }, - { url = "https://files.pythonhosted.org/packages/d4/67/91eb047e69c5e845f2f14b8a2e4a1aab0f283cb885531e9e22c8adb176bc/cryptography-50.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:19736989797678c6af1e55cd49055cdbcb55d8f6b5583ac5335f933aba9101dc", size = 4700648, upload-time = "2026-07-31T14:24:00.702Z" }, - { url = "https://files.pythonhosted.org/packages/30/82/85f0f7425c856b9f96459411eb12e74ef72df9caf6f8f15bf23a33ff131f/cryptography-50.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:80b63928fa35083b33966ce1efb70e5b9607181e49dcd1c22c8c005e319f667f", size = 4682442, upload-time = "2026-07-31T14:24:02.538Z" }, - { url = "https://files.pythonhosted.org/packages/1a/28/b555a365adff1cca2fbe7b9e487d68a40de6bc67ff2cb587473eb43de0e7/cryptography-50.0.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:d58c3db7cd6eed54e6c06744db55456b65ebd7492ddeae9c1e93cfca7aa857d3", size = 4707596, upload-time = "2026-07-31T14:24:04.394Z" }, - { url = "https://files.pythonhosted.org/packages/72/d8/f52538140cc719df62a01cf87d1c7142318d235817109d6f4054d7c352d6/cryptography-50.0.0-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:df2a58a472f332225671c35b0a830208b86d004f82baa8530fa3782c85646533", size = 5314552, upload-time = "2026-07-31T14:24:06.31Z" }, - { url = "https://files.pythonhosted.org/packages/38/14/6120e5bd7c5aa022ad15424ba4d5c5269d0d9448ed4d55e492ea91e3c1c4/cryptography-50.0.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:11b74db56cdbe3cdee6e3f6982ecb70334fa10dce99ed58bf7894aaaa3b2a037", size = 4717113, upload-time = "2026-07-31T14:24:08.349Z" }, - { url = "https://files.pythonhosted.org/packages/fa/71/190bf38c3ee2e0f8efc9860ae100c9df4169742eef274b91e7aa1cb133b9/cryptography-50.0.0-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:f59e38625469987d7ef6d495323c55e7db6c212eaf6112267e0d3b565a2e9c9f", size = 4338580, upload-time = "2026-07-31T14:24:10.227Z" }, - { url = "https://files.pythonhosted.org/packages/3a/63/504ccfbbe61fd8aa983f7f146399cdf034c72c2fc55f5b2dfdcdcdb20c99/cryptography-50.0.0-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:ecfed7367f965a0328cfbdd70da860f15441f002f613185668c6e6ebf5a0ac11", size = 4707038, upload-time = "2026-07-31T14:24:12.169Z" }, - { url = "https://files.pythonhosted.org/packages/01/77/2cf79bbfc4d12ca106437a6e170d6aaa01a373e93093118aaaef0e801bd4/cryptography-50.0.0-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:9aa87839c383bdbab6ef865787a1fb877af8dd03464c4400322726feaaadfc6d", size = 5273110, upload-time = "2026-07-31T14:24:14.38Z" }, - { url = "https://files.pythonhosted.org/packages/e5/45/8aae2972c520145377ea3559a605a899bebe227bf070b33cdb445929a9b9/cryptography-50.0.0-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:6ba6a53445bd3cfa809ef3ef5f1589aa6ba08784a1d962bf47d0940e871dab1c", size = 4716439, upload-time = "2026-07-31T14:24:16.415Z" }, - { url = "https://files.pythonhosted.org/packages/7b/20/4fe50b619a48c2525cc46e2dbc1ac490708d704be5d467bdaac6dc955682/cryptography-50.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:3f5735ffe4996d28b809371756219f5354864902a3b9e7c0b9ee87041209fc9c", size = 4837383, upload-time = "2026-07-31T14:24:18.553Z" }, - { url = "https://files.pythonhosted.org/packages/92/91/3a31366e183343d3703f8995c095f5734676bd6938118047e50fcf279eb4/cryptography-50.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:1b4a266766514614f8aa60416e71f2fc6e575d36e7bdc90f644fadb2f4b75b95", size = 4985772, upload-time = "2026-07-31T14:24:20.385Z" }, - { url = "https://files.pythonhosted.org/packages/74/9a/02ffe35b2853d121689871eb5dce862092562b3a1ed5cc98f1aaed441506/cryptography-50.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:12b9c6996425c76ea6c457ace4f3073e715b8c545add07cd1a8f3a4f90691269", size = 3816291, upload-time = "2026-07-31T14:24:22.125Z" }, - { url = "https://files.pythonhosted.org/packages/03/37/73d005be173aff344af30e9fd2a576575cb2391a7101d9cd3842e1fa8cce/cryptography-50.0.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:ccdc4a71a4dabae05de219404f9f4abc38e3b58422177ff93d0da05967dafa07", size = 4036009, upload-time = "2026-07-31T14:24:24.122Z" }, - { url = "https://files.pythonhosted.org/packages/ff/c6/7a6202a534e32103a285b7834a120869557fe198d51d7cfe59754c8bda9c/cryptography-50.0.0-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:910e1d2668e7de9648f2bcee30e180db2a6b15c30f887d7c4c93ddf96e3992e3", size = 4745252, upload-time = "2026-07-31T14:24:26.118Z" }, - { url = "https://files.pythonhosted.org/packages/85/4f/0fa8c2f4428198f15d9ff8d63400e27afbf94ce833f6108da1eb3753f945/cryptography-50.0.0-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a91296cb61e8df6f86d0c19cc4068228da256bf59bf86049fbd821084565327f", size = 4728939, upload-time = "2026-07-31T14:24:27.994Z" }, - { url = "https://files.pythonhosted.org/packages/d1/63/54dd723490ba2dc09b299682c10b38db38f159728bcaae8c591b8af2f22d/cryptography-50.0.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:e722f16708d854fe924790e051061f6704a472c3bac347b6fd88033ea8dd0dc5", size = 4748483, upload-time = "2026-07-31T14:24:30.254Z" }, - { url = "https://files.pythonhosted.org/packages/1d/dd/7c77d26285cc7f6991efce64a0f5b4f9383bfa5dd8c5033003eaf7db4cdb/cryptography-50.0.0-cp39-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:d764dcf130c428ef66786f866dd750f53182bc608813489915e9fc106bb0c82f", size = 5367599, upload-time = "2026-07-31T14:24:32.457Z" }, - { url = "https://files.pythonhosted.org/packages/46/c9/f60aed34c013f317f92817b6c171c2d22a78270fa41109bd4b08af26b194/cryptography-50.0.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:105110f43a471dbd0060b9c9516cb8a6a79233631a04cc2ba16f28323ac6e025", size = 4762647, upload-time = "2026-07-31T14:24:34.599Z" }, - { url = "https://files.pythonhosted.org/packages/be/f3/f9a0173b139372c3a48ed98154b45cc6b9de17c789d5ab552e621c293609/cryptography-50.0.0-cp39-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:828743d939e9629bc267b8e2d08d8bb67cd4319c771a33d4b18b22dd8fb7440a", size = 4385197, upload-time = "2026-07-31T14:24:36.647Z" }, - { url = "https://files.pythonhosted.org/packages/d8/36/83bb81f6e569bc38e1e4a7bc80f29b46bb9601920bc455fc8e888f5d5742/cryptography-50.0.0-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:2a8183b489dc1f7f80f135780fadc1108f14b31b8a40411c7a5b17425f65f28b", size = 4748095, upload-time = "2026-07-31T14:24:39.493Z" }, - { url = "https://files.pythonhosted.org/packages/6b/16/d3008eff98c764979865834c3d386d4fd041b5f52e7f34fc29ac1a5eb515/cryptography-50.0.0-cp39-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:6e7d61120573a7f2cd94cc095f9e81f6967c61ccdf194285aa143ecec8e0b708", size = 5325948, upload-time = "2026-07-31T14:24:41.556Z" }, - { url = "https://files.pythonhosted.org/packages/9c/f8/d97f9603efda3888187bfdb893f26c41be4735c10631d05d284ee6b047c4/cryptography-50.0.0-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:37fdb0d0111f1e2ff07139dfb79f1b49531f8e213c46f1163dd7642979b58c47", size = 4762400, upload-time = "2026-07-31T14:24:43.636Z" }, - { url = "https://files.pythonhosted.org/packages/64/a2/4615c8f7d81a00b1d6e6afe19f694e1543582349fb5f4076f6cb5dc36485/cryptography-50.0.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:c87f62a3d3b9888ed0fdde100ec06aa61ca9cd44bad9057d1dff9a516b5f5bb9", size = 4878208, upload-time = "2026-07-31T14:24:45.522Z" }, - { url = "https://files.pythonhosted.org/packages/d2/1a/efcfb02f91407149a0dacffffab791f7e19bf6385f63b3666dc8b5e5c9c8/cryptography-50.0.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:65c2c3add92b45fd0709db8594536aea39c2a67af0e27ffcf049c498501140b7", size = 5037050, upload-time = "2026-07-31T14:24:47.697Z" }, - { url = "https://files.pythonhosted.org/packages/57/30/4a22984d4f1bdfb8c054f07a92bc176b97a3134cc1d6c4b3bffb1f3688b4/cryptography-50.0.0-cp39-abi3-win_amd64.whl", hash = "sha256:d24fead1d4d076e1bfb006dcec392074a3cd8d7b4fc8a595aa64073b2b7a96ba", size = 3874135, upload-time = "2026-07-31T14:24:50.085Z" }, - { url = "https://files.pythonhosted.org/packages/9d/3e/e54cde8c01631a5a8226ccd617eab9e57fd5cfdad90f1a9e6bb570794631/cryptography-50.0.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:5e34edd123674534acd70147f0ca331eaa2c74e6325fb2028c886aa26ba0b68c", size = 3963170, upload-time = "2026-07-31T14:24:51.968Z" }, - { url = "https://files.pythonhosted.org/packages/01/b6/0b9e125e90f3d2dcf599a218a899cda7326a3158cfa258723f0b398b08f6/cryptography-50.0.0-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:8eb5e1172eb569ea8a872796576e6a67c276351728b6455d5beb01242b027c6a", size = 4692441, upload-time = "2026-07-31T14:24:53.743Z" }, - { url = "https://files.pythonhosted.org/packages/53/c9/a5151588710785a96d7bc4de27d4cd62f263bbbcb203cfe29df537eb6505/cryptography-50.0.0-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:910d11e1a385c654bf738bf3e6b8e6ed5de0f5610fcae2be9e5b398d8081d20e", size = 4699810, upload-time = "2026-07-31T14:24:55.746Z" }, - { url = "https://files.pythonhosted.org/packages/c7/1a/15b92b25eb6ce3089cd49377ae990a0f3ad485a510f968aed1f19dbdcdf2/cryptography-50.0.0-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:62598a8a57f815db4c6259a4e97d857dab56697e7de8e8ab02352ab74da1995d", size = 4691924, upload-time = "2026-07-31T14:24:58.082Z" }, - { url = "https://files.pythonhosted.org/packages/62/15/219075012ab13e8905f3cd572204f4acb4b111df787104346b9bc0cea789/cryptography-50.0.0-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:07479a1cb08219ab719147e742e76090c9c773321959bb94946fffdd397a6437", size = 4699593, upload-time = "2026-07-31T14:24:59.951Z" }, - { url = "https://files.pythonhosted.org/packages/8e/b5/c2c5fce26f0ee40d21bafe7f191d29a34b35a65ac4fe8a1191d1983612e9/cryptography-50.0.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:c99c003e088647b8a5b7c145d6f78c335f6348332b62e142d411c4b63d1460b9", size = 3813796, upload-time = "2026-07-31T14:25:02.298Z" }, + { 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]] @@ -272,15 +264,15 @@ wheels = [ [[package]] name = "jwcrypto" -version = "1.5.8" +version = "1.5.9" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cryptography" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/de/7e/53c14813521693e931e420d9db00efe317419ff194495903bce03402275e/jwcrypto-1.5.8.tar.gz", hash = "sha256:c3d7114b6f6e65b52f6b7da817eb8cb8423e1da31e1ef13508447c81ecbdcc34", size = 90772, upload-time = "2026-06-24T19:36:50.782Z" } +sdist = { url = "https://files.pythonhosted.org/packages/09/c7/00b87b49ffec758a5a957e448c863f6c4bb6223b3fab22c8c5161362ddcf/jwcrypto-1.5.9.tar.gz", hash = "sha256:dbbbfcdad7a6fc40cf3d5a635b11115fc42591b7ab869a36ad347a516a6e1c5d", size = 115793, upload-time = "2026-08-26T12:54:09.689Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/6e/7c/f87c5db042c4f7b4476346fc0d22f1025a777fee08eebde3720d3b9c4eb5/jwcrypto-1.5.8-py3-none-any.whl", hash = "sha256:85aeb475f808d56bbc2f2ed1f6f73e6a317c4011a4321505f02f0aed695a3742", size = 96133, upload-time = "2026-06-24T19:36:49.519Z" }, + { url = "https://files.pythonhosted.org/packages/9e/54/b819eb2f8caedb1bccef0bf6188c051f0d898a177b271e1ed3a5cc0a8b66/jwcrypto-1.5.9-py3-none-any.whl", hash = "sha256:30b1c9afc898eefd112c31d836eb2d7a4c6febcac95d4aa727e963cd42911d8e", size = 120772, upload-time = "2026-08-26T12:54:08.637Z" }, ] [[package]] diff --git a/labs/agent-identity/docker-compose.yml b/labs/agent-identity/docker-compose.yml index 3e35e94..befdfa1 100644 --- a/labs/agent-identity/docker-compose.yml +++ b/labs/agent-identity/docker-compose.yml @@ -4,7 +4,7 @@ # TLS checks that make a production Keycloak safe to expose. services: keycloak: - image: quay.io/keycloak/keycloak:26.7.1 + image: quay.io/keycloak/keycloak:26.7.2 container_name: agent-identity-keycloak # DPoP is a preview feature, so it must be named explicitly; without it # the client attribute below is accepted and silently does nothing, and diff --git a/labs/agent-identity/evidence/discovery.json b/labs/agent-identity/evidence/discovery.json index 23129f8..fe0808d 100644 --- a/labs/agent-identity/evidence/discovery.json +++ b/labs/agent-identity/evidence/discovery.json @@ -251,18 +251,18 @@ "scopes_supported": [ "openid", "phone", - "web-origins", - "invoice:read", - "organization", - "basic", - "offline_access", - "roles", - "email", "profile", "address", + "roles", "service_account", + "organization", + "offline_access", + "invoice:read", + "email", "microprofile-jwt", - "acr" + "basic", + "acr", + "web-origins" ], "request_parameter_supported": true, "request_uri_parameter_supported": true, diff --git a/labs/agent-identity/evidence/jwks.json b/labs/agent-identity/evidence/jwks.json index c472ade..bc2a44b 100644 --- a/labs/agent-identity/evidence/jwks.json +++ b/labs/agent-identity/evidence/jwks.json @@ -1,29 +1,29 @@ { "keys": [ { - "kid": "1Cbj1fJexAP-lSwhI9GbI3FzwsRCCRlvMPlVcU1mTtw", + "kid": "hoRXAvbCxRJCSoD84Be_cUHE0FaebLE3utQtkD3hPyA", "kty": "RSA", "alg": "RSA-OAEP", "use": "enc", "x5c": [ - "MIICqzCCAZMCBgGgOhFDzTANBgkqhkiG9w0BAQsFADAZMRcwFQYDVQQDDA5hZ2VudC1pZGVudGl0eTAeFw0yNjA4MjUxNzU1MDVaFw0zNjA4MjUxNzU2NDVaMBkxFzAVBgNVBAMMDmFnZW50LWlkZW50aXR5MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0hsm901e5w7RraHSbT4sV58bpi+WJ3517OdXG/UksXFd6Lq7Mq+6kd/MzUVY5bqHpTMp7roJq7cEdNoLcevFwOh+g7pdQFhKBkjzc1Gy3rIl7sjb2rzVqgV11LL0LhnWNm/sVphEg/w6K7gnXxOlwYaeKKAcSLAIutxm8G8pJaFAdwBAfLB282WQ4CkwVsfQ107KUkBtC2n8cuo7aHmLzM1JD6Qg2agGIBUAL8c+Nffm10uWZ8ricPxpjnDvqJuIPk6BpExRnetdYBAGimQVyI0C30xRpiPkH7qvRQT+1wvUIfwm0sbyr77bz1Vg8YNxGmN2E9XBY3OfFz94Qk3QKwIDAQABMA0GCSqGSIb3DQEBCwUAA4IBAQAhmPzPpGZSPh48s/+Ah+m/MQjJebBA38ynRlejPcPxHeG0Bjc3d0c3gjiCcjl0hleg1fMUUgmzqQgHHpI+18/GSBgbylKtUDbJEhV0EHt3khFC96nD4nRchSEjgn8LoCg7KeIZHwgXwqEAVeV9C3ndEFNudXgiDgMH5nvEhccalxzGtZi5Pb97W4q0rG4BZ5gb8S9cdLaPs4fhC+XhO45LNzh68Xj2XFjJcX9Gcp3cKDW+72yQ9tapOrqknrOH7scvUHhWV9N5WJk6TYp/sH+ZVBCnoCvXUMdW8zArjuKh2/tWG/bV4uBu/Ezl79SYR8GOIwa6QFYDYb0pHb7Q/7jr" + "MIICqzCCAZMCBgGgZwl/fTANBgkqhkiG9w0BAQsFADAZMRcwFQYDVQQDDA5hZ2VudC1pZGVudGl0eTAeFw0yNjA5MDMxMTI5MzBaFw0zNjA5MDMxMTMxMTBaMBkxFzAVBgNVBAMMDmFnZW50LWlkZW50aXR5MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA1ZpY1hJ2GdB862di7dA9pzkhWZM8jMdNW1asllDpRj59d92IITDdKNF4wldzL1+8CgnPBDUdfK5Iwa77vvYW/q6m2dtXsTrGygjhBG+JzwyIIP3AnSOIt8IkO2IwHlIKMnyr51OWros1ogoRiq4V0Bg6i/QStBgCZrCyVHAxalUj56Ak+mjO/osZ1CnQpYcJ1RDo8+F6TfJHMIWl2m5hrKTkFiJowJpf+jKKA8Xspw62hMZkBBbDZTwMbm1MgKU9pDmkxuznILmwzCE5ww66yj02uFRG1R3u7qQ+VMLaVoL/MYyle7te2JDVANXXy0kQWzCTlc3WubjbBqKCOsu9GwIDAQABMA0GCSqGSIb3DQEBCwUAA4IBAQA/RlFuA8TR0cN8s5S3VYuudIN73PInPf33KS5afHDN+ndQqvMSULP65hdXaMrTUsQIaBH26UUM+HBaovYcr2UJ78shkDXEcrVHTYLk68yFMXD0GVvx29rpavE5sguPLdqRQ5p+ketA0p2FHBIaVD6R4TKj+x/Cwv44n3ai4Az2rFxZAdXtEIVobArRYIxDK9UyNx4I5Y3tVQK3ZRV+Ltxh5fCI+5kiRmfbOuH8E9rnpekMH2vDM4CPDZ6YHI+22g6JefEsrURm7yJjHTEJEd09ao1BkQ9nY5zod9jyUvjxOr4Ke6V0K4JaDzcUYg2ADg6nAdO07Uyk1oxFrPSNwows" ], - "x5t": "lNZIRUBYy-WR-7T9l5LirrEUjNw", - "x5t#S256": "m9xQrd4vqBxk-_zvQAXqogF3dBKaQcJLC3P08cbwKJ8", - "n": "0hsm901e5w7RraHSbT4sV58bpi-WJ3517OdXG_UksXFd6Lq7Mq-6kd_MzUVY5bqHpTMp7roJq7cEdNoLcevFwOh-g7pdQFhKBkjzc1Gy3rIl7sjb2rzVqgV11LL0LhnWNm_sVphEg_w6K7gnXxOlwYaeKKAcSLAIutxm8G8pJaFAdwBAfLB282WQ4CkwVsfQ107KUkBtC2n8cuo7aHmLzM1JD6Qg2agGIBUAL8c-Nffm10uWZ8ricPxpjnDvqJuIPk6BpExRnetdYBAGimQVyI0C30xRpiPkH7qvRQT-1wvUIfwm0sbyr77bz1Vg8YNxGmN2E9XBY3OfFz94Qk3QKw", + "x5t": "imtcrzP_GXvgL_5DPcqmBYN1I4k", + "x5t#S256": "Wko8wRLCsG9sKAwU5cNddCctegriIrgou6Ww3Yp6obQ", + "n": "1ZpY1hJ2GdB862di7dA9pzkhWZM8jMdNW1asllDpRj59d92IITDdKNF4wldzL1-8CgnPBDUdfK5Iwa77vvYW_q6m2dtXsTrGygjhBG-JzwyIIP3AnSOIt8IkO2IwHlIKMnyr51OWros1ogoRiq4V0Bg6i_QStBgCZrCyVHAxalUj56Ak-mjO_osZ1CnQpYcJ1RDo8-F6TfJHMIWl2m5hrKTkFiJowJpf-jKKA8Xspw62hMZkBBbDZTwMbm1MgKU9pDmkxuznILmwzCE5ww66yj02uFRG1R3u7qQ-VMLaVoL_MYyle7te2JDVANXXy0kQWzCTlc3WubjbBqKCOsu9Gw", "e": "AQAB" }, { - "kid": "I4U8jzzTXBxpLuhKzkAOCREoTV9f-xnzuRWD_4FBVm0", + "kid": "maeQFQlyFz9-6DKPokjsRfq9oBFM1pu4ebT5NrzK9Qg", "kty": "RSA", "alg": "RS256", "use": "sig", "x5c": [ - "MIICqzCCAZMCBgGgOhFDojANBgkqhkiG9w0BAQsFADAZMRcwFQYDVQQDDA5hZ2VudC1pZGVudGl0eTAeFw0yNjA4MjUxNzU1MDVaFw0zNjA4MjUxNzU2NDVaMBkxFzAVBgNVBAMMDmFnZW50LWlkZW50aXR5MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxjckA7j8SmGP6GQSyxxrotN4cUwYpnjs3bU2oeYpJsqFwTc5Xk5lBdWI1jJfSfH+vajjzVqiat+5JjCdt1fJqrevnomzRRAgT7x+2ylcAyQwO9mWZU3Ua8ttJ1Gup47RS1ZYyPflXSvbtaY9Gp/RjWs+fz0YzDL/U5LbVAaVnx99A+tgzwFgf9qQCCtRKsSscgrjdiPcUTTqkeMaovK5gn+5dTZdt5gXkOsA45I9UP3/FtzkVieZcqc9EhoUlz/vdEJR6qMMyR+/oKEpA9JWy/0SalzO/2w8ySCj7Y2hK4q+R65di2o+mhFVnCaWI85SnFUmgs8tdbYLUt2J3nQoSwIDAQABMA0GCSqGSIb3DQEBCwUAA4IBAQCCfnBg9S40f66VpmDuwyMacp8HpznFAOjPMgzirc5GdXTbUPjXqX/crEGZ3GEw/KQI3HeMkr08jwPsaVS+DKY8Oco5XgWu8B5GjNzUr1J+oaLb/2Oz7bg4GKJZPsJkekMm1zlwzZX0QWRBtatElja6xFUsaLXez9Dco/QeP58SXKkkI9iVkpDrnzcRZoeeU6qjrdWMSyah7azReQhIxBQo/b/rqa4JSalqqDnzq5/1ZOtq70QbI4tokiHNrqSroNoaqB3yd6K+v/tQehRRgEaI8tFRbYEVcf7i+PPtNrKOEmnb9BNs3NmTmoo1jUJiud881Qhr0N+LbyeX9bh/g3yz" + "MIICqzCCAZMCBgGgZwl/YDANBgkqhkiG9w0BAQsFADAZMRcwFQYDVQQDDA5hZ2VudC1pZGVudGl0eTAeFw0yNjA5MDMxMTI5MzBaFw0zNjA5MDMxMTMxMTBaMBkxFzAVBgNVBAMMDmFnZW50LWlkZW50aXR5MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEArU0UGD/rSyvdyB+zQNhaKLfBiP1pT/kRvtq/HA+KU3HDH9F42xvpBC2nVaMq7JCEtskEAgv+W9BxoIG43aNXW2XqQxytSv0A7LKScu+2SY3bKTW44WcTaFWENhUC+CFMR7KQCDEWpg0k6gs/PXWY1QLJvnDo2bKHm8VfCat41bU0VK10I9JgokzKrGoJ0rzvwCPhLtkUUZVaNxDQi7shDWviqzEevYWykAl4DcQKFzDuQSgl6aY2Y0G0ECrygrQb2IhUN9yYpSaV1/wUo6tXCidgaoUKYvN6jjP5phRDgktYJ4TLLgEmmyaM1cxC1YR1PZYIlqtgui1OFd5/y224wQIDAQABMA0GCSqGSIb3DQEBCwUAA4IBAQB5zufI4IIoTOeN3rGDmiZDjdXt64afp+XqEQTJXfwT/d7OK5wIxBz9M2gThyDYxp3qvv7nmvE4tSM7shQIuW07nFUu19jM8dMtaFsNmydoQNfb5BGTHZxqQxjZEoKP6NqNeEPynMhiujxrDlhUw5BNtAoBqOhd41JXU1iY31f8z7+ZJJ8C7tRZB3cuqEK6sJf1d4+DEkohFwtp3tez7aZVhTqDz5HbFkUkPhChkY34qQbKTL78WruOYllV8ZQn19Tx+2ulnhWv0MX9Q9DHoaCNyTbrkc6eYJR1ocQMp0NijAh0vWTGcGBsXq40qibKzqAcK7D1MhNOBReS09ZvXF2l" ], - "x5t": "QJ0-uZd3grzZCLZk8ZttQ5xLSFk", - "x5t#S256": "naeJ9gmad9-fTegFwyH-o87qsdhISanILFO7_vAKShM", - "n": "xjckA7j8SmGP6GQSyxxrotN4cUwYpnjs3bU2oeYpJsqFwTc5Xk5lBdWI1jJfSfH-vajjzVqiat-5JjCdt1fJqrevnomzRRAgT7x-2ylcAyQwO9mWZU3Ua8ttJ1Gup47RS1ZYyPflXSvbtaY9Gp_RjWs-fz0YzDL_U5LbVAaVnx99A-tgzwFgf9qQCCtRKsSscgrjdiPcUTTqkeMaovK5gn-5dTZdt5gXkOsA45I9UP3_FtzkVieZcqc9EhoUlz_vdEJR6qMMyR-_oKEpA9JWy_0SalzO_2w8ySCj7Y2hK4q-R65di2o-mhFVnCaWI85SnFUmgs8tdbYLUt2J3nQoSw", + "x5t": "JFxWnP6MTlg9R4eQA8oVI7JmYQ0", + "x5t#S256": "CMbC8JJIboGTQEoVylY2Nt7sfY2TINWZBrLEngum88c", + "n": "rU0UGD_rSyvdyB-zQNhaKLfBiP1pT_kRvtq_HA-KU3HDH9F42xvpBC2nVaMq7JCEtskEAgv-W9BxoIG43aNXW2XqQxytSv0A7LKScu-2SY3bKTW44WcTaFWENhUC-CFMR7KQCDEWpg0k6gs_PXWY1QLJvnDo2bKHm8VfCat41bU0VK10I9JgokzKrGoJ0rzvwCPhLtkUUZVaNxDQi7shDWviqzEevYWykAl4DcQKFzDuQSgl6aY2Y0G0ECrygrQb2IhUN9yYpSaV1_wUo6tXCidgaoUKYvN6jjP5phRDgktYJ4TLLgEmmyaM1cxC1YR1PZYIlqtgui1OFd5_y224wQ", "e": "AQAB" } ] diff --git a/labs/agent-identity/evidence/negative-tests.txt b/labs/agent-identity/evidence/negative-tests.txt index 00ce481..929df15 100644 --- a/labs/agent-identity/evidence/negative-tests.txt +++ b/labs/agent-identity/evidence/negative-tests.txt @@ -13,7 +13,7 @@ DPoP -- attacker has exfiltrated the access token [ HELD] stolen token + attacker's own proof key HTTP 401 DPoP proof rejected: proof key thumbprint does not match token cnf.jkt [ HELD] captured token + proof, replayed verbatim - first 200, replay HTTP 401 DPoP proof rejected: replayed jti ce0033cd-8bd1-4067-a2f1-b0035fc3a13a + first 200, replay HTTP 401 DPoP proof rejected: replayed jti 86cd9a62-7c7f-49b5-97df-f98acc5b0b70 [ HELD] proof minted for a different URL (htu) HTTP 401 DPoP proof rejected: htu 'http://localhost:8081/invoices' != 'http://localhost:8081/invoices/strict' [ HELD] proof minted for a different method (htm) @@ -39,7 +39,7 @@ SD-JWT -- what a verifier learns, and what it can replay [ HELD] verifier sees only the disclosed claims disclosed ['capabilities', 'cnf', 'iss', 'operator', 'sub']; withheld ['cost_center', 'deployment_env', 'model', 'operator_contact', 'owner_employee_id'] [ HELD] count of withheld claims is blurred by decoys - 9 unopenable digests hide 5 real claims + 7 unopenable digests hide 5 real claims [ HELD] verifier replays the presentation elsewhere (aud) rejected: Invalid audience in KB-JWT [ HELD] presentation replayed in a later session (nonce) diff --git a/labs/agent-identity/evidence/tokens-decoded.txt b/labs/agent-identity/evidence/tokens-decoded.txt index 36b2f9d..15bcdab 100644 --- a/labs/agent-identity/evidence/tokens-decoded.txt +++ b/labs/agent-identity/evidence/tokens-decoded.txt @@ -1,4 +1,4 @@ -Agent's DPoP public key thumbprint (RFC 7638): zchOwz-IApvBEn3smxCdMryqhqg508v_8l2Ye4uctgA +Agent's DPoP public key thumbprint (RFC 7638): zz194foJ8J2uAMk63qn-Hx96X3j52xNoIA-p8LpPQL0 --- agent-bound --- token_type: DPoP @@ -6,8 +6,8 @@ expires_in: 300 iss: "http://localhost:8080/realms/agent-identity" aud: ["ledger-api", "account"] azp: "agent-bound" -scope: "email profile invoice:read" -cnf: {"jkt": "zchOwz-IApvBEn3smxCdMryqhqg508v_8l2Ye4uctgA", "kc-jkt-type": "DPoP"} +scope: "profile invoice:read email" +cnf: {"jkt": "zz194foJ8J2uAMk63qn-Hx96X3j52xNoIA-p8LpPQL0", "kc-jkt-type": "DPoP"} --- agent-bearer --- token_type: Bearer @@ -15,6 +15,6 @@ expires_in: 300 iss: "http://localhost:8080/realms/agent-identity" aud: ["ledger-api", "account"] azp: "agent-bearer" -scope: "email profile invoice:read" +scope: "profile invoice:read email" cnf: null diff --git a/labs/agent-identity/evidence/versions.txt b/labs/agent-identity/evidence/versions.txt index a90686e..19fa948 100644 --- a/labs/agent-identity/evidence/versions.txt +++ b/labs/agent-identity/evidence/versions.txt @@ -1,8 +1,8 @@ -Keycloak 26.7.1 +Keycloak 26.7.2 docker: 29.7.2 pyjwt: 2.13.0 sd-jwt: 0.10.4 -jwcrypto: 1.5.8 -cryptography: 50.0.0 +jwcrypto: 1.5.9 +cryptography: 50.0.1 httpx: 0.28.1 python: 3.13.12 diff --git a/labs/agentgateway-mcp/README.md b/labs/agentgateway-mcp/README.md index 9368ef1..0b5d867 100644 --- a/labs/agentgateway-mcp/README.md +++ b/labs/agentgateway-mcp/README.md @@ -43,10 +43,10 @@ docker compose ps ```console NAME IMAGE COMMAND SERVICE CREATED STATUS PORTS -agentgateway ghcr.io/agentgateway/agentgateway:v1.4.1 "/app/agentgateway -…" agentgateway 18 seconds ago Up 17 seconds 127.0.0.1:3000->3000/tcp, 127.0.0.1:15000->15000/tcp, 127.0.0.1:15020->15020/tcp -jaeger jaegertracing/all-in-one:1.68.0 "/go/bin/all-in-one-…" jaeger 18 seconds ago Up 17 seconds 127.0.0.1:16686->16686/tcp -mcp-get-started agentgateway-mcp-mcp-get-started "fastmcp run server.…" mcp-get-started 18 seconds ago Up 17 seconds 8000/tcp -tickets agentgateway-mcp-tickets "fastmcp run tickets…" tickets 18 seconds ago Up 17 seconds 8000/tcp +agentgateway ghcr.io/agentgateway/agentgateway:v1.4.1 "/app/agentgateway -…" agentgateway 18 seconds ago Up 18 seconds 127.0.0.1:3000->3000/tcp, 127.0.0.1:15000->15000/tcp, 127.0.0.1:15020->15020/tcp +jaeger jaegertracing/all-in-one:1.76.0 "/go/bin/all-in-one-…" jaeger 19 seconds ago Up 18 seconds 127.0.0.1:16686->16686/tcp +mcp-get-started agentgateway-mcp-mcp-get-started "fastmcp run server.…" mcp-get-started 19 seconds ago Up 18 seconds 8000/tcp +tickets agentgateway-mcp-tickets "fastmcp run tickets…" tickets 19 seconds ago Up 18 seconds 8000/tcp ``` Look at the two highlighted lines. `mcp-get-started` and `tickets` show `8000/tcp` with no address in front, so neither publishes a port to your machine. The gateway is the only way in, which is how you'd run this anywhere that matters. @@ -165,17 +165,17 @@ Success! Every tool call any agent makes, in one place, in a shape a log pipelin The same traffic is already counted, too: ```bash -curl -s http://127.0.0.1:15020/metrics | grep '^agentgateway_mcp_requests_total' +curl -s http://127.0.0.1:15020/metrics | grep '^agentgateway_mcp_requests_total' | sort ``` ```console -agentgateway_mcp_requests_total{method="tools/list",resource_type="unknown",server="unknown",resource="unknown",bind="bind/3000",gateway="default/default",listener="listener0",route="default/route0",route_rule="unknown"} 2 -agentgateway_mcp_requests_total{method="notifications/initialized",resource_type="unknown",server="unknown",resource="unknown",bind="bind/3000",gateway="default/default",listener="listener0",route="default/route0",route_rule="unknown"} 2 agentgateway_mcp_requests_total{method="initialize",resource_type="unknown",server="unknown",resource="unknown",bind="bind/3000",gateway="default/default",listener="listener0",route="default/route0",route_rule="unknown"} 2 +agentgateway_mcp_requests_total{method="notifications/initialized",resource_type="unknown",server="unknown",resource="unknown",bind="bind/3000",gateway="default/default",listener="listener0",route="default/route0",route_rule="unknown"} 2 agentgateway_mcp_requests_total{method="tools/call",resource_type="tool",server="get-started",resource="add",bind="bind/3000",gateway="default/default",listener="listener0",route="default/route0",route_rule="unknown"} 1 +agentgateway_mcp_requests_total{method="tools/list",resource_type="unknown",server="unknown",resource="unknown",bind="bind/3000",gateway="default/default",listener="listener0",route="default/route0",route_rule="unknown"} 2 ``` -This is a [Prometheus](https://prometheus.io/) metrics endpoint, showing data with `server` and `resource` as labels. Since we've done two client runs so far, we see two handshakes and a tool call. +This is a [Prometheus](https://prometheus.io/) metrics endpoint, showing data with `server` and `resource` as labels. Since we've done two client runs so far, we see two handshakes and a tool call. The `sort` is there because Prometheus makes no promise about line order, and it changes from one scrape to the next. Now, let's configure tracing using `02-observed.yaml`: @@ -343,7 +343,7 @@ curl -s http://127.0.0.1:15000/api/config \ The metrics keep the two apart as well: ```bash -curl -s http://127.0.0.1:15020/metrics | grep 'resource_type="tool"' | sed 's/,bind=.*} / -> /' +curl -s http://127.0.0.1:15020/metrics | grep 'resource_type="tool"' | sed 's/,bind=.*} / -> /' | sort ``` ```console @@ -493,11 +493,7 @@ goose 1.46.0 goose is a full host, so it needs a model to drive tool calls. Your sandbox already runs [Ollama](https://ollama.com/) with `qwen3:1.7b`, a 1.7B model quantized to 1.4 GB that can call tools, which is the only capability this section needs. It needs no API key and no account, and costs nothing to run. -`qwen3:1.7b` is a reasoning model: before it answers it writes out its thinking, and on the sandbox's two CPU cores that takes minutes even for a trivial reply. goose has no way to turn thinking off in its calls, so we turn it off in the Ollama runtime instead. - -An Ollama model carries a prompt template, and qwen3's already knows how to skip thinking; that switch only fires when the caller asks for it. Copy the model's own definition, flip the switch on permanently, and build it under a new name. - -The `ollama pull` on the first line covers a sandbox that came up without the weights on disk; when they're already there it returns straight away. +`qwen3:1.7b` is a reasoning model: before it answers it writes out its thinking, and on the sandbox's two CPU cores that costs about twenty seconds for a one-word reply and several minutes for a turn that calls two tools. We turn thinking off by building a copy of the model with the switch baked into its template. The `ollama pull` on the first line covers a sandbox that came up without the weights on disk; when they're already there it returns straight away. ```bash ollama pull qwen3:1.7b @@ -505,62 +501,120 @@ ollama show --modelfile qwen3:1.7b > Modelfile.nothink sed -i 's|^FROM /.*|FROM qwen3:1.7b|' Modelfile.nothink sed -i 's|{{- if and $.IsThinkSet (eq $i $lastUserIdx) }}|{{- if (eq $i $lastUserIdx) }}|' Modelfile.nothink sed -i 's|{{- if $.Think -}}|{{- if false -}}|' Modelfile.nothink +sed -i 's|{{ if and $.IsThinkSet (not $.Think) -}}|{{ if true -}}|' Modelfile.nothink +grep -q '{{ if true -}}' Modelfile.nothink ollama create qwen3-nothink -f Modelfile.nothink ``` -The first `sed` points the new model at the tag instead of a blob path on disk. The other two make the template take the no-thinking branch for every request. It reuses the weights already on disk, so there's no download and it finishes in about a second. - -Check that it answers without thinking: +It reuses the weights already on disk, so there's no download and it finishes in about a second. Check that it answers without thinking: ```bash -ollama run qwen3-nothink "say ok" +ollama run qwen3-nothink "Reply with only the word: pong" ``` ```console -Okay, I'm ready to help you with whatever you need. Let me know how I can assist you today! +pong ``` -No `Thinking...` block, and seconds rather than minutes. Now point goose at it. goose reads its provider from the environment, and these four variables replace anything `goose configure` would have written: +No `Thinking...` block, and the wait is loading the weights rather than generating. Try the same prompt against plain `qwen3:1.7b` if you want to watch the difference: a `Thinking...` block, and about twenty seconds of it. + +
+Qwen3 is a thinking model, but we turned thinking off. Open this panel to go down the rabbit hole of how, and why + +Why: a 1.7B model on two CPU cores produces a handful of tokens a second, and thinking mode spends hundreds of them before the first word of the answer. On the sandbox, the two-tool prompt we give goose later in this section takes around a minute with thinking off and five to seven minutes with it on. + +The [Qwen3 model card](https://huggingface.co/Qwen/Qwen3-1.7B#switching-between-thinking-and-non-thinking-mode) documents two ways to switch it off. A soft switch, by including `/no_think` in the prompt, which the model treats as a request. And a hard switch, by setting `enable_thinking=False` in the chat template, which starts the reply with an empty `` block so there's nowhere left to think. Ollama's copy of that template carries both, and flips both when a caller sends `think: false` on a request. However, goose has no way to send `think: false`, so we bake both switches into a copy of the model instead. + +`ollama show --modelfile` prints the model's definition, template included, and each `sed` changes one line of it: + +- `FROM /usr/share/ollama/.ollama/models/blobs/sha256-…` becomes `FROM qwen3:1.7b`. The printed definition points at a blob on disk; pointing at the tag reuses the same weights without copying them. +- `{{- if and $.IsThinkSet (eq $i $lastUserIdx) }}` becomes `{{- if (eq $i $lastUserIdx) }}`. The template appended a thinking switch word to your message only when the caller had set `think`; now it appends one on every request. +- `{{- if $.Think -}}` becomes `{{- if false -}}`. That word was ` /think` or ` /no_think` depending on the request; now it's always ` /no_think`, the soft switch. +- `{{ if and $.IsThinkSet (not $.Think) -}}` becomes `{{ if true -}}`. The empty `` block opened the reply only when a caller sent `think: false`; now it opens every reply, the hard switch. +- `grep -q '{{ if true -}}'` fails the block if that last edit didn't land. `sed` exits 0 whether or not it matched anything, so without this a changed upstream template would build a model that thinks under a name that says it doesn't. + +The request no longer has a say. Send `think: true` to `qwen3-nothink` and you still get no thinking, because there's no branch left for the value to reach. + +> [!NOTE] +> A model this small still reasons in the open when a prompt gives it room to wonder what you meant. Ask it to "say ok" and you'll get a paragraph of deliberation with no think block around it. Direct prompts get direct answers. + +
+ +Now point goose at it. Rather than exporting variables and editing the config file in your home directory, the lab ships a goose profile, and one environment variable tells goose to read it: + +```yaml +GOOSE_PROVIDER: ollama +GOOSE_MODEL: qwen3-nothink +OLLAMA_HOST: http://localhost:11434 +GOOSE_CONTEXT_LIMIT: 8192 + +extensions: + agentgateway: + enabled: true + type: streamable_http + name: agentgateway + uri: http://127.0.0.1:3000/mcp + timeout: 300 + developer: {enabled: false, type: platform, name: developer} + analyze: {enabled: false, type: platform, name: analyze} + todo: {enabled: false, type: platform, name: todo} + # ...and every other extension goose would otherwise enable on its own +``` ```bash -export GOOSE_PROVIDER=ollama -export GOOSE_MODEL=qwen3-nothink -export OLLAMA_HOST=http://localhost:11434 -export OLLAMA_CONTEXT_LENGTH=8192 +cat goose/config/config.yaml +export GOOSE_PATH_ROOT="$PWD/goose" ``` -> [!WARNING] -> `OLLAMA_CONTEXT_LENGTH` isn't optional here. Ollama defaults to a 4096-token context, a tool-calling agent spends that on tool definitions alone, and goose then looks like it's ignoring its own instructions when really the context was silently truncated. +Three things in that file matter. The provider and model are what `goose configure` would have asked you for. `GOOSE_CONTEXT_LIMIT` is what goose sends Ollama as the context window; Ollama's own default is 4096 tokens, a tool-calling agent spends that on tool definitions alone, and the failure looks like a model ignoring instructions when really the prompt was silently truncated. And the `extensions` list is exactly one entry, the gateway, with every extension goose ships turned off by name. + +`GOOSE_PATH_ROOT` also moves goose's sessions and logs under `goose/` in this directory, so nothing in your own goose setup is read or written.
-Not on a Zenable sandbox, or want a different model? +goose has tools of its own, and the profile hides them. Open this panel to see which ones, and how we found out -On your own machine, `curl -fsSL https://ollama.com/install.sh | sh` then `ollama pull qwen3:1.7b` gets you to the same place, and the `ollama create` above works unchanged. +goose ships a set of extensions and turns most of them on by default, in whatever config it finds, even an empty one. Point it at an empty directory and ask what it has: + +```bash +GOOSE_PATH_ROOT="$(mktemp -d)" goose info -v | grep -E '^ [a-z_]+:$|enabled:' | paste - - | sort +``` + +```console +analyze: enabled: true +apps: enabled: true +chatrecall: enabled: false +code_execution: enabled: false +developer: enabled: true +extensionmanager: enabled: true +orchestrator: enabled: false +scheduler: enabled: true +skills: enabled: true +summarize: enabled: false +summon: enabled: true +todo: enabled: true +tom: enabled: true +``` + +Nine of thirteen on, before a single MCP server has been added. Each contributes tools to the list the model picks from: `analyze`, `delegate`, `load_skill`, `todo_write`, `create_app`, `manage_extensions` and more, thirteen tools next to the gateway's three. Given that list and asked to add two numbers, the 1.7B model picked `analyze`, then `delegate`, and never reached the gateway. Given three tools, it picks correctly. -For anything else, `goose configure` walks you through any [provider goose supports](https://goose-docs.ai/docs/getting-started/providers/), and the rest of this section works the same on all of them. A bigger model calls the tools more reliably, so if `qwen3-nothink` gets confused, this is the knob to turn. +The profile lists all thirteen with `enabled: false` rather than only the ones that matter today, because goose treats any it can't find as enabled by default. `goose session --no-profile` would get the same result in one flag; a file you can read shows exactly what the agent was given.
-One thing to set first. goose ships a `developer` extension that's on by default, and its tools sit in the same list the model picks from. Asked to add two numbers, a small model will reach for goose's own `analyze` tool and never touch the gateway. +
+Not on a Zenable sandbox, or want a different model? -Turn it off so the gateway's tools are the only ones the model can see: +On your own machine, `curl -fsSL https://ollama.com/install.sh | sh` then `ollama pull qwen3:1.7b` gets you to the same place, and the `ollama create` above works unchanged. -```bash -mkdir -p ~/.config/goose -cat > ~/.config/goose/config.yaml <<'EOF' -extensions: - developer: - enabled: false - name: developer - type: builtin -EOF -``` +For anything else, change `GOOSE_PROVIDER` and `GOOSE_MODEL` in `goose/config/config.yaml` to any [provider goose supports](https://goose-docs.ai/docs/getting-started/providers/), add its API key the way that provider's page describes, and the rest of this section works the same. A bigger model calls the tools more reliably, so if `qwen3-nothink` gets confused, this is the knob to turn. -The extension URL is the gateway's, which is all goose needs to know: +
+ +The profile already names the gateway, so starting a session needs no flags: ```bash export PATH="$HOME/.local/bin:$PATH" -goose session --with-streamable-http-extension "http://127.0.0.1:3000/mcp" +goose session ``` Ask for something that needs two different servers: @@ -579,17 +633,21 @@ docker compose logs --no-log-prefix agentgateway | grep '^{' \ ``` ```console -{"tool":"add","target":"get-started","status":200,"ms":"4ms"} +{"tool":"close_ticket","target":"tickets","status":400,"ms":"0ms"} {"tool":"list_tickets","target":"tickets","status":200,"ms":"3ms"} +{"tool":"list_tickets","target":"tickets","status":200,"ms":"6ms"} +{"tool":"add","target":"get-started","status":200,"ms":"4ms"} ``` +The last two lines are goose. It asked for both tools in one turn, so they land in whichever order they finish, and yours may show `add` first. Above them sit our own refused `close_ticket` and the `list_tickets` that followed it, still in the same log, because the gateway doesn't know or care which client made a call. + Success! 🎉 goose drove a local model through the gateway, and every tool call it made is one JSON record in one place. > [!WARNING] -> Running a model locally on two CPU cores is still slow even with thinking off, so expect a wait after you send a message. +> Two CPU cores are still two CPU cores. With thinking off, a turn that calls two tools takes about a minute on the sandbox, so expect a wait after you send a message. > [!NOTE] -> A model this small sometimes misses the tool call. Ask again, or name the tool more insistently. goose lists the three tools at startup, before the model acts, so the gateway and the protocol are working either way. If it reaches for a tool you have never heard of, check that you disabled the `developer` extension above. +> A model this small sometimes misses a tool call, or lists the tickets without repeating the sum. Ask again, or name the tool more insistently. goose lists the three tools at startup, before the model acts, so the gateway and the protocol are working either way. If it reaches for a tool you have never heard of, check that `GOOSE_PATH_ROOT` is set in the shell you started goose from. ## Cleanup @@ -613,11 +671,11 @@ Container tickets Removed Network agentgateway-mcp_default Removed ``` -The derived model and the goose config outlive the containers, so drop them too: +The derived model and goose's session files outlive the containers, so drop them too: ```bash ollama rm qwen3-nothink -rm -f Modelfile.nothink ~/.config/goose/config.yaml +rm -rf Modelfile.nothink goose/data goose/state ``` If you're on a workshop VM, terminating the instance is enough. Thanks for building with us! diff --git a/labs/agentgateway-mcp/compose.yaml b/labs/agentgateway-mcp/compose.yaml index 5c6af44..8f243b9 100644 --- a/labs/agentgateway-mcp/compose.yaml +++ b/labs/agentgateway-mcp/compose.yaml @@ -25,7 +25,7 @@ services: container_name: tickets jaeger: - image: jaegertracing/all-in-one:1.68.0 + image: jaegertracing/all-in-one:1.76.0 container_name: jaeger environment: # Jaeger accepts OTLP directly with this set, so the stack needs no diff --git a/labs/agentgateway-mcp/pyproject.toml b/labs/agentgateway-mcp/pyproject.toml index 1826f90..eb19885 100644 --- a/labs/agentgateway-mcp/pyproject.toml +++ b/labs/agentgateway-mcp/pyproject.toml @@ -11,3 +11,7 @@ dependencies = [ [tool.uv] package = false +# Adopt only packages published more than a week ago, so a compromised +# release has time to surface before a lab installs it. Enforced by the +# resolver, so `uv lock --upgrade` and a reader's `uv sync` obey it alike. +exclude-newer = "7 days" diff --git a/labs/agentgateway-mcp/uv.lock b/labs/agentgateway-mcp/uv.lock index 021ac03..a760a11 100644 --- a/labs/agentgateway-mcp/uv.lock +++ b/labs/agentgateway-mcp/uv.lock @@ -8,6 +8,10 @@ resolution-markers = [ "python_full_version < '3.14' and sys_platform != 'win32'", ] +[options] +exclude-newer = "0001-01-01T00:00:00Z" # This has no effect and is included for backwards compatibility when using relative exclude-newer values. +exclude-newer-span = "P7D" + [[package]] name = "agentgateway-mcp" version = "0.1.0" diff --git a/labs/ema-mcp/pyproject.toml b/labs/ema-mcp/pyproject.toml index ddfdeff..b212fc1 100644 --- a/labs/ema-mcp/pyproject.toml +++ b/labs/ema-mcp/pyproject.toml @@ -13,3 +13,7 @@ dependencies = [ [tool.uv] package = false +# Adopt only packages published more than a week ago, so a compromised +# release has time to surface before a lab installs it. Enforced by the +# resolver, so `uv lock --upgrade` and a reader's `uv sync` obey it alike. +exclude-newer = "7 days" diff --git a/labs/ema-mcp/run.sh b/labs/ema-mcp/run.sh index 5322b39..6b4866a 100755 --- a/labs/ema-mcp/run.sh +++ b/labs/ema-mcp/run.sh @@ -8,7 +8,16 @@ set -euo pipefail DIR="$(cd "$(dirname "$0")" && pwd)" cd "$DIR" -IMAGE="${KC_IMAGE:-ceposta/keycloak:id-jag}" +# Pinned by digest, not just by tag. `id-jag` is a mutable tag on a third +# party's personal Docker Hub account — it carries no version, has no release +# feed behind it, and its contents can be replaced under the same name at any +# time, so the weekly update automation cannot cooldown or verify it. The digest +# is the only thing here that names a fixed set of bytes. +# +# Resolve a replacement with: +# docker buildx imagetools inspect ceposta/keycloak:id-jag --format '{{.Manifest.Digest}}' +# and only after deciding the new contents are trustworthy. +IMAGE="${KC_IMAGE:-ceposta/keycloak:id-jag@sha256:5d945dc3e04fa616eae7ad883f158f32951503f465dfedd0ab866e0a38bb8934}" CONTAINER=kc-idjag down() { diff --git a/labs/ema-mcp/uv.lock b/labs/ema-mcp/uv.lock index f5b4d89..ed22fed 100644 --- a/labs/ema-mcp/uv.lock +++ b/labs/ema-mcp/uv.lock @@ -7,6 +7,10 @@ resolution-markers = [ "(python_full_version < '3.14' and sys_platform != 'emscripten') or (python_full_version < '3.12' and sys_platform == 'emscripten')", ] +[options] +exclude-newer = "0001-01-01T00:00:00Z" # This has no effect and is included for backwards compatibility when using relative exclude-newer values. +exclude-newer-span = "P7D" + [[package]] name = "annotated-types" version = "0.8.0" @@ -147,79 +151,67 @@ wheels = [ [[package]] name = "click" -version = "8.4.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "colorama", marker = "sys_platform == 'win32'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/76/d4/81420972a676e8ffea40450d8c8c92943e7218a78fe9b64359836cc9876b/click-8.4.2.tar.gz", hash = "sha256:9a6cea6e60b17ebe0a44c5cc636d94f09bd66142c1cd7d8b4cd731c4917a15f6", size = 338000, upload-time = "2026-06-24T17:45:15.148Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/fb/e2/79c688af8b210d232694e31e59da9f6ec747bae31c3f5946e4e9b98860d5/click-8.4.2-py3-none-any.whl", hash = "sha256:e6f9f66136c816745b9d65817da91d61d957fb16e02e4dcd0552553c5a197b76", size = 119243, upload-time = "2026-06-24T17:45:13.73Z" }, -] - -[[package]] -name = "colorama" -version = "0.4.6" +version = "8.5.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +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/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, + { url = "https://files.pythonhosted.org/packages/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.0" +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/de/41/6cbdcf9142d00fe82836fbb51e503e58088575cf7a0fe1dbff6695bf0840/cryptography-50.0.0.tar.gz", hash = "sha256:eeac2acb5a20ed25e0ad6d1df9891a520b78b404266b6d11778f25d5d691a6c9", size = 880201, upload-time = "2026-07-31T14:25:10.11Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c5/5c/59086b4aac5e879d38ddbcf74e4be7ade89cebc3eb199a55da998c3bb46a/cryptography-50.0.0-cp311-abi3-macosx_11_0_arm64.whl", hash = "sha256:031e2d5dd4bb9caa3ca9c82e5a197fd8ae680232cee62603d1a813f3f07e3d03", size = 4001252, upload-time = "2026-07-31T14:23:33.331Z" }, - { url = "https://files.pythonhosted.org/packages/57/ef/8f2df13c7216bcad3e1c74e07f6e193d93e998e114f524a53877c9af27ad/cryptography-50.0.0-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fd9192b7b70c573d7f214eb1ae35e00d359f6f5e4b27c7e21e30de1fc6204645", size = 4719554, upload-time = "2026-07-31T14:23:35.611Z" }, - { url = "https://files.pythonhosted.org/packages/d9/41/029086c34d91052fc3b88bcc8056f709a7c915c7a23b235a54eb800b1c97/cryptography-50.0.0-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:06a32a980526a6ab9a4b9bf8f7385800791e2bb960903cb6b530e4817509a3b7", size = 4702130, upload-time = "2026-07-31T14:23:37.635Z" }, - { url = "https://files.pythonhosted.org/packages/7d/ff/b6ce0954962e7f7b969f850a883744197bb3910bdfd7b6da162eab7d9f68/cryptography-50.0.0-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:a1b30560f2acc95aa8b2e06e716a13dbfc97314747b80d9707e307f77b40d6b3", size = 4725244, upload-time = "2026-07-31T14:23:39.471Z" }, - { url = "https://files.pythonhosted.org/packages/06/1e/63a1027cb7fec360a182208e1b7767d5aa1fe57be3d6aa856e69a321edc0/cryptography-50.0.0-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:8d89f3976b10b4ce31118de72329025f70d2c6ead14a8217c5514dd2c6d5a78f", size = 5342265, upload-time = "2026-07-31T14:23:41.286Z" }, - { url = "https://files.pythonhosted.org/packages/6b/72/a1116d683a6d7ece94590013882515de087edf9ef0e6292aae615a44df73/cryptography-50.0.0-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:b42a28c1844fd9de8f3f7d540e36b66f3a9c83fceac7170ebc7a6a19edd9dcae", size = 4734609, upload-time = "2026-07-31T14:23:43.139Z" }, - { url = "https://files.pythonhosted.org/packages/15/37/36a9c479bbe49acea2636c7fd3360d20f7b7e079c300352011c44850b181/cryptography-50.0.0-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:900131fafd8aead39ac7dd3a7e833be754c17a95cfd91221636949fe4eb0aa8a", size = 4356517, upload-time = "2026-07-31T14:23:44.939Z" }, - { url = "https://files.pythonhosted.org/packages/32/98/8a151d64367204cbc63ec65d37502f1d9c53cf4bfc6ec3c532614dbec60d/cryptography-50.0.0-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:07949c449a1abcf60d1ee6e88956d89404c7df3c8258f46589e912988e551987", size = 4724529, upload-time = "2026-07-31T14:23:46.93Z" }, - { url = "https://files.pythonhosted.org/packages/22/f6/ec13b470172126464a86bf54d2294a46d29837fc51ba3e45d4047946fb5e/cryptography-50.0.0-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:f89831ef99dd7dd169ab06d63a831adb9e20a87aac6d380266bbda5823349169", size = 5299852, upload-time = "2026-07-31T14:23:48.851Z" }, - { url = "https://files.pythonhosted.org/packages/da/3a/f05e32c99d440c9bb891ea0e36c9091891e36be5a9a87ab2ee6ea20729f6/cryptography-50.0.0-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:82148ec5bddac30b51a5b3c1945075f896fa022cb93f8e4a01e9f6ee95292c5f", size = 4734462, upload-time = "2026-07-31T14:23:50.861Z" }, - { url = "https://files.pythonhosted.org/packages/ca/dc/bd72b26be8953f80625f63151efd38eee71c76ca6cf591c08ff34615a79e/cryptography-50.0.0-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:1489e263a8048bb8b6a8bac662eb2d402ea5d2b7b4699b72f385f1e2772db105", size = 4852708, upload-time = "2026-07-31T14:23:52.715Z" }, - { url = "https://files.pythonhosted.org/packages/27/20/c930314a2ab476d15dec966ec87e2e9637bb02b06106b12c0396c57bb603/cryptography-50.0.0-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:7cec5b856506da6defb290f30c9ee687d5f5e8cb0bd3f6459dde43b0b4fa40ef", size = 5004179, upload-time = "2026-07-31T14:23:54.887Z" }, - { url = "https://files.pythonhosted.org/packages/32/2e/c9db68a0c4bfa28e310707527c0ee3a2bd254104d2e02e68f368e197aa4c/cryptography-50.0.0-cp311-abi3-win_amd64.whl", hash = "sha256:bd1c592e4d5974f0d08d4888e432157adba757c66da0246918e43677fafa2d30", size = 3840395, upload-time = "2026-07-31T14:23:56.677Z" }, - { url = "https://files.pythonhosted.org/packages/c3/fb/951032a3bf22a5697c83183fb6294a4843772947a70e616c57b3ff5f522e/cryptography-50.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:49e7d93abdbd2990caced757e5fade25302f719c3c8fb6e6fff2dde98999fc41", size = 3989258, upload-time = "2026-07-31T14:23:58.881Z" }, - { url = "https://files.pythonhosted.org/packages/d4/67/91eb047e69c5e845f2f14b8a2e4a1aab0f283cb885531e9e22c8adb176bc/cryptography-50.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:19736989797678c6af1e55cd49055cdbcb55d8f6b5583ac5335f933aba9101dc", size = 4700648, upload-time = "2026-07-31T14:24:00.702Z" }, - { url = "https://files.pythonhosted.org/packages/30/82/85f0f7425c856b9f96459411eb12e74ef72df9caf6f8f15bf23a33ff131f/cryptography-50.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:80b63928fa35083b33966ce1efb70e5b9607181e49dcd1c22c8c005e319f667f", size = 4682442, upload-time = "2026-07-31T14:24:02.538Z" }, - { url = "https://files.pythonhosted.org/packages/1a/28/b555a365adff1cca2fbe7b9e487d68a40de6bc67ff2cb587473eb43de0e7/cryptography-50.0.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:d58c3db7cd6eed54e6c06744db55456b65ebd7492ddeae9c1e93cfca7aa857d3", size = 4707596, upload-time = "2026-07-31T14:24:04.394Z" }, - { url = "https://files.pythonhosted.org/packages/72/d8/f52538140cc719df62a01cf87d1c7142318d235817109d6f4054d7c352d6/cryptography-50.0.0-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:df2a58a472f332225671c35b0a830208b86d004f82baa8530fa3782c85646533", size = 5314552, upload-time = "2026-07-31T14:24:06.31Z" }, - { url = "https://files.pythonhosted.org/packages/38/14/6120e5bd7c5aa022ad15424ba4d5c5269d0d9448ed4d55e492ea91e3c1c4/cryptography-50.0.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:11b74db56cdbe3cdee6e3f6982ecb70334fa10dce99ed58bf7894aaaa3b2a037", size = 4717113, upload-time = "2026-07-31T14:24:08.349Z" }, - { url = "https://files.pythonhosted.org/packages/fa/71/190bf38c3ee2e0f8efc9860ae100c9df4169742eef274b91e7aa1cb133b9/cryptography-50.0.0-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:f59e38625469987d7ef6d495323c55e7db6c212eaf6112267e0d3b565a2e9c9f", size = 4338580, upload-time = "2026-07-31T14:24:10.227Z" }, - { url = "https://files.pythonhosted.org/packages/3a/63/504ccfbbe61fd8aa983f7f146399cdf034c72c2fc55f5b2dfdcdcdb20c99/cryptography-50.0.0-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:ecfed7367f965a0328cfbdd70da860f15441f002f613185668c6e6ebf5a0ac11", size = 4707038, upload-time = "2026-07-31T14:24:12.169Z" }, - { url = "https://files.pythonhosted.org/packages/01/77/2cf79bbfc4d12ca106437a6e170d6aaa01a373e93093118aaaef0e801bd4/cryptography-50.0.0-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:9aa87839c383bdbab6ef865787a1fb877af8dd03464c4400322726feaaadfc6d", size = 5273110, upload-time = "2026-07-31T14:24:14.38Z" }, - { url = "https://files.pythonhosted.org/packages/e5/45/8aae2972c520145377ea3559a605a899bebe227bf070b33cdb445929a9b9/cryptography-50.0.0-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:6ba6a53445bd3cfa809ef3ef5f1589aa6ba08784a1d962bf47d0940e871dab1c", size = 4716439, upload-time = "2026-07-31T14:24:16.415Z" }, - { url = "https://files.pythonhosted.org/packages/7b/20/4fe50b619a48c2525cc46e2dbc1ac490708d704be5d467bdaac6dc955682/cryptography-50.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:3f5735ffe4996d28b809371756219f5354864902a3b9e7c0b9ee87041209fc9c", size = 4837383, upload-time = "2026-07-31T14:24:18.553Z" }, - { url = "https://files.pythonhosted.org/packages/92/91/3a31366e183343d3703f8995c095f5734676bd6938118047e50fcf279eb4/cryptography-50.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:1b4a266766514614f8aa60416e71f2fc6e575d36e7bdc90f644fadb2f4b75b95", size = 4985772, upload-time = "2026-07-31T14:24:20.385Z" }, - { url = "https://files.pythonhosted.org/packages/74/9a/02ffe35b2853d121689871eb5dce862092562b3a1ed5cc98f1aaed441506/cryptography-50.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:12b9c6996425c76ea6c457ace4f3073e715b8c545add07cd1a8f3a4f90691269", size = 3816291, upload-time = "2026-07-31T14:24:22.125Z" }, - { url = "https://files.pythonhosted.org/packages/03/37/73d005be173aff344af30e9fd2a576575cb2391a7101d9cd3842e1fa8cce/cryptography-50.0.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:ccdc4a71a4dabae05de219404f9f4abc38e3b58422177ff93d0da05967dafa07", size = 4036009, upload-time = "2026-07-31T14:24:24.122Z" }, - { url = "https://files.pythonhosted.org/packages/ff/c6/7a6202a534e32103a285b7834a120869557fe198d51d7cfe59754c8bda9c/cryptography-50.0.0-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:910e1d2668e7de9648f2bcee30e180db2a6b15c30f887d7c4c93ddf96e3992e3", size = 4745252, upload-time = "2026-07-31T14:24:26.118Z" }, - { url = "https://files.pythonhosted.org/packages/85/4f/0fa8c2f4428198f15d9ff8d63400e27afbf94ce833f6108da1eb3753f945/cryptography-50.0.0-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a91296cb61e8df6f86d0c19cc4068228da256bf59bf86049fbd821084565327f", size = 4728939, upload-time = "2026-07-31T14:24:27.994Z" }, - { url = "https://files.pythonhosted.org/packages/d1/63/54dd723490ba2dc09b299682c10b38db38f159728bcaae8c591b8af2f22d/cryptography-50.0.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:e722f16708d854fe924790e051061f6704a472c3bac347b6fd88033ea8dd0dc5", size = 4748483, upload-time = "2026-07-31T14:24:30.254Z" }, - { url = "https://files.pythonhosted.org/packages/1d/dd/7c77d26285cc7f6991efce64a0f5b4f9383bfa5dd8c5033003eaf7db4cdb/cryptography-50.0.0-cp39-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:d764dcf130c428ef66786f866dd750f53182bc608813489915e9fc106bb0c82f", size = 5367599, upload-time = "2026-07-31T14:24:32.457Z" }, - { url = "https://files.pythonhosted.org/packages/46/c9/f60aed34c013f317f92817b6c171c2d22a78270fa41109bd4b08af26b194/cryptography-50.0.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:105110f43a471dbd0060b9c9516cb8a6a79233631a04cc2ba16f28323ac6e025", size = 4762647, upload-time = "2026-07-31T14:24:34.599Z" }, - { url = "https://files.pythonhosted.org/packages/be/f3/f9a0173b139372c3a48ed98154b45cc6b9de17c789d5ab552e621c293609/cryptography-50.0.0-cp39-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:828743d939e9629bc267b8e2d08d8bb67cd4319c771a33d4b18b22dd8fb7440a", size = 4385197, upload-time = "2026-07-31T14:24:36.647Z" }, - { url = "https://files.pythonhosted.org/packages/d8/36/83bb81f6e569bc38e1e4a7bc80f29b46bb9601920bc455fc8e888f5d5742/cryptography-50.0.0-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:2a8183b489dc1f7f80f135780fadc1108f14b31b8a40411c7a5b17425f65f28b", size = 4748095, upload-time = "2026-07-31T14:24:39.493Z" }, - { url = "https://files.pythonhosted.org/packages/6b/16/d3008eff98c764979865834c3d386d4fd041b5f52e7f34fc29ac1a5eb515/cryptography-50.0.0-cp39-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:6e7d61120573a7f2cd94cc095f9e81f6967c61ccdf194285aa143ecec8e0b708", size = 5325948, upload-time = "2026-07-31T14:24:41.556Z" }, - { url = "https://files.pythonhosted.org/packages/9c/f8/d97f9603efda3888187bfdb893f26c41be4735c10631d05d284ee6b047c4/cryptography-50.0.0-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:37fdb0d0111f1e2ff07139dfb79f1b49531f8e213c46f1163dd7642979b58c47", size = 4762400, upload-time = "2026-07-31T14:24:43.636Z" }, - { url = "https://files.pythonhosted.org/packages/64/a2/4615c8f7d81a00b1d6e6afe19f694e1543582349fb5f4076f6cb5dc36485/cryptography-50.0.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:c87f62a3d3b9888ed0fdde100ec06aa61ca9cd44bad9057d1dff9a516b5f5bb9", size = 4878208, upload-time = "2026-07-31T14:24:45.522Z" }, - { url = "https://files.pythonhosted.org/packages/d2/1a/efcfb02f91407149a0dacffffab791f7e19bf6385f63b3666dc8b5e5c9c8/cryptography-50.0.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:65c2c3add92b45fd0709db8594536aea39c2a67af0e27ffcf049c498501140b7", size = 5037050, upload-time = "2026-07-31T14:24:47.697Z" }, - { url = "https://files.pythonhosted.org/packages/57/30/4a22984d4f1bdfb8c054f07a92bc176b97a3134cc1d6c4b3bffb1f3688b4/cryptography-50.0.0-cp39-abi3-win_amd64.whl", hash = "sha256:d24fead1d4d076e1bfb006dcec392074a3cd8d7b4fc8a595aa64073b2b7a96ba", size = 3874135, upload-time = "2026-07-31T14:24:50.085Z" }, - { url = "https://files.pythonhosted.org/packages/9d/3e/e54cde8c01631a5a8226ccd617eab9e57fd5cfdad90f1a9e6bb570794631/cryptography-50.0.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:5e34edd123674534acd70147f0ca331eaa2c74e6325fb2028c886aa26ba0b68c", size = 3963170, upload-time = "2026-07-31T14:24:51.968Z" }, - { url = "https://files.pythonhosted.org/packages/01/b6/0b9e125e90f3d2dcf599a218a899cda7326a3158cfa258723f0b398b08f6/cryptography-50.0.0-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:8eb5e1172eb569ea8a872796576e6a67c276351728b6455d5beb01242b027c6a", size = 4692441, upload-time = "2026-07-31T14:24:53.743Z" }, - { url = "https://files.pythonhosted.org/packages/53/c9/a5151588710785a96d7bc4de27d4cd62f263bbbcb203cfe29df537eb6505/cryptography-50.0.0-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:910d11e1a385c654bf738bf3e6b8e6ed5de0f5610fcae2be9e5b398d8081d20e", size = 4699810, upload-time = "2026-07-31T14:24:55.746Z" }, - { url = "https://files.pythonhosted.org/packages/c7/1a/15b92b25eb6ce3089cd49377ae990a0f3ad485a510f968aed1f19dbdcdf2/cryptography-50.0.0-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:62598a8a57f815db4c6259a4e97d857dab56697e7de8e8ab02352ab74da1995d", size = 4691924, upload-time = "2026-07-31T14:24:58.082Z" }, - { url = "https://files.pythonhosted.org/packages/62/15/219075012ab13e8905f3cd572204f4acb4b111df787104346b9bc0cea789/cryptography-50.0.0-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:07479a1cb08219ab719147e742e76090c9c773321959bb94946fffdd397a6437", size = 4699593, upload-time = "2026-07-31T14:24:59.951Z" }, - { url = "https://files.pythonhosted.org/packages/8e/b5/c2c5fce26f0ee40d21bafe7f191d29a34b35a65ac4fe8a1191d1983612e9/cryptography-50.0.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:c99c003e088647b8a5b7c145d6f78c335f6348332b62e142d411c4b63d1460b9", size = 3813796, upload-time = "2026-07-31T14:25:02.298Z" }, +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]] @@ -267,15 +259,15 @@ wheels = [ [[package]] name = "httpcore2" -version = "2.10.0" +version = "2.12.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "h11" }, { name = "truststore" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a9/83/a896fc59940fc5a6e2aff3a4be1d92fa890112936803b331cae75a993c34/httpcore2-2.10.0.tar.gz", hash = "sha256:13c0cc3d1919d4f28457f60cd2c2abe04113a8af184ccf1142811beba936f9dc", size = 67427, upload-time = "2026-08-09T09:11:32.123Z" } +sdist = { url = "https://files.pythonhosted.org/packages/be/ad/f4f0e57345f1870f3e8cb624e058d7eca6e5a27d33bcc3311d9b618734cd/httpcore2-2.12.0.tar.gz", hash = "sha256:9293522bba0aa7c4c8e9e3f040c16575bd8868e155a77fa30c7a9085a5eae648", size = 67548, upload-time = "2026-08-18T13:22:08.211Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e5/4f/d149104195a35e2853a2fc203a8e3477747e58c80e17dda686dace174383/httpcore2-2.10.0-py3-none-any.whl", hash = "sha256:7df06cfb34070cae4f7c89be69dc1095eca138e9704ceffb98d25c1912ab6f01", size = 83000, upload-time = "2026-08-09T09:11:29.555Z" }, + { url = "https://files.pythonhosted.org/packages/d2/74/d370e55600d9bcfa0d9794b0166126d49291a3d2b20c268fc98c453a4948/httpcore2-2.12.0-py3-none-any.whl", hash = "sha256:7e04258ce01013d7d615e5b910a3b27fac937d7a95038227e79652b4ba3b4ceb", size = 83074, upload-time = "2026-08-18T13:22:05.854Z" }, ] [[package]] @@ -295,7 +287,7 @@ wheels = [ [[package]] name = "httpx2" -version = "2.10.0" +version = "2.12.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio", marker = "sys_platform != 'emscripten'" }, @@ -305,9 +297,9 @@ dependencies = [ { name = "truststore", marker = "sys_platform != 'emscripten'" }, { name = "typing-extensions", marker = "python_full_version < '3.13'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/bd/3d/f9a8c07a3884f3e5b26205e8436a18b3af61c5d53192c3bea235574dbbec/httpx2-2.10.0.tar.gz", hash = "sha256:8741d7329fe2c7885fc9ceb61c8217acfb87a85f75723714b89ebf7ad7196338", size = 98749, upload-time = "2026-08-09T09:11:33.24Z" } +sdist = { url = "https://files.pythonhosted.org/packages/7f/f8/579a8b51e42e38ee32647df9f08aa25643ae788e275cc625b199829c4671/httpx2-2.12.0.tar.gz", hash = "sha256:7631fe9887a8a2275f4a2540e053aa670fcc50742864a9ae7c66e609fdcf12cf", size = 100040, upload-time = "2026-08-18T13:22:09.086Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b9/6d/a637d52449d98a6892d9a4dc0262587afdb6a66f201871842dce5a97b1c1/httpx2-2.10.0-py3-none-any.whl", hash = "sha256:5e3194a432701e1cc6f69a8b1b2fa199ef907013fede8d9a09a2c5b7b8141a18", size = 94355, upload-time = "2026-08-09T09:11:30.882Z" }, + { url = "https://files.pythonhosted.org/packages/c8/95/411ba65569158e862368917aaf56597f3e5fa3b91b0502919638465a08f3/httpx2-2.12.0-py3-none-any.whl", hash = "sha256:cc8b6eecb8661c146b8f89a60e97456ee086e91a784ed31ac450c3a9e613dd36", size = 95427, upload-time = "2026-08-18T13:22:06.834Z" }, ] [[package]] @@ -321,11 +313,11 @@ wheels = [ [[package]] name = "idna" -version = "3.18" +version = "3.19" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/cd/63/9496c57188a2ee585e0f1db071d75089a11e98aa86eb99d9d7618fc1edce/idna-3.18.tar.gz", hash = "sha256:ffb385a7e039654cef1ab9ef32c6fafe283c0c0467bba1d9029738ce4a14a848", size = 196711, upload-time = "2026-06-02T14:34:07.794Z" } +sdist = { url = "https://files.pythonhosted.org/packages/5f/f7/abb373e5757eaec4b922b92f97ec8d6d7e057cf06778247604fbc4e7c3f3/idna-3.19.tar.gz", hash = "sha256:5e0811a4383b21dc5838069f801c4fb62113b7447663d2530d2bd6e77b49bf15", size = 215237, upload-time = "2026-08-18T05:14:24.27Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/1e/5e/d4e9f1a599fb8e573b7b87160658329fbf28d19eac2718f51fc3def3aa5a/idna-3.18-py3-none-any.whl", hash = "sha256:7f952cbe720b688055e3f87de14f5c3e5fdaa8bc3928985c4077ca689de849a2", size = 65455, upload-time = "2026-06-02T14:34:06.319Z" }, + { url = "https://files.pythonhosted.org/packages/57/b0/0e52c878c53f245edd3a11020f20979b3f490f245af532c7cae3027754b5/idna-3.19-py3-none-any.whl", hash = "sha256:815e7be7a7806d54abb586dc943addc79e8b2ee16915059658cbeff4b1b43bf4", size = 68550, upload-time = "2026-08-18T05:14:22.343Z" }, ] [[package]] @@ -357,7 +349,7 @@ wheels = [ [[package]] name = "mcp" -version = "2.0.0" +version = "2.1.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, @@ -375,22 +367,22 @@ dependencies = [ { name = "typing-inspection" }, { name = "uvicorn", marker = "sys_platform != 'emscripten'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/74/33/32d4dff2c95bb5d897c3ef4c83649a08996b17b58f0a326d2495d4c81179/mcp-2.0.0.tar.gz", hash = "sha256:0f440e735c13ece8bb19bc62cf0b86f4313448432fbb77d35e14034f4e050728", size = 1662284, upload-time = "2026-07-28T13:45:32.346Z" } +sdist = { url = "https://files.pythonhosted.org/packages/d4/6e/21fb8e5d579dbe21d96ea4d5034200d46d8bdf2261053b5bd041f3c2f612/mcp-2.1.1.tar.gz", hash = "sha256:50b7ba1ebbe117008ea7bdd288234043e69c20b403d6851d19661e6d431a75ef", size = 3984589, upload-time = "2026-08-25T16:14:02.376Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/67/72/7d7897418912c1d12e87556630dfb7bf0eac71160e9bef8b447960804ee3/mcp-2.0.0-py3-none-any.whl", hash = "sha256:1cb4c75d2d2c7b8c1d756355e5d82a39f2822cc7f13e22a2051d7ca3592349d6", size = 349980, upload-time = "2026-07-28T13:45:28.853Z" }, + { url = "https://files.pythonhosted.org/packages/50/af/8644cc5fa26a59afd2df2e98eeb19e72926887fa4b7441aba4ff661140db/mcp-2.1.1-py3-none-any.whl", hash = "sha256:1c6c31c5d6471c58db76af3af8af67f46d11d01f0a59077d0a308cbdb3d3e915", size = 357912, upload-time = "2026-08-25T16:13:59.024Z" }, ] [[package]] name = "mcp-types" -version = "2.0.0" +version = "2.1.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pydantic" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/bb/56/9b8e1c152f61f6c6b07c4b5896c88c7d0ae90bac6ee6306f852fcc5c1eb0/mcp_types-2.0.0.tar.gz", hash = "sha256:d7d939b9285c9961ae8866ba75ef85da34d12bafe276efbf4eb6a131786d8379", size = 66632, upload-time = "2026-07-28T13:45:33.804Z" } +sdist = { url = "https://files.pythonhosted.org/packages/6a/dd/1c4417dc0b722c23a1669032d5f044e41170fe5d4773b488a50fcce98c32/mcp_types-2.1.1.tar.gz", hash = "sha256:77dcbe48fba73cca71a673f2646a5f037a017b7a0a07ac89cec1113028890eda", size = 66674, upload-time = "2026-08-25T16:14:03.861Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f5/4c/c78d78c3d52b0ac594ad7cc8ef5972adfe070e3597a8a4c6ce0cd39196ea/mcp_types-2.0.0-py3-none-any.whl", hash = "sha256:6b2de797ca2797f568b79529e1b25948e34de511bcc0bd82fef1039a6d1b8eb0", size = 69649, upload-time = "2026-07-28T13:45:30.713Z" }, + { url = "https://files.pythonhosted.org/packages/71/d0/242e63c510f4a17381f55b1549a3f94f5687a0595984febd2b6f87a687a0/mcp_types-2.1.1-py3-none-any.whl", hash = "sha256:26f9f7f03f2a5730717a5b98e2ab7eb640ac352d05a00cdc725c311864778295", size = 69656, upload-time = "2026-08-25T16:14:00.667Z" }, ] [[package]] @@ -771,13 +763,13 @@ wheels = [ [[package]] name = "uvicorn" -version = "0.52.3" +version = "0.52.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "click" }, { name = "h11" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/2e/28/64ca011edf31c715b4fad359c587ea52391aaffa125065695590241ff617/uvicorn-0.52.3.tar.gz", hash = "sha256:18857b9e6579300be55c91c0a1cfd37d9a2cf0cabea33b88275f199eb73b8b58", size = 100621, upload-time = "2026-08-13T16:50:02.899Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f2/0f/3f86e61397dd33bf2ccf28188c40db6a740658aeebbbf6e7dbc101a1f487/uvicorn-0.52.4.tar.gz", hash = "sha256:73acfee47a0b133c5de13d219492d62d8a31e935f4fe6e41a232451a15379f86", size = 100627, upload-time = "2026-08-19T06:27:41.821Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/dc/2b/ebd108734a8204c6b4b93c681c9a38c5273b3ccd5d129fee4ffc1d97772c/uvicorn-0.52.3-py3-none-any.whl", hash = "sha256:116af2710dbf47c80f463cd20ee4884b6662f4c9f227d797ddc7279d2fcc2c7c", size = 79859, upload-time = "2026-08-13T16:50:01.323Z" }, + { url = "https://files.pythonhosted.org/packages/f1/79/4a20b54ab0491485ccd8c077db2d39187c7f12b3e15485d38a7be37c81b4/uvicorn-0.52.4-py3-none-any.whl", hash = "sha256:f86e41a149d7d05a9969337e3946a9c171c06a5d42680896daaba624aeac8da1", size = 79871, upload-time = "2026-08-19T06:27:40.36Z" }, ] diff --git a/labs/mcp-get-started/Dockerfile b/labs/mcp-get-started/Dockerfile index b8a5ed2..d7fa803 100644 --- a/labs/mcp-get-started/Dockerfile +++ b/labs/mcp-get-started/Dockerfile @@ -1,5 +1,7 @@ FROM python:3.13-slim -RUN pip install --no-cache-dir 'fastmcp[tasks]>=4,<5' +# Same floor as pyproject.toml. 4.0.2 is the first release carrying the tasks +# behaviour this lab walks through. +RUN pip install --no-cache-dir 'fastmcp[tasks]>=4.0.2,<5' WORKDIR /app COPY server.py . EXPOSE 8000 diff --git a/labs/mcp-get-started/README.md b/labs/mcp-get-started/README.md index 5aacee4..f9a33bb 100644 --- a/labs/mcp-get-started/README.md +++ b/labs/mcp-get-started/README.md @@ -408,9 +408,7 @@ goose 1.46.0 This is where a model finally enters the story: goose is a full host, so it needs a model to drive tool calls. Your sandbox already runs [Ollama](https://ollama.com/) with `qwen3:1.7b`, a 1.7B model quantized to 1.4 GB that fits alongside your container and can call tools, which is the only capability this section needs. So there's no key, no account, and no token cost for the rest of the lab. -`qwen3:1.7b` is a reasoning model: before it answers it writes out its thinking, and on the sandbox's two CPU cores that takes minutes even for a trivial reply. Since goose doesn't currently have a way to turn thinking off in its calls, we're going to adjust the upstream qwen model to turn it off in the Ollama inference runtime. - -An Ollama model carries a prompt template, and qwen3's already knows how to skip thinking; that switch just only fires when the caller asks for it. Copy the model's own definition, flip the switch on permanently, and build it under a new name. The `ollama pull` on the first line covers a sandbox that came up without the weights on disk; when they're already there it returns straight away. +`qwen3:1.7b` is a reasoning model: before it answers it writes out its thinking, and on the sandbox's two CPU cores that costs about twenty seconds for a one-word reply and several minutes for a turn that calls two tools. We turn thinking off by building a copy of the model with the switch baked into its template. The `ollama pull` on the first line covers a sandbox that came up without the weights on disk; when they're already there it returns straight away. ```bash ollama pull qwen3:1.7b @@ -418,65 +416,120 @@ ollama show --modelfile qwen3:1.7b > Modelfile.nothink sed -i 's|^FROM /.*|FROM qwen3:1.7b|' Modelfile.nothink sed -i 's|{{- if and $.IsThinkSet (eq $i $lastUserIdx) }}|{{- if (eq $i $lastUserIdx) }}|' Modelfile.nothink sed -i 's|{{- if $.Think -}}|{{- if false -}}|' Modelfile.nothink +sed -i 's|{{ if and $.IsThinkSet (not $.Think) -}}|{{ if true -}}|' Modelfile.nothink +grep -q '{{ if true -}}' Modelfile.nothink ollama create qwen3-nothink -f Modelfile.nothink ``` -The first `sed` points the new model at the tag instead of a blob path on disk. The other two make the template append qwen3's own `/no_think` token to every request, whatever the caller asked for. Because it reuses weights already on disk there's no download, and it finishes in about a second. - -Check what it does now: +Because it reuses weights already on disk there's no download, and it finishes in about a second. Check that it answers without thinking: ```bash -ollama run qwen3-nothink "say ok" +ollama run qwen3-nothink "Reply with only the word: pong" ``` ```console -Okay, I'll say "Okay" and be ready to help you. Let me know how I can assist you today! +pong ``` -An answer in seconds rather than minutes, with no `` block in front of it. +No `Thinking...` block, and the wait is loading the weights rather than generating. Try the same prompt against plain `qwen3:1.7b` if you want to watch the difference: a `Thinking...` block, and about twenty seconds of it. -> [!WARNING] -> `/no_think` is a request, and a 1.7B model grants it most of the time rather than always. If your run comes back with a paragraph of reasoning and a `` before the answer, that's the model ignoring the hint; run it again. The point of the switch is that most turns get shorter, which is what makes the next section bearable on two CPU cores. +
+Qwen3 is a thinking model, but we turned thinking off. Open this panel to go down the rabbit hole of how, and why + +Why: a 1.7B model on two CPU cores produces a handful of tokens a second, and thinking mode spends hundreds of them before the first word of the answer. On the sandbox, a two-tool prompt like the one we give goose below takes around a minute with thinking off and five to seven minutes with it on. + +The [Qwen3 model card](https://huggingface.co/Qwen/Qwen3-1.7B#switching-between-thinking-and-non-thinking-mode) documents two ways to switch it off. A soft switch, by including `/no_think` in the prompt, which the model treats as a request. And a hard switch, by setting `enable_thinking=False` in the chat template, which starts the reply with an empty `` block so there's nowhere left to think. Ollama's copy of that template carries both, and flips both when a caller sends `think: false` on a request. However, goose has no way to send `think: false`, so we bake both switches into a copy of the model instead. -Now point goose at it. goose reads its provider from the environment, and these four variables replace anything `goose configure` would have written: +`ollama show --modelfile` prints the model's definition, template included, and each `sed` changes one line of it: + +- `FROM /usr/share/ollama/.ollama/models/blobs/sha256-…` becomes `FROM qwen3:1.7b`. The printed definition points at a blob on disk; pointing at the tag reuses the same weights without copying them. +- `{{- if and $.IsThinkSet (eq $i $lastUserIdx) }}` becomes `{{- if (eq $i $lastUserIdx) }}`. The template appended a thinking switch word to your message only when the caller had set `think`; now it appends one on every request. +- `{{- if $.Think -}}` becomes `{{- if false -}}`. That word was ` /think` or ` /no_think` depending on the request; now it's always ` /no_think`, the soft switch. +- `{{ if and $.IsThinkSet (not $.Think) -}}` becomes `{{ if true -}}`. The empty `` block opened the reply only when a caller sent `think: false`; now it opens every reply, the hard switch. +- `grep -q '{{ if true -}}'` fails the block if that last edit didn't land. `sed` exits 0 whether or not it matched anything, so without this a changed upstream template would build a model that thinks under a name that says it doesn't. + +The request no longer has a say. Send `think: true` to `qwen3-nothink` and you still get no thinking, because there's no branch left for the value to reach. + +> [!NOTE] +> A model this small still reasons in the open when a prompt gives it room to wonder what you meant. Ask it to "say ok" and you'll get a paragraph of deliberation with no think block around it. Direct prompts get direct answers. + +
+ +Now point goose at it. Rather than exporting variables and editing the config file in your home directory, the lab ships a goose profile, and one environment variable tells goose to read it: + +```yaml +GOOSE_PROVIDER: ollama +GOOSE_MODEL: qwen3-nothink +OLLAMA_HOST: http://localhost:11434 +GOOSE_CONTEXT_LIMIT: 8192 + +extensions: + mcp-get-started: + enabled: true + type: streamable_http + name: mcp-get-started + uri: http://127.0.0.1:8765/mcp + timeout: 300 + developer: {enabled: false, type: platform, name: developer} + analyze: {enabled: false, type: platform, name: analyze} + todo: {enabled: false, type: platform, name: todo} + # ...and every other extension goose would otherwise enable on its own +``` ```bash -export GOOSE_PROVIDER=ollama -export GOOSE_MODEL=qwen3-nothink -export OLLAMA_HOST=http://localhost:11434 -export OLLAMA_CONTEXT_LENGTH=8192 +cat goose/config/config.yaml +export GOOSE_PATH_ROOT="$PWD/goose" ``` -> [!WARNING] -> `OLLAMA_CONTEXT_LENGTH` is not optional here. Ollama defaults to a 4096-token context, a tool-calling agent spends that on tool definitions alone, and goose then looks like it's ignoring its own instructions when really the context was silently truncated. +Three things in that file matter. The provider and model are what `goose configure` would have asked you for. `GOOSE_CONTEXT_LIMIT` is what goose sends Ollama as the context window; Ollama's own default is 4096 tokens, a tool-calling agent spends that on tool definitions alone, and the failure looks like a model ignoring instructions when really the prompt was silently truncated. And the `extensions` list is exactly one entry, your server, with every extension goose ships turned off by name. + +`GOOSE_PATH_ROOT` also moves goose's sessions and logs under `goose/` in this directory, so nothing in your own goose setup is read or written.
-Not on a Zenable sandbox, or want a different model? +goose has tools of its own, and the profile hides them. Open this panel to see which ones, and how we found out + +goose ships a set of extensions and turns most of them on by default, in whatever config it finds, even an empty one. Point it at an empty directory and ask what it has: + +```bash +GOOSE_PATH_ROOT="$(mktemp -d)" goose info -v | grep -E '^ [a-z_]+:$|enabled:' | paste - - | sort +``` -On your own machine, `curl -fsSL https://ollama.com/install.sh | sh` then `ollama pull qwen3:1.7b` gets you to the same place. +```console +analyze: enabled: true +apps: enabled: true +chatrecall: enabled: false +code_execution: enabled: false +developer: enabled: true +extensionmanager: enabled: true +orchestrator: enabled: false +scheduler: enabled: true +skills: enabled: true +summarize: enabled: false +summon: enabled: true +todo: enabled: true +tom: enabled: true +``` + +Nine of thirteen on, before a single MCP server has been added. Each contributes tools to the list the model picks from: `analyze`, `delegate`, `load_skill`, `todo_write`, `create_app`, `manage_extensions` and more, thirteen tools next to your server's three. Given a list like that and asked to add two numbers, a small model reaches for `analyze` and never touches your server. Given three tools, it picks correctly. -For anything else, `goose configure` walks you through any [provider goose supports](https://goose-docs.ai/docs/getting-started/providers/), and the rest of this section works the same on all of them: an [OpenRouter](https://openrouter.ai/) free-tier model, or a paid provider you already use. A bigger model calls the tools more reliably, so if `qwen3:1.7b` gets confused, this is the knob to turn. Our [ACP workshop](https://www.zenable.app/learn?lab=acp-agent-client&utm_source=github&utm_medium=labs_repo&utm_campaign=mcp-get-started_readme) walks the local-model setup in more depth. +The profile lists all thirteen with `enabled: false` rather than only the ones that matter today, because goose treats any it can't find as enabled by default. `goose session --no-profile` would get the same result in one flag; a file you can read shows exactly what the agent was given.
-One thing to set first. goose ships a `developer` extension that's on by default, and its tools sit in the same list the model picks from. Asked to add two numbers, a small model will reach for goose's own `analyze` tool and never touch your server. Turn it off so your two tools are the only ones the model can see: +
+Not on a Zenable sandbox, or want a different model? + +On your own machine, `curl -fsSL https://ollama.com/install.sh | sh` then `ollama pull qwen3:1.7b` gets you to the same place, and the `ollama create` above works unchanged. -```bash -mkdir -p ~/.config/goose -cat > ~/.config/goose/config.yaml <<'EOF' -extensions: - developer: - enabled: false - name: developer - type: builtin -EOF -``` +For anything else, change `GOOSE_PROVIDER` and `GOOSE_MODEL` in `goose/config/config.yaml` to any [provider goose supports](https://goose-docs.ai/docs/getting-started/providers/), add its API key the way that provider's page describes, and the rest of this section works the same: an [OpenRouter](https://openrouter.ai/) free-tier model, or a paid provider you already use. A bigger model calls the tools more reliably, so if `qwen3-nothink` gets confused, this is the knob to turn. Our [ACP workshop](https://www.zenable.app/learn?lab=acp-agent-client&utm_source=github&utm_medium=labs_repo&utm_campaign=mcp-get-started_readme) walks the local-model setup in more depth. + +
-Now start a session with your container attached as a Streamable HTTP extension: +The profile already names your server, so starting a session needs no flags: ```bash export PATH="$HOME/.local/bin:$PATH" -goose session --with-streamable-http-extension "http://127.0.0.1:8765/mcp" +goose session ``` In the session, ask something that forces a tool call rather than mental arithmetic: @@ -485,12 +538,12 @@ In the session, ask something that forces a tool call rather than mental arithme Use the add tool to compute 20260825 + 101, then shout the phrase "protocols over plugins". ``` -Watch the transcript: goose lists your tools on connecting, the model picks `add`, and the result comes back through a `tools/call`. When it responds with `20260926` and `PROTOCOLS OVER PLUGINS!`, you've watched one unchanged server answer three different clients. +Watch the transcript: goose lists your tools on connecting, the model picks `add`, and the result comes back through a `tools/call`. When the transcript shows `add` and then `shout` being called, you've watched one unchanged server answer three different clients. The model's summary afterwards varies; the two tool calls are the part that matters. goose is on an older protocol revision than the one you've been sending by hand, so it opens with the `initialize` handshake and gets a session, where your `server/discover` got a stateless envelope. Your server answers both without knowing or caring which is on the other end, which is the whole reason a version-negotiating protocol is worth the trouble. > [!WARNING] -> Running a model locally can be a little bit slow; keep that in mind after you send a message. Also, such a small model sometimes doesn't correctly call the tool. Ask again, or say "use the add tool" more insistently. If it reaches for a tool you never wrote, check that you disabled the `developer` extension above. If it never reaches for a tool at all, switch to a bigger model in the collapsible; the server and the protocol are not the problem. Either way, "goose connected and listed `add`, `shout` and `slow_shout`" appears in the session startup before the model does anything at all. +> Running a model locally can be a little bit slow; keep that in mind after you send a message. Also, such a small model sometimes doesn't correctly call the tool, or answers without repeating the sum. Ask again, or say "use the add tool" more insistently. If it reaches for a tool you never wrote, check that `GOOSE_PATH_ROOT` is set in the shell you started goose from. If it never reaches for a tool at all, switch to a bigger model in the collapsible; the server and the protocol are not the problem. Either way, "goose connected and listed `add`, `shout` and `slow_shout`" appears in the session startup before the model does anything at all. ## Send the work and come back for it @@ -615,6 +668,13 @@ Untagged: mcp-get-started-mcp-get-started:latest Deleted: sha256:e971ad962ecec954073aa4bc6af0b8d81dda6635fa7cfb418c29d45d7a88183d ``` +The derived model and goose's session files outlive the container, so drop them too: + +```bash +ollama rm qwen3-nothink +rm -rf Modelfile.nothink goose/data goose/state +``` + If you want to delete the lab code samples and instructions as well, run `rm -rf ~/zenable-labs`. Thanks for building with us! --- diff --git a/labs/mcp-get-started/pyproject.toml b/labs/mcp-get-started/pyproject.toml index 354e126..da3060d 100644 --- a/labs/mcp-get-started/pyproject.toml +++ b/labs/mcp-get-started/pyproject.toml @@ -4,7 +4,12 @@ version = "0" description = "An MVP MCP server and scripted client for the MCP 101 workshop" requires-python = ">=3.11" dependencies = [ - "fastmcp[tasks]>=4,<5", + # Deliberately not on the release cooldown the other labs use. This lab + # teaches the SEP-2663 tasks extension, which only exists in fastmcp 4 — + # released 2026-08-31, days before this floor. A cooldown on the very + # thing the lab is about would leave the lock unsatisfiable rather than + # safe, so this project resolves against current PyPI. + "fastmcp[tasks]>=4.0.2,<5", ] [tool.uv] diff --git a/labs/mcp-get-started/uv.lock b/labs/mcp-get-started/uv.lock index 9ff8de1..963630e 100644 --- a/labs/mcp-get-started/uv.lock +++ b/labs/mcp-get-started/uv.lock @@ -740,7 +740,7 @@ dependencies = [ ] [package.metadata] -requires-dist = [{ name = "fastmcp", extras = ["tasks"], specifier = ">=4,<5" }] +requires-dist = [{ name = "fastmcp", extras = ["tasks"], specifier = ">=4.0.2,<5" }] [[package]] name = "mcp-types"